diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-19 18:08:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-19 18:08:55 +0300 |
commit | 51238b2afe1ef4783b41368fa1e22b50f473c070 (patch) | |
tree | 8efb6e48b1edf709f051d03c6c5767f2d46730ab | |
parent | ba55ca9bc4bf2c85d2d78fcb11552ad130151110 (diff) |
Add latest changes from gitlab-org/gitlab@master
50 files changed, 844 insertions, 85 deletions
@@ -510,7 +510,7 @@ gem 'gitaly', '~> 15.9.0-rc3' # KAS GRPC protocol definitions gem 'kas-grpc', '~> 0.1.0' -gem 'grpc', '~> 1.54.2' +gem 'grpc', '~> 1.42.0' gem 'google-protobuf', '~> 3.23', '>= 3.23.1' diff --git a/Gemfile.checksum b/Gemfile.checksum index f507322ace0..bac1e6990fd 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -269,13 +269,12 @@ {"name":"graphql","version":"1.13.12","platform":"ruby","checksum":"1d82666cf201193a8d0cb54cea38576b820418db4869b549f61a35f3a2d97ac3"}, {"name":"graphql-client","version":"0.17.0","platform":"ruby","checksum":"5aaf02ce8f2dbc8e3ba05a7eaeb3ad9336762c4424c6093f4438fbb9490eeb5d"}, {"name":"graphql-docs","version":"2.1.0","platform":"ruby","checksum":"7eb82402f8fda455104b2b60364e9ada145d79d3121a8f915790d49da38bb576"}, -{"name":"grpc","version":"1.54.2","platform":"ruby","checksum":"036f931bcf8d43e1b65eb1c1dc010e1ab24fb64ed7e8703948a8288eda61812f"}, -{"name":"grpc","version":"1.54.2","platform":"x64-mingw-ucrt","checksum":"adbfa4b6004d229ecd75d8f33e07be8dcdf185b30263724288a75154dc9a3637"}, -{"name":"grpc","version":"1.54.2","platform":"x64-mingw32","checksum":"5c1c1ff87bdf928c442ec4c7166fb8abd4fcce00d7ec6e60313f488919f5d4f5"}, -{"name":"grpc","version":"1.54.2","platform":"x86-linux","checksum":"e8cf13e91714f33bba0ad06a3f4b9d98f056d09d7300e92ccb499cc95cc5cb9d"}, -{"name":"grpc","version":"1.54.2","platform":"x86-mingw32","checksum":"7e28b9a2e60d1fea6a5a774e20978628b27220ac9fe0a2a9c9e9cf2874700de4"}, -{"name":"grpc","version":"1.54.2","platform":"x86_64-darwin","checksum":"57b9effbdc25c43ebd706c4e9575baf31fd975a52cde67d41d8d33a7264e508e"}, -{"name":"grpc","version":"1.54.2","platform":"x86_64-linux","checksum":"d8ca04d8741b7d646c0634292f68925e95e29cad9805bcf19e820dd67d3e4bbb"}, +{"name":"grpc","version":"1.42.0","platform":"ruby","checksum":"b3d2649e67c6a636544996843d9ec191699c54c1aca797dbfea4dff36c14584a"}, +{"name":"grpc","version":"1.42.0","platform":"x64-mingw32","checksum":"6aac1b6576134b0a83e000b1269f60d502eb24aee96c64e2658c3f24f8e32ac0"}, +{"name":"grpc","version":"1.42.0","platform":"x86-linux","checksum":"4aa50538aa929f1f3bcefb11c65ee1a1606b5aef838ea4d4e93c100b5f4263a5"}, +{"name":"grpc","version":"1.42.0","platform":"x86-mingw32","checksum":"eeb2a9381bea43fafe879b6ddaa011351a44d0894d48bdc965a07bcb67c6eb56"}, +{"name":"grpc","version":"1.42.0","platform":"x86_64-darwin","checksum":"20fa202d46d8a055628260622e98fb6439529fbac283f0552af620b909f78535"}, +{"name":"grpc","version":"1.42.0","platform":"x86_64-linux","checksum":"92e2ceb2aca335d5755163dd8030082091d5b0e63c117b1ca07051b66c53eb2e"}, {"name":"gssapi","version":"1.3.1","platform":"ruby","checksum":"c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1"}, {"name":"guard","version":"2.16.2","platform":"ruby","checksum":"71ba7abaddecc8be91ab77bbaf78f767246603652ebbc7b976fda497ebdc8fbb"}, {"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"}, diff --git a/Gemfile.lock b/Gemfile.lock index 1c807080efc..c804e096235 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -750,8 +750,8 @@ GEM graphql (~> 1.12) html-pipeline (~> 2.9) sass (~> 3.4) - grpc (1.54.2) - google-protobuf (~> 3.21) + grpc (1.42.0) + google-protobuf (~> 3.18) googleapis-common-protos-types (~> 1.0) gssapi (1.3.1) ffi (>= 1.0.1) @@ -1777,7 +1777,7 @@ DEPENDENCIES graphlyte (~> 1.0.0) graphql (~> 1.13.12) graphql-docs (~> 2.1.0) - grpc (~> 1.54.2) + grpc (~> 1.42.0) gssapi (~> 1.3.1) guard-rspec haml_lint (~> 0.40.0) diff --git a/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql b/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql new file mode 100644 index 00000000000..6a16557fb8b --- /dev/null +++ b/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql @@ -0,0 +1,39 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getUserSnippets( + $id: UserID! + $first: Int + $last: Int + $afterToken: String + $beforeToken: String +) { + user(id: $id) { + id + avatarUrl + name + username + snippets(first: $first, last: $last, before: $beforeToken, after: $afterToken) { + pageInfo { + ...PageInfo + } + nodes { + id + title + webUrl + visibilityLevel + createdAt + updatedAt + blobs { + nodes { + name + } + } + commenters { + nodes { + id + } + } + } + } + } +} diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue index 8e52a98803d..fdc31edfe5f 100644 --- a/app/assets/javascripts/profile/components/profile_tabs.vue +++ b/app/assets/javascripts/profile/components/profile_tabs.vue @@ -11,7 +11,7 @@ import GroupsTab from './groups_tab.vue'; import ContributedProjectsTab from './contributed_projects_tab.vue'; import PersonalProjectsTab from './personal_projects_tab.vue'; import StarredProjectsTab from './starred_projects_tab.vue'; -import SnippetsTab from './snippets_tab.vue'; +import SnippetsTab from './snippets/snippets_tab.vue'; import FollowersTab from './followers_tab.vue'; import FollowingTab from './following_tab.vue'; diff --git a/app/assets/javascripts/profile/components/snippets/snippet_row.vue b/app/assets/javascripts/profile/components/snippets/snippet_row.vue new file mode 100644 index 00000000000..b2eed2a9189 --- /dev/null +++ b/app/assets/javascripts/profile/components/snippets/snippet_row.vue @@ -0,0 +1,21 @@ +<script> +export default { + name: 'SnippetRow', + props: { + snippet: { + type: Object, + required: true, + }, + userInfo: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div> + {{ snippet.title }} + </div> +</template> diff --git a/app/assets/javascripts/profile/components/snippets/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue new file mode 100644 index 00000000000..fce5e2f5e78 --- /dev/null +++ b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue @@ -0,0 +1,110 @@ +<script> +import { GlTab, GlKeysetPagination, GlEmptyState } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_USER } from '~/graphql_shared/constants'; +import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants'; +import getUserSnippets from '../graphql/get_user_snippets.query.graphql'; +import SnippetRow from './snippet_row.vue'; + +export default { + name: 'SnippetsTab', + i18n: { + title: s__('UserProfile|Snippets'), + noSnippets: s__('UserProfiles|No snippets found.'), + }, + components: { + GlTab, + GlKeysetPagination, + GlEmptyState, + SnippetRow, + }, + inject: ['userId', 'snippetsEmptyState'], + data() { + return { + userInfo: {}, + pageInfo: {}, + cursor: { + first: SNIPPET_MAX_LIST_COUNT, + last: null, + }, + }; + }, + apollo: { + userSnippets: { + query: getUserSnippets, + variables() { + return { + id: convertToGraphQLId(TYPENAME_USER, this.userId), + ...this.cursor, + }; + }, + update(data) { + this.userInfo = { + avatarUrl: data.user?.avatarUrl, + name: data.user?.name, + username: data.user?.username, + }; + this.pageInfo = data?.user?.snippets?.pageInfo; + return data?.user?.snippets?.nodes || []; + }, + error() { + return []; + }, + }, + }, + computed: { + hasSnippets() { + return this.userSnippets?.length; + }, + }, + methods: { + isLastSnippet(index) { + return index === this.userSnippets.length - 1; + }, + nextPage() { + this.cursor = { + first: SNIPPET_MAX_LIST_COUNT, + last: null, + afterToken: this.pageInfo.endCursor, + }; + }, + prevPage() { + this.cursor = { + first: null, + last: SNIPPET_MAX_LIST_COUNT, + beforeToken: this.pageInfo.startCursor, + }; + }, + }, +}; +</script> + +<template> + <gl-tab :title="$options.i18n.title"> + <template v-if="hasSnippets"> + <snippet-row + v-for="(snippet, index) in userSnippets" + :key="snippet.id" + :snippet="snippet" + :user-info="userInfo" + :class="{ 'gl-border-b': !isLastSnippet(index) }" + /> + <div class="gl-display-flex gl-justify-content-center gl-mt-6"> + <gl-keyset-pagination + v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage" + v-bind="pageInfo" + @prev="prevPage" + @next="nextPage" + /> + </div> + </template> + <template v-if="!hasSnippets"> + <gl-empty-state class="gl-mt-5" :svg-height="75" :svg-path="snippetsEmptyState"> + <template #title> + <p class="gl-font-weight-bold gl-mt-n5">{{ $options.i18n.noSnippets }}</p> + </template> + </gl-empty-state> + </template> + </gl-tab> +</template> diff --git a/app/assets/javascripts/profile/components/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets_tab.vue deleted file mode 100644 index d64c5b900a5..00000000000 --- a/app/assets/javascripts/profile/components/snippets_tab.vue +++ /dev/null @@ -1,17 +0,0 @@ -<script> -import { GlTab } from '@gitlab/ui'; -import { s__ } from '~/locale'; - -export default { - i18n: { - title: s__('UserProfile|Snippets'), - }, - components: { GlTab }, -}; -</script> - -<template> - <gl-tab :title="$options.i18n.title"> - <!-- placeholder --> - </gl-tab> -</template> diff --git a/app/assets/javascripts/profile/constants.js b/app/assets/javascripts/profile/constants.js index e19994c6784..9d3dcd648a8 100644 --- a/app/assets/javascripts/profile/constants.js +++ b/app/assets/javascripts/profile/constants.js @@ -5,3 +5,5 @@ export const CALENDAR_PERIOD_12_MONTHS = 12; * (see activity_calendar.js) */ export const OVERVIEW_CALENDAR_BREAKPOINT = 918; + +export const SNIPPET_MAX_LIST_COUNT = 20; diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js index 101e52c873e..894912d8e4b 100644 --- a/app/assets/javascripts/profile/index.js +++ b/app/assets/javascripts/profile/index.js @@ -13,10 +13,22 @@ export const initProfileTabs = () => { if (!el) return false; - const { followees, followers, userCalendarPath, utcOffset, userId } = el.dataset; + const { + followees, + followers, + userCalendarPath, + utcOffset, + userId, + snippetsEmptyState, + } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); return new Vue({ el, + apolloProvider, name: 'ProfileRoot', provide: { followees: parseInt(followers, 10), @@ -24,6 +36,7 @@ export const initProfileTabs = () => { userCalendarPath, utcOffset, userId, + snippetsEmptyState, }, render(createElement) { return createElement(ProfileTabs); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index f120680b440..fad0830388e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -146,6 +146,8 @@ export default { AddedCommitMessage, RelatedLinks, HelpPopover, + AiCommitMessage: () => + import('ee_component/vue_merge_request_widget/components/ai_commit_message.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -502,6 +504,10 @@ export default { this.squashCommitMessage = val; this.squashCommitMessageIsTouched = true; }, + appendCommitMessage(val) { + this.commitMessage = `${this.commitMessage}\n\n${val}`; + this.commitMessageIsTouched = true; + }, }, i18n: { mergeCommitTemplateHintText: s__( @@ -596,7 +602,15 @@ export default { input-id="merge-message-edit" class="gl-m-0! gl-p-0!" @input="setCommitMessage" - /> + > + <template #header> + <ai-commit-message + v-if="mr.aiCommitMessageEnabled" + :id="mr.id" + @update="appendCommitMessage" + /> + </template> + </commit-edit> <li class="gl-m-0! gl-p-0!"> <p class="form-text text-muted"> <gl-sprintf :message="commitTemplateHintText"> diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index c6edb16e5a1..c5dc45e2138 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -188,7 +188,8 @@ module UsersHelper followers: user.followers.count, user_calendar_path: user_calendar_path(user, :json), utc_offset: local_timezone_instance(user.timezone).now.utc_offset, - user_id: user.id + user_id: user.id, + snippets_empty_state: image_path('illustrations/empty-state/empty-snippets-md.svg') } end diff --git a/app/services/concerns/search/filter.rb b/app/services/concerns/search/filter.rb new file mode 100644 index 00000000000..c358f49eef1 --- /dev/null +++ b/app/services/concerns/search/filter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Search + module Filter + private + + def filters + { state: params[:state], confidential: params[:confidential] } + end + end +end + +Search::Filter.prepend_mod diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index cee59360b4b..2d4952dacfd 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -2,6 +2,7 @@ module Search class GlobalService + include Search::Filter include Gitlab::Utils::StrongMemoize ALLOWED_SCOPES = %w(issues merge_requests milestones users).freeze @@ -19,7 +20,7 @@ module Search projects, order_by: params[:order_by], sort: params[:sort], - filters: { state: params[:state], confidential: params[:confidential] }) + filters: filters) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index daed0df83f3..fa80a6ecf58 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -18,7 +18,7 @@ module Search group: group, order_by: params[:order_by], sort: params[:sort], - filters: { state: params[:state], confidential: params[:confidential] } + filters: filters ) end diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 6acc32ea0a8..d30c500df14 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -2,8 +2,8 @@ module Search class ProjectService + include Search::Filter include Gitlab::Utils::StrongMemoize - ALLOWED_SCOPES = %w(notes issues merge_requests milestones wiki_blobs commits users).freeze attr_accessor :project, :current_user, :params @@ -21,7 +21,7 @@ module Search repository_ref: params[:repository_ref], order_by: params[:order_by], sort: params[:sort], - filters: { confidential: params[:confidential], state: params[:state] } + filters: filters ) end diff --git a/config/feature_flags/development/generate_commit_message_flag.yml b/config/feature_flags/development/generate_commit_message_flag.yml new file mode 100644 index 00000000000..b0655fd9929 --- /dev/null +++ b/config/feature_flags/development/generate_commit_message_flag.yml @@ -0,0 +1,8 @@ +--- +name: generate_commit_message_flag +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120259 +rollout_issue_url: +milestone: '16.0' +type: development +group: group::code review +default_enabled: false diff --git a/config/feature_flags/development/generate_commit_message_vertex.yml b/config/feature_flags/development/generate_commit_message_vertex.yml new file mode 100644 index 00000000000..cee53bc7e71 --- /dev/null +++ b/config/feature_flags/development/generate_commit_message_vertex.yml @@ -0,0 +1,8 @@ +--- +name: generate_commit_message_vertex +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120259 +rollout_issue_url: +milestone: '16.0' +type: development +group: group::code review +default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d096174fca3..8db6a3bd6b5 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -789,6 +789,12 @@ Gitlab.ee do Settings.cron_jobs['sync_seat_link_worker'] ||= {} Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} #{rand(3..4)} * * * UTC" Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker' + Settings.cron_jobs['tanuki_bot_recreate_records_worker'] ||= {} + Settings.cron_jobs['tanuki_bot_recreate_records_worker']['cron'] ||= '0 5 * * 1,2,3,4,5' + Settings.cron_jobs['tanuki_bot_recreate_records_worker']['job_class'] ||= 'Llm::TanukiBot::RecreateRecordsWorker' + Settings.cron_jobs['tanuki_bot_remove_previous_records_worker'] ||= {} + Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['cron'] ||= '0 0 * * *' + Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['job_class'] ||= 'Llm::TanukiBot::RemovePreviousRecordsWorker' Settings.cron_jobs['users_create_statistics_worker'] ||= {} Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *' Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 30f630eb39b..e3ee44dcfda 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -311,6 +311,8 @@ - 2 - - llm_completion - 1 +- - llm_tanuki_bot_update + - 1 - - mail_scheduler - 2 - - mailers diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 103947da992..6cf95675fb4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1019,6 +1019,7 @@ Input type: `AiActionInput` | <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationaiactionexplaincode"></a>`explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. | | <a id="mutationaiactionexplainvulnerability"></a>`explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. | +| <a id="mutationaiactiongeneratecommitmessage"></a>`generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. | | <a id="mutationaiactiongeneratedescription"></a>`generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. | | <a id="mutationaiactiongeneratetestfile"></a>`generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. | | <a id="mutationaiactionmarkupformat"></a>`markupFormat` | [`MarkupFormat`](#markupformat) | Indicates the response format. | @@ -19206,7 +19207,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. | | <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. | | <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. | -| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.0. This feature is an Experiment. It can be changed or removed at any time. Fingerprint of the vulnerability finding. | +| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Deprecated** in 16.1. Use uuid instead. | | <a id="pipelinesecurityreportfindingremediations"></a>`remediations` | [`[VulnerabilityRemediationType!]`](#vulnerabilityremediationtype) | Remediations of the security report finding. | | <a id="pipelinesecurityreportfindingreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability finding. | | <a id="pipelinesecurityreportfindingscanner"></a>`scanner` | [`VulnerabilityScanner`](#vulnerabilityscanner) | Scanner metadata for the vulnerability. | @@ -27618,6 +27619,14 @@ see the associated mutation type above. | ---- | ---- | ----------- | | <a id="aiexplainvulnerabilityinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. | +### `AiGenerateCommitMessageInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="aigeneratecommitmessageinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. | + ### `AiGenerateDescriptionInput` #### Arguments diff --git a/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png b/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png Binary files differnew file mode 100644 index 00000000000..9353c5266e5 --- /dev/null +++ b/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md index ff4604b61bf..79660dcaac2 100644 --- a/doc/architecture/blueprints/ci_pipeline_components/index.md +++ b/doc/architecture/blueprints/ci_pipeline_components/index.md @@ -597,6 +597,29 @@ For example: index the content of `spec:` section for CI components. See an [example of development workflow](dev_workflow.md) for a components repository. +## Availability of CI catalog as a feature + +We plan to introduce 2 features of CI catalog as separate views: + +1. **Namespace Catalog (GitLab Ultimate):** allows organizations to share and discover catalog resources + created inside the top-level namespace. + Users will be able to access the Namespace Catalog from a project or subgroup inside the top-level + namespace. +1. **Community Catalog (GitLab free):** allows anyone in a GitLab instance to share and discover catalog + resources. The Community Catalog presents only resources/projects that are public. + +If a resource in a Namespace Catalog is made public (changing the project's visibility) the resource is +available in both Namespace Catalog (because it comes from there) as well as the Community Catalog +(because it's public). + +![Namespace and Community Catalogs](img/catalogs.png) + +There is only 1 CI catalog. The Namespace and Community Catalogs are different views of the CI catalog. + +Project admins are responsible for setting the project private or public. +The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog. +If the project is public it's visible to the world anyway. + ## Note about future resource types In the future, to support multiple types of resources in the Catalog we could diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md index 1c6a241d5f4..befde99cc37 100644 --- a/doc/user/product_analytics/index.md +++ b/doc/user/product_analytics/index.md @@ -10,12 +10,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - `cube_api_proxy` revised to only reference the [Product Analytics API](../../api/product_analytics.md) in GitLab 15.6. > - `cube_api_proxy` removed and replaced with `product_analytics_internal_preview` in GitLab 15.10. > - `product_analytics_internal_preview` replaced with `product_analytics_dashboards` in GitLab 15.11. +> - Snowplow integration introduced in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `product_analytics_snowplow_support`. Disabled by default. FLAG: On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_dashboards`. On GitLab.com, this feature is not available. This feature is not ready for production use. +FLAG: +On self-managed GitLab, by default the Snowplow integration is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_snowplow_support`. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + This page is a work in progress, and we're updating the information as we add more features. For more information, see the [group direction page](https://about.gitlab.com/direction/analytics/product-analytics/). @@ -165,6 +171,52 @@ create a `line_chart.yaml` file with the following required fields: - data - options +## Dashboards editor + +> Introduced in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `combined_analytics_dashboards_editor`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `combined_analytics_dashboards_editor`. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +NOTE: +This feature does not work in conjunction with the `product_analytics_snowplow_support` feature flag. + +You can use the dashboards editor to: + +- Create dashboards +- Rename dashboards +- Add visualizations to new and existing dashboards +- Resize or move panels within dashboards + +### Create a dashboard + +To create a dashboard: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Analytics > Dashboards**. +1. Select **New dashboard**. +1. In the **New dashboard** input, enter the name of the dashboard. +1. From the **Add visualizations** list on the right, select the visualizations to add to the dashboard. +1. Optional. Drag or resize the selected visualizations how you prefer. +1. Select **Save**. + +### Edit a dashboard + +You can rename your created dashboards and add or resize visualizations within them. + +To edit an existing dashboard: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Analytics > Dashboards**. +1. From the list of available dashboards, select the dashboard you want to edit. +1. Select **Edit**. +1. Optional. Change the name of the dashboard. +1. Optional. From the **Add visualizations** list on the right, select other visualizations to add to the dashboard. +1. Optional. In the dashboard, select a visualization and drag or resize it how you prefer. +1. Select **Save**. + ## Funnel analysis Use funnel analysis to understand the flow of users through your application, and where diff --git a/lib/gitlab/avatar_cache.rb b/lib/gitlab/avatar_cache.rb index ed00a279299..2bb3254ebef 100644 --- a/lib/gitlab/avatar_cache.rb +++ b/lib/gitlab/avatar_cache.rb @@ -66,9 +66,13 @@ module Gitlab Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do if ::Feature.enabled?(:use_pipeline_over_multikey) - redis.pipelined do |pipeline| - keys.each { |key| pipeline.unlink(key) } - end.sum + expired_count = 0 + keys.each_slice(1000) do |subset| + expired_count += redis.pipelined do |pipeline| + subset.each { |key| pipeline.unlink(key) } + end.sum + end + expired_count else redis.unlink(*keys) end diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb index c141398bee0..ab0df39e512 100644 --- a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb +++ b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb @@ -6,8 +6,7 @@ module Gitlab module GrpcErrorProcessor extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions - # Braces added by gRPC Ruby code: https://github.com/grpc/grpc/blob/0e38b075ffff72ab2ad5326e3f60ba6dcc234f46/src/ruby/lib/grpc/errors.rb#L46 - DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:\{(.*)\}') + DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)') class << self def call(event) diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb index dc13bb927e6..242c69ab057 100644 --- a/lib/gitlab/reactive_cache_set_cache.rb +++ b/lib/gitlab/reactive_cache_set_cache.rb @@ -11,17 +11,19 @@ module Gitlab end def clear_cache!(key) - use_pipeline = ::Feature.enabled?(:use_pipeline_over_multikey) - with do |redis| keys = read(key).map { |value| "#{cache_namespace}:#{value}" } keys << cache_key(key) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.pipelined do |pipeline| - if use_pipeline - keys.each { |key| pipeline.unlink(key) } - else + if ::Feature.enabled?(:use_pipeline_over_multikey) + keys.each_slice(1000) do |subset| + redis.pipelined do |pipeline| + subset.each { |key| pipeline.unlink(key) } + end + end + else + redis.pipelined do |pipeline| keys.each_slice(1000) { |subset| pipeline.unlink(*subset) } end end diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb index 623b254c4e0..09e07add293 100644 --- a/lib/gitlab/set_cache.rb +++ b/lib/gitlab/set_cache.rb @@ -23,9 +23,13 @@ module Gitlab Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do if ::Feature.enabled?(:use_pipeline_over_multikey) - redis.pipelined do |pipeline| - keys_to_expire.each { |key| pipeline.unlink(key) } - end.sum + expired_count = 0 + keys_to_expire.each_slice(1000) do |subset| + expired_count += redis.pipelined do |pipeline| + subset.each { |key| pipeline.unlink(key) } + end.sum + end + expired_count else redis.unlink(*keys_to_expire) end diff --git a/lib/gitlab/usage_data_counters/known_events/workspaces.yml b/lib/gitlab/usage_data_counters/known_events/workspaces.yml new file mode 100644 index 00000000000..8a96524b167 --- /dev/null +++ b/lib/gitlab/usage_data_counters/known_events/workspaces.yml @@ -0,0 +1,5 @@ +- name: users_updating_workspaces + aggregation: weekly + +- name: users_creating_workspaces + aggregation: weekly diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c6be174be47..8f709205234 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11050,6 +11050,9 @@ msgstr "" msgid "Commit message (optional)" msgstr "" +msgid "Commit message generated by AI" +msgstr "" + msgid "Commit statistics for %{ref} %{start_time} - %{end_time}" msgstr "" @@ -12592,6 +12595,9 @@ msgstr "" msgid "Create %{workspace} label" msgstr "" +msgid "Create AI-generated commit message" +msgstr "" + msgid "Create LLM-generated summary from diff(s)" msgstr "" @@ -23815,6 +23821,9 @@ msgstr "" msgid "Input the remote repository URL" msgstr "" +msgid "Insert" +msgstr "" + msgid "Insert a %{rows}×%{cols} table" msgstr "" @@ -45774,6 +45783,9 @@ msgstr "" msgid "There was an error gathering the chart data" msgstr "" +msgid "There was an error generating commit message." +msgstr "" + msgid "There was an error getting the epic participants." msgstr "" @@ -48918,6 +48930,9 @@ msgstr "" msgid "UserList|created %{timeago}" msgstr "" +msgid "UserProfiles|No snippets found." +msgstr "" + msgid "UserProfile|Activity" msgstr "" diff --git a/package.json b/package.json index 5f798734b0d..3b4a061d752 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@gitlab/cluster-client": "^1.2.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", - "@gitlab/svgs": "3.46.0", + "@gitlab/svgs": "3.47.0", "@gitlab/ui": "62.10.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230511143809", diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index a812b94957f..1dcb41de816 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -141,8 +141,8 @@ GEM google-apis-core (>= 0.7.2, < 2.a) google-apis-sqladmin_v1beta4 (0.36.0) google-apis-core (>= 0.7.2, < 2.a) - google-apis-storage_v1 (0.18.0) - google-apis-core (>= 0.7, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) googleauth (1.2.0) diff --git a/qa/qa/tools/migrate_influx_data_to_gcs.rb b/qa/qa/tools/migrate_influx_data_to_gcs.rb new file mode 100644 index 00000000000..3313131275a --- /dev/null +++ b/qa/qa/tools/migrate_influx_data_to_gcs.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'csv' +require "fog/google" + +module QA + module Tools + class MigrateInfluxDataToGcs + include Support::InfluxdbTools + + # Google Cloud Storage bucket from which Snowpipe would pull data into Snowflake + QA_GCS_BUCKET_NAME = ENV["QA_GCS_BUCKET_NAME"] || raise("Missing QA_GCS_BUCKET_NAME env variable") + QA_GCS_PROJECT_ID = ENV["QA_GCS_PROJECT_ID"] || raise("Missing QA_GCS_PROJECT_ID env variable") + QA_GCS_JSON_KEY_FILE = ENV["QA_GCS_JSON_KEY_FILE"] || raise("Missing QA_GCS_JSON_KEY_FILE env variable") + INFLUX_STATS_TYPE = %w[test-stats fabrication-stats].freeze + INFLUX_BUCKETS = [Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET, + Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET].freeze + TEST_STATS_FIELDS = %w[id testcase file_path name product_group stage job_id job_name + job_url pipeline_id pipeline_url merge_request merge_request_iid smoke reliable quarantined + retried retry_attempts run_time run_type status ui_fabrication api_fabrication total_fabrication].freeze + FABRICATION_STATS_FIELDS = %w[timestamp resource fabrication_method http_method run_type + merge_request fabrication_time info job_url].freeze + + def initialize(range) + @range = range.to_i + end + + # Run Influx Migrator + # @param [Integer] the last x hours for which data is required + # @return [void] + def self.run(range: 1) + migrator = new(range) + + migrator.migrate_data + end + + # Fetch data from Influx DB , store as CSV and upload to GCS + # @return [void] + def migrate_data + INFLUX_BUCKETS.each do |bucket| + INFLUX_STATS_TYPE.each do |stats_type| + if bucket == Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET && stats_type == "fabrication-stats" + break + end + + file_name = "#{bucket.end_with?('main') ? 'main' : 'all'}-#{stats_type}_#{Time.now.to_i}.csv" + influx_to_csv(bucket, stats_type, file_name) + + # Upload to Google Cloud Storage + upload_to_gcs(QA_GCS_BUCKET_NAME, file_name) + end + end + end + + private + + # FluxQL query used to fetch data + # @param [String] influx bucket to fetch data + # @param [String] Type of data to fetch + # @return [String] query string + def query(influx_bucket, stats_type) + <<~QUERY + from(bucket: "#{influx_bucket}") + |> range(start: -#{@range}h) + |> filter(fn: (r) => r["_measurement"] == "#{stats_type}") + |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") + |> drop(columns: ["_start", "_stop", "result", "table", "_time", "_measurement"]) + QUERY + end + + # Query InfluxDB and store in CSV + # @param [String] influx bucket to fetch data + # @param [String] Type of data to fetch + # @param [String] CSV filename to store data + # @return void + def influx_to_csv(influx_bucket, stats_type, data_file_name) + all_runs = query_api.query(query: query(influx_bucket, stats_type)) + CSV.open(data_file_name, "wb") do |csv| + stats_array = stats_type == "test-stats" ? TEST_STATS_FIELDS : FABRICATION_STATS_FIELDS + csv << stats_array.flatten + all_runs.each do |table| + table.records.each do |record| + csv << stats_array.map { |key| record.values[key] } + end + end + QA::Runtime::Logger.info("File #{data_file_name} contains #{all_runs.count} rows") + end + end + + # Fetch GCS Credentials + # @return [Hash] GCS Credentials + def gcs_credentials + json_key = ENV["QA_GCS_JSON_KEY_FILE"] || raise( + "QA_GCS_JSON_KEY_FILE env variable is required!" + ) + return { google_json_key_location: json_key } if File.exist?(json_key) + + { google_json_key_string: json_key } + end + + # Upload file to GCS + # @param [String] bucket to be uploaded to + # @param [String] path of file to be uploaded + # return void + def upload_to_gcs(bucket, file_path) + client = Fog::Storage::Google.new(google_project: QA_GCS_PROJECT_ID, **gcs_credentials) + file = client.put_object(bucket, file_path, File.new(file_path, "r")) + QA::Runtime::Logger.info("File #{file_path} uploaded to gs://#{bucket}/#{file.name}") + end + end + end +end diff --git a/qa/tasks/migrate_influx_data_to_gcs.rake b/qa/tasks/migrate_influx_data_to_gcs.rake new file mode 100644 index 00000000000..d6ac6e962f4 --- /dev/null +++ b/qa/tasks/migrate_influx_data_to_gcs.rake @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +desc "Migrate the test results data from InfluxDB to GCS to visualise in Sisense/Tableau" +task :influx_to_gcs, [:range] do |_task, args| + QA::Tools::MigrateInfluxDataToGcs.run(**args) +end diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js index 80a1ff422ab..f3dda2e205f 100644 --- a/spec/frontend/profile/components/profile_tabs_spec.js +++ b/spec/frontend/profile/components/profile_tabs_spec.js @@ -10,7 +10,7 @@ import GroupsTab from '~/profile/components/groups_tab.vue'; import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue'; import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue'; import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue'; -import SnippetsTab from '~/profile/components/snippets_tab.vue'; +import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue'; import FollowersTab from '~/profile/components/followers_tab.vue'; import FollowingTab from '~/profile/components/following_tab.vue'; import waitForPromises from 'helpers/wait_for_promises'; diff --git a/spec/frontend/profile/components/snippets/snippet_row_spec.js b/spec/frontend/profile/components/snippets/snippet_row_spec.js new file mode 100644 index 00000000000..0c0e262bd9a --- /dev/null +++ b/spec/frontend/profile/components/snippets/snippet_row_spec.js @@ -0,0 +1,31 @@ +import SnippetRow from '~/profile/components/snippets/snippet_row.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { MOCK_USER, MOCK_SNIPPET } from 'jest/profile/mock_data'; + +describe('UserProfileSnippetRow', () => { + let wrapper; + + const defaultProps = { + userInfo: MOCK_USER, + snippet: MOCK_SNIPPET, + }; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(SnippetRow, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders snippet title', () => { + expect(wrapper.text()).toBe(MOCK_SNIPPET.title); + }); + }); +}); diff --git a/spec/frontend/profile/components/snippets/snippets_tab_spec.js b/spec/frontend/profile/components/snippets/snippets_tab_spec.js new file mode 100644 index 00000000000..47e2fbcf2c0 --- /dev/null +++ b/spec/frontend/profile/components/snippets/snippets_tab_spec.js @@ -0,0 +1,162 @@ +import { GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_USER } from '~/graphql_shared/constants'; +import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants'; +import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue'; +import SnippetRow from '~/profile/components/snippets/snippet_row.vue'; +import getUserSnippets from '~/profile/components/graphql/get_user_snippets.query.graphql'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { + MOCK_USER, + MOCK_SNIPPETS_EMPTY_STATE, + MOCK_USER_SNIPPETS_RES, + MOCK_USER_SNIPPETS_PAGINATION_RES, + MOCK_USER_SNIPPETS_EMPTY_RES, +} from 'jest/profile/mock_data'; + +Vue.use(VueApollo); + +describe('UserProfileSnippetsTab', () => { + let wrapper; + + let queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES); + + const createComponent = () => { + const apolloProvider = createMockApollo([[getUserSnippets, queryHandlerMock]]); + + wrapper = shallowMountExtended(SnippetsTab, { + apolloProvider, + provide: { + userId: MOCK_USER.id, + snippetsEmptyState: MOCK_SNIPPETS_EMPTY_STATE, + }, + }); + }; + + const findSnippetRows = () => wrapper.findAllComponents(SnippetRow); + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination); + + describe('when user has no snippets', () => { + beforeEach(async () => { + queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_EMPTY_RES); + createComponent(); + + await nextTick(); + }); + + it('does not render snippet row', () => { + expect(findSnippetRows().exists()).toBe(false); + }); + + it('does render empty state with correct svg', () => { + expect(findGlEmptyState().exists()).toBe(true); + expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE); + }); + }); + + describe('when snippets returns an error', () => { + beforeEach(async () => { + queryHandlerMock = jest.fn().mockRejectedValue({ errors: [] }); + createComponent(); + + await nextTick(); + }); + + it('does not render snippet row', () => { + expect(findSnippetRows().exists()).toBe(false); + }); + + it('does render empty state with correct svg', () => { + expect(findGlEmptyState().exists()).toBe(true); + expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE); + }); + }); + + describe('when snippets are returned', () => { + beforeEach(async () => { + queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES); + createComponent(); + + await nextTick(); + }); + + it('renders a snippet row for each snippet', () => { + expect(findSnippetRows().exists()).toBe(true); + expect(findSnippetRows().length).toBe(MOCK_USER_SNIPPETS_RES.data.user.snippets.nodes.length); + }); + + it('does not render empty state', () => { + expect(findGlEmptyState().exists()).toBe(false); + }); + + it('adds bottom border when snippet is not last in list', () => { + expect(findSnippetRows().at(0).classes('gl-border-b')).toBe(true); + }); + + it('does not add bottom border when snippet is last in list', () => { + expect( + findSnippetRows() + .at(MOCK_USER_SNIPPETS_RES.data.user.snippets.nodes.length - 1) + .classes('gl-border-b'), + ).toBe(false); + }); + }); + + describe('Snippet Pagination', () => { + describe('when user has one page of snippets', () => { + beforeEach(async () => { + queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES); + createComponent(); + + await nextTick(); + }); + + it('does not render pagination', () => { + expect(findGlKeysetPagination().exists()).toBe(false); + }); + }); + + describe('when user has multiple pages of snippets', () => { + beforeEach(async () => { + queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_PAGINATION_RES); + createComponent(); + + await nextTick(); + }); + + it('does render pagination', () => { + expect(findGlKeysetPagination().exists()).toBe(true); + }); + + it('when nextPage is clicked', async () => { + findGlKeysetPagination().vm.$emit('next'); + + await nextTick(); + + expect(queryHandlerMock).toHaveBeenCalledWith({ + id: convertToGraphQLId(TYPENAME_USER, MOCK_USER.id), + first: SNIPPET_MAX_LIST_COUNT, + last: null, + afterToken: MOCK_USER_SNIPPETS_RES.data.user.snippets.pageInfo.endCursor, + }); + }); + + it('when previousPage is clicked', async () => { + findGlKeysetPagination().vm.$emit('prev'); + + await nextTick(); + + expect(queryHandlerMock).toHaveBeenCalledWith({ + id: convertToGraphQLId(TYPENAME_USER, MOCK_USER.id), + first: null, + last: SNIPPET_MAX_LIST_COUNT, + beforeToken: MOCK_USER_SNIPPETS_RES.data.user.snippets.pageInfo.startCursor, + }); + }); + }); + }); +}); diff --git a/spec/frontend/profile/components/snippets_tab_spec.js b/spec/frontend/profile/components/snippets_tab_spec.js deleted file mode 100644 index 1306757314c..00000000000 --- a/spec/frontend/profile/components/snippets_tab_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { GlTab } from '@gitlab/ui'; - -import { s__ } from '~/locale'; -import SnippetsTab from '~/profile/components/snippets_tab.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; - -describe('SnippetsTab', () => { - let wrapper; - - const createComponent = () => { - wrapper = shallowMountExtended(SnippetsTab); - }; - - it('renders `GlTab` and sets `title` prop', () => { - createComponent(); - - expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets')); - }); -}); diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js index 7106ea84619..74387af5be7 100644 --- a/spec/frontend/profile/mock_data.js +++ b/spec/frontend/profile/mock_data.js @@ -20,3 +20,79 @@ export const userCalendarResponse = { '2023-02-06': 2, '2023-02-07': 2, }; + +export const MOCK_SNIPPETS_EMPTY_STATE = 'illustrations/empty-state/empty-snippets-md.svg'; + +export const MOCK_USER = { + id: '1', + avatarUrl: 'https://www.gravatar.com/avatar/test', + name: 'Test User', + username: 'test', +}; + +const getMockSnippet = (id) => { + return { + id: `gid://gitlab/PersonalSnippet/${id}`, + title: `Test snippet ${id}`, + visibilityLevel: 'public', + webUrl: `http://gitlab.com/-/snippets/${id}`, + createdAt: new Date(), + updatedAt: new Date(), + blobs: { + nodes: [ + { + name: 'test.txt', + }, + ], + }, + commenters: { + nodes: [ + { + id: 'git://gitlab/User/1', + }, + ], + }, + }; +}; + +const MOCK_PAGE_INFO = { + startCursor: 'asdfqwer', + endCursor: 'reqwfdsa', + __typename: 'PageInfo', +}; + +const getMockSnippetRes = (hasPagination) => { + return { + data: { + user: { + ...MOCK_USER, + snippets: { + pageInfo: { + ...MOCK_PAGE_INFO, + hasNextPage: hasPagination, + hasPreviousPage: hasPagination, + }, + nodes: [getMockSnippet(1), getMockSnippet(2)], + }, + }, + }, + }; +}; + +export const MOCK_SNIPPET = getMockSnippet(1); +export const MOCK_USER_SNIPPETS_RES = getMockSnippetRes(false); +export const MOCK_USER_SNIPPETS_PAGINATION_RES = getMockSnippetRes(true); +export const MOCK_USER_SNIPPETS_EMPTY_RES = { + data: { + user: { + ...MOCK_USER, + snippets: { + pageInfo: { + endCursor: null, + startCursor: null, + }, + nodes: [], + }, + }, + }, +}; diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index 0a259b80219..74b90ce3c6e 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -502,12 +502,13 @@ RSpec.describe UsersHelper do end it 'returns expected hash' do - expect(helper.user_profile_tabs_app_data(user)).to eq({ + expect(helper.user_profile_tabs_app_data(user)).to match({ followees: 3, followers: 2, user_calendar_path: '/users/root/calendar.json', utc_offset: 0, - user_id: user.id + user_id: user.id, + snippets_empty_state: match_asset_path('illustrations/empty-state/empty-snippets-md.svg') }) end end diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb index a57d811edaf..db2fcf9d140 100644 --- a/spec/lib/gitlab/avatar_cache_spec.rb +++ b/spec/lib/gitlab/avatar_cache_spec.rb @@ -100,6 +100,16 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do end end + context 'when deleting over 1000 emails' do + it 'deletes in batches of 1000' do + Gitlab::Redis::Cache.with do |redis| + expect(redis).to receive(:pipelined).twice.and_call_original + end + + described_class.delete_by_email(*(Array.new(1001) { |i| i })) + end + end + context 'when feature flag disabled' do before do stub_feature_flags(use_pipeline_over_multikey: false) diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb index 3399c6dd9f4..33d322d0d44 100644 --- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry, feature_category: :integrations do +RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry do describe '.call' do let(:raven_required_options) do { diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb index a78d15134fa..1a71b23533e 100644 --- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb +++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb @@ -68,6 +68,20 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do it_behaves_like 'clears cache' end + context 'when key size is large' do + before do + 1001.times { |i| cache.write(cache_prefix, i) } + end + + it 'sends multiple pipelines of 1000 unlinks' do + Gitlab::Redis::Cache.with do |redis| + expect(redis).to receive(:pipelined).twice.and_call_original + end + + cache.clear_cache!(cache_prefix) + end + end + it_behaves_like 'clears cache' end diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb index 65a50b68c44..df980b50d03 100644 --- a/spec/lib/gitlab/repository_set_cache_spec.rb +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -117,6 +117,16 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do end end + context 'when deleting over 1000 keys' do + it 'deletes in batches of 1000' do + Gitlab::Redis::RepositoryCache.with do |redis| + expect(redis).to receive(:pipelined).twice.and_call_original + end + + cache.expire(*(Array.new(1001) { |i| i })) + end + end + context 'when feature flag is disabled' do before do stub_feature_flags(use_pipeline_over_multikey: false) diff --git a/spec/support/helpers/database/multiple_databases_helpers.rb b/spec/support/helpers/database/multiple_databases_helpers.rb index 3c9a5762c47..806956ca648 100644 --- a/spec/support/helpers/database/multiple_databases_helpers.rb +++ b/spec/support/helpers/database/multiple_databases_helpers.rb @@ -69,8 +69,10 @@ module Database config_model: base_model ) - delete_from_all_tables!(except: deletion_except_tables) + # Delete after migrating so that rows created during migration don't impact other + # specs (for example, async foreign key creation rows) schema_migrate_up! + delete_from_all_tables!(except: deletion_except_tables) end end diff --git a/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb new file mode 100644 index 00000000000..b7e408415c3 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search results filtered by labels' do + let(:project_label) { create(:label, project: project) } + let!(:issue_1) { create(:labeled_issue, labels: [project_label], project: project, title: 'foo project') } + let!(:unlabeled_issue) { create(:issue, project: project, title: 'foo unlabeled') } + + let(:filters) { { labels: [project_label.id] } } + + before do + ensure_elasticsearch_index! + end + + subject(:issue_results) { results.objects(scope) } + + it 'filters by labels', :sidekiq_inline do + expect(issue_results).to contain_exactly(issue_1) + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 16173e322b6..425bbd9278a 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -345,6 +345,8 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'JiraConnect::SyncProjectWorker' => 3, 'LdapGroupSyncWorker' => 3, 'Licenses::ResetSubmitLicenseUsageDataBannerWorker' => 13, + 'Llm::TanukiBot::UpdateWorker' => 1, + 'Llm::TanukiBot::RecreateRecordsWorker' => 3, 'MailScheduler::IssueDueWorker' => 3, 'MailScheduler::NotificationServiceWorker' => 3, 'MembersDestroyer::UnassignIssuablesWorker' => 3, diff --git a/workhorse/go.mod b/workhorse/go.mod index 20daa9e4843..6a6f756eeef 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -25,7 +25,7 @@ require ( github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sirupsen/logrus v1.9.0 github.com/smartystreets/goconvey v1.7.2 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.3 gitlab.com/gitlab-org/gitaly/v15 v15.11.0 gitlab.com/gitlab-org/labkit v1.18.0 gocloud.dev v0.29.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index 6ab1ff99c4c..94c11568369 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1862,8 +1862,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= diff --git a/yarn.lock b/yarn.lock index 134e383cc4c..3f8333fbf76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1110,10 +1110,10 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@3.46.0": - version "3.46.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.46.0.tgz#741fea428ce9cac9fd8ccdb65a7b863ab5f7773d" - integrity sha512-+NEdjNTBCTnJIjQKomf6yasT3ezg8UNBGJVUDf+ZgXgENKbwOjV9ngcVeHQMZM4hDaQSaJx08fagyadCcTw0Ow== +"@gitlab/svgs@3.47.0": + version "3.47.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.47.0.tgz#1a18f008aef1ecb5407688017c3bbdbc597b7ec1" + integrity sha512-xP8AyuFYRFmlxtcBYRqCnLmBgMjrACa0mUliRk/hAKUWcXoz/U4vdK69T1DhWalVi4cpUqmi4+rrIWI6fBdzew== "@gitlab/ui@62.10.0": version "62.10.0" |