diff --git a/app/graphql/types/ci/catalog/resource_sort_enum.rb b/app/graphql/types/ci/catalog/resource_sort_enum.rb index 04af70137a2f594cb4ee6efb0c896a18d83906b7..454414923a01785838a6064292fb0c314e500993 100644 --- a/app/graphql/types/ci/catalog/resource_sort_enum.rb +++ b/app/graphql/types/ci/catalog/resource_sort_enum.rb @@ -15,6 +15,8 @@ class ResourceSortEnum < BaseEnum value 'CREATED_DESC', 'Created date by descending order.', value: :created_at_desc value 'STAR_COUNT_ASC', 'Star count by ascending order.', value: :star_count_asc value 'STAR_COUNT_DESC', 'Star count by descending order.', value: :star_count_desc + value 'USAGE_COUNT_ASC', 'Last 30-day usage count by ascending order.', value: :usage_count_asc + value 'USAGE_COUNT_DESC', 'Last 30-day usage count by descending order.', value: :usage_count_desc end end end diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb index c35699e6e01d2dbf50926e3776334c5cde105032..e4d0fff0f2e7c243f5e31d6a159895be7350b9e9 100644 --- a/app/graphql/types/ci/catalog/resource_type.rb +++ b/app/graphql/types/ci/catalog/resource_type.rb @@ -57,6 +57,11 @@ class ResourceType < BaseObject description: 'Relative path to the starrers page for the catalog resource project.', alpha: { milestone: '16.10' } + field :last_30_day_usage_count, GraphQL::Types::Int, null: false, + description: 'Number of projects that used a component from this catalog resource in a pipeline, by using ' \ + '`include:component`, in the last 30 days.', + alpha: { milestone: '17.0' } + def open_issues_count BatchLoader::GraphQL.wrap(object.project.open_issues_count) end diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index 72af569b170ce49bb59415eca21fc6ec6a52f84b..1f3cc4c559e24d5e13ef5bbe2752aa7e6eb69b0e 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -22,6 +22,8 @@ def resources(sort: nil, search: nil, scope: :all) when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc when 'created_at_asc' then relation.order_by_created_at_asc when 'created_at_desc' then relation.order_by_created_at_desc + when 'usage_count_asc' then relation.order_by_last_30_day_usage_count_asc + when 'usage_count_desc' then relation.order_by_last_30_day_usage_count_desc when 'star_count_asc' then relation.order_by_star_count(:asc) else relation.order_by_star_count(:desc) diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index b50aac09cedb383597429f9f1ecbfb4f135b118d..f7faef5ff2840d226b1f2abbac1c462dc1c6d760 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -48,6 +48,11 @@ class Resource < ::ApplicationRecord ) end + # TODO: The usage counts will be populated by a worker that aggregates the data daily. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/452545. + scope :order_by_last_30_day_usage_count_desc, -> { reorder(last_30_day_usage_count: :desc) } + scope :order_by_last_30_day_usage_count_asc, -> { reorder(last_30_day_usage_count: :asc) } + delegate :avatar_path, :star_count, :full_path, to: :project enum state: { unpublished: 0, published: 1 } diff --git a/app/models/ci/catalog/resources/components/usage.rb b/app/models/ci/catalog/resources/components/usage.rb index b721e57d0a75eadb56aa380555ce346b979dfc0c..6ec2b7397022e575cf59dc54b10c889c0c4afe1f 100644 --- a/app/models/ci/catalog/resources/components/usage.rb +++ b/app/models/ci/catalog/resources/components/usage.rb @@ -4,9 +4,11 @@ module Ci module Catalog module Resources module Components - # This model is used to track when a project includes a component in a pipeline. - # The column `used_by_project_id` does not have an FK constraint because we want - # to preserve historical usage data. + # This model is used to track when a project includes a catalog component in + # a pipeline with the keyword `include:component`. Usage data is recorded + # during pipeline creation in Gitlab::Ci::Pipeline::Chain::ComponentUsage. + # The column `used_by_project_id` does not have an FK constraint because + # we want to preserve historical usage data. class Usage < ::ApplicationRecord include PartitionedTable diff --git a/db/post_migrate/20240504042340_add_index_catalog_resources_on_usage_count.rb b/db/post_migrate/20240504042340_add_index_catalog_resources_on_usage_count.rb new file mode 100644 index 0000000000000000000000000000000000000000..6e844f3fcc74620744226258e31cb404ae56b8a3 --- /dev/null +++ b/db/post_migrate/20240504042340_add_index_catalog_resources_on_usage_count.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexCatalogResourcesOnUsageCount < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + + milestone '17.0' + + INDEX_NAME = 'index_catalog_resources_on_last_30_day_usage_count' + + def up + add_concurrent_index :catalog_resources, :last_30_day_usage_count, where: 'state = 1', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :catalog_resources, INDEX_NAME + end +end diff --git a/db/schema_migrations/20240504042340 b/db/schema_migrations/20240504042340 new file mode 100644 index 0000000000000000000000000000000000000000..4132cb4ef4f7955823e9a6318b5b231c15e3116e --- /dev/null +++ b/db/schema_migrations/20240504042340 @@ -0,0 +1 @@ +41bc8518c30c6a10db8eae14b971418ef6ae65cf6cb70ddd42f72ecd848a01b4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e0568bd8061618c7b7f1b725101bebfbfe3d3460..a3f06e99b24739d7f38e246b16c632ff1c8da2f8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24657,6 +24657,8 @@ CREATE UNIQUE INDEX index_catalog_resource_versions_on_release_id ON catalog_res CREATE INDEX index_catalog_resource_versions_on_resource_id_and_released_at ON catalog_resource_versions USING btree (catalog_resource_id, released_at); +CREATE INDEX index_catalog_resources_on_last_30_day_usage_count ON catalog_resources USING btree (last_30_day_usage_count) WHERE (state = 1); + CREATE UNIQUE INDEX index_catalog_resources_on_project_id ON catalog_resources USING btree (project_id); CREATE INDEX index_catalog_resources_on_search_vector ON catalog_resources USING gin (search_vector); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ee1edeb80baac764cdc4eac115113d390df7bc4e..1bc4bcf821f2a98d95d8aef40e3ac4dd1017b846 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -17154,6 +17154,7 @@ Represents the total number of issues and their weights for a particular day. | <a id="cicatalogresourcefullpath"></a>`fullPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Full project path of the catalog resource. | | <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 15.11. **Status**: Experiment. Icon for the catalog resource. | | <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in GitLab 15.11. **Status**: Experiment. ID of the catalog resource. | +| <a id="cicatalogresourcelast30dayusagecount"></a>`last30DayUsageCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 17.0. **Status**: Experiment. Number of projects that used a component from this catalog resource in a pipeline, by using `include:component`, in the last 30 days. | | <a id="cicatalogresourcelatestreleasedat"></a>`latestReleasedAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in GitLab 16.5. **Status**: Experiment. Release date of the catalog resource's latest version. | | <a id="cicatalogresourcename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 15.11. **Status**: Experiment. Name of the catalog resource. | | <a id="cicatalogresourceopenissuescount"></a>`openIssuesCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 16.3. **Status**: Experiment. Count of open issues that belong to the the catalog resource. | @@ -32494,6 +32495,8 @@ Values for sorting catalog resources. | <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. | | <a id="cicatalogresourcesortstar_count_asc"></a>`STAR_COUNT_ASC` | Star count by ascending order. | | <a id="cicatalogresourcesortstar_count_desc"></a>`STAR_COUNT_DESC` | Star count by descending order. | +| <a id="cicatalogresourcesortusage_count_asc"></a>`USAGE_COUNT_ASC` | Last 30-day usage count by ascending order. | +| <a id="cicatalogresourcesortusage_count_desc"></a>`USAGE_COUNT_DESC` | Last 30-day usage count by descending order. | ### `CiCatalogResourceVerificationLevel` diff --git a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb index 020b00e704346e547bf6a547acdd4718283599e3..122757c4f6ed79331331f8724ce76630deb265eb 100644 --- a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb +++ b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb @@ -8,7 +8,7 @@ it 'exposes all the existing catalog resource sort orders' do expect(described_class.values.keys).to include( *%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC CREATED_ASC CREATED_DESC - STAR_COUNT_ASC STAR_COUNT_DESC] + STAR_COUNT_ASC STAR_COUNT_DESC USAGE_COUNT_ASC USAGE_COUNT_DESC] ) end end diff --git a/spec/graphql/types/ci/catalog/resource_type_spec.rb b/spec/graphql/types/ci/catalog/resource_type_spec.rb index f81d885e31a74cde6457e50fd8ff03f16228c958..a2c701537c628f07c74e98922cf19e7a6223d640 100644 --- a/spec/graphql/types/ci/catalog/resource_type_spec.rb +++ b/spec/graphql/types/ci/catalog/resource_type_spec.rb @@ -20,6 +20,7 @@ starrers_path open_issues_count open_merge_requests_count + last_30_day_usage_count ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb index f052f9580def8336b134013d79d7c8993e30ff63..9cff299c9ce7a29a6adb0908f93fd35c7212f0e4 100644 --- a/spec/models/ci/catalog/listing_spec.rb +++ b/spec/models/ci/catalog/listing_spec.rb @@ -35,10 +35,22 @@ let(:params) { {} } - let_it_be(:public_resource_a) { create(:ci_catalog_resource, :published, project: public_namespace_project) } - let_it_be(:public_resource_b) { create(:ci_catalog_resource, :published, project: public_project) } - let_it_be(:internal_resource) { create(:ci_catalog_resource, :published, project: internal_project) } - let_it_be(:private_namespace_resource) { create(:ci_catalog_resource, :published, project: namespace_project_a) } + let_it_be(:public_resource_a) do + create(:ci_catalog_resource, :published, project: public_namespace_project, last_30_day_usage_count: 100) + end + + let_it_be(:public_resource_b) do + create(:ci_catalog_resource, :published, project: public_project, last_30_day_usage_count: 70) + end + + let_it_be(:internal_resource) do + create(:ci_catalog_resource, :published, project: internal_project, last_30_day_usage_count: 80) + end + + let_it_be(:private_namespace_resource) do + create(:ci_catalog_resource, :published, project: namespace_project_a, last_30_day_usage_count: 90) + end + let_it_be(:unpublished_resource) { create(:ci_catalog_resource, project: namespace_project_b) } it 'by default returns all resources visible to the current user' do @@ -158,6 +170,22 @@ is_expected.to eq([internal_resource, public_resource_a, public_resource_b, private_namespace_resource]) end end + + context 'when the sort is usage_count descending' do + let_it_be(:sort) { :usage_count_desc } + + it 'contains catalog resources sorted by last_30_day_usage_count descending' do + is_expected.to eq([public_resource_a, private_namespace_resource, internal_resource, public_resource_b]) + end + end + + context 'when the sort is usage_count ascending' do + let_it_be(:sort) { :usage_count_asc } + + it 'contains catalog resources sorted by last_30_day_usage_count ascending' do + is_expected.to eq([public_resource_b, internal_resource, private_namespace_resource, public_resource_a]) + end + end end end diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb index f07a9faeed94adab51d1df8ae8e3dd4368e2e7d0..dcb140e1fb84dbeb87c53ca2edc56b0047f17c93 100644 --- a/spec/models/ci/catalog/resource_spec.rb +++ b/spec/models/ci/catalog/resource_spec.rb @@ -10,11 +10,13 @@ let_it_be(:project_c) { create(:project, name: 'C', description: 'B', star_count: 30) } let_it_be_with_reload(:resource_a) do - create(:ci_catalog_resource, project: project_a, latest_released_at: '2023-02-01T00:00:00Z') + create(:ci_catalog_resource, project: project_a, latest_released_at: '2023-02-01T00:00:00Z', + last_30_day_usage_count: 150) end let_it_be(:resource_b) do - create(:ci_catalog_resource, project: project_b, latest_released_at: '2023-01-01T00:00:00Z') + create(:ci_catalog_resource, project: project_b, latest_released_at: '2023-01-01T00:00:00Z', + last_30_day_usage_count: 100) end let_it_be(:resource_c) { create(:ci_catalog_resource, project: project_c) } @@ -144,6 +146,22 @@ end end + describe 'order_by_last_30_day_usage_count_desc' do + it 'returns catalog resources sorted by last 30-day usage count in descending order' do + ordered_resources = described_class.order_by_last_30_day_usage_count_desc + + expect(ordered_resources).to eq([resource_a, resource_b, resource_c]) + end + end + + describe 'order_by_last_30_day_usage_count_asc' do + it 'returns catalog resources sorted by last 30-day usage count in ascending order' do + ordered_resources = described_class.order_by_last_30_day_usage_count_asc + + expect(ordered_resources).to eq([resource_c, resource_b, resource_a]) + end + end + describe 'authorized catalog resources' do let_it_be(:namespace) { create(:group) } let_it_be(:other_namespace) { create(:group) } diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb index c3d8edb2175fb72138e332108567da7f49d6148c..facf96f60ea92a85be4234438ec1c0af321edfa4 100644 --- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -22,7 +22,9 @@ ) end - let_it_be_with_reload(:resource) { create(:ci_catalog_resource, :published, project: project) } + let_it_be_with_reload(:resource) do + create(:ci_catalog_resource, :published, project: project, last_30_day_usage_count: 15) + end let(:query) do <<~GQL @@ -47,7 +49,8 @@ fullPath: project.full_path, webPath: "/#{project.full_path}", verificationLevel: "UNVERIFIED", - starCount: project.star_count + starCount: project.star_count, + last30DayUsageCount: resource.last_30_day_usage_count ) ) end diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb index fe071ea575819ad1587a337480b6a824da58db60..f0488ef23f6c7093b1773aa7f4e15c58e187dc3c 100644 --- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -30,7 +30,8 @@ end let_it_be(:resource1) do - create(:ci_catalog_resource, :published, project: project1, latest_released_at: '2023-01-01T00:00:00Z') + create(:ci_catalog_resource, :published, project: project1, latest_released_at: '2023-01-01T00:00:00Z', + last_30_day_usage_count: 15) end let_it_be(:public_resource) { create(:ci_catalog_resource, :published, project: public_project) } @@ -50,6 +51,7 @@ latestReleasedAt starCount starrersPath + last30DayUsageCount } } } @@ -88,7 +90,8 @@ starrersPath: Gitlab::Routing.url_helpers.project_starrers_path(project1), verificationLevel: 'UNVERIFIED', fullPath: project1.full_path, - webPath: "/#{project1.full_path}" + webPath: "/#{project1.full_path}", + last30DayUsageCount: resource1.last_30_day_usage_count ), a_graphql_entity_for(public_resource) )