Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-03 03:09:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-03 03:09:11 +0300
commita0754ad291e60e9411897ae4e05e01a600037ee9 (patch)
treed23b71bd0d4db9245aaa12d2fc81da20bcf0f105
parent90693cc231ba6e1645dc57f2a9111a7b5a5ceae0 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/commons/nav/user_merge_requests.js12
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/merge_request_menu.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue25
-rw-r--r--app/assets/javascripts/super_sidebar/user_counts_manager.js69
-rw-r--r--app/helpers/sidebars_helper.rb29
-rw-r--r--app/services/draft_notes/publish_service.rb3
-rw-r--r--app/services/notes/create_service.rb16
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/merge_requests/set_reviewer_reviewed_worker.rb30
-rw-r--r--config/feature_flags/development/ai_git_command_ff.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--data/removals/16_0/16-0-redis-5.yml14
-rw-r--r--doc/administration/reference_architectures/10k_users.md6
-rw-r--r--doc/administration/reference_architectures/25k_users.md6
-rw-r--r--doc/administration/reference_architectures/2k_users.md8
-rw-r--r--doc/administration/reference_architectures/3k_users.md5
-rw-r--r--doc/administration/reference_architectures/50k_users.md6
-rw-r--r--doc/administration/reference_architectures/5k_users.md5
-rw-r--r--doc/development/ai_architecture.md108
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/install/requirements.md5
-rw-r--r--doc/tutorials/left_sidebar/img/explore_v16_0.pngbin0 -> 13189 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/pin_v16_0.pngbin0 -> 2295 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/pinned_v16_0.pngbin0 -> 1475 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/project_selected_v16_0.pngbin0 -> 24164 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/search_projects_v16_0.pngbin0 -> 40408 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/shortcuts_v16_0.pngbin0 -> 1180 bytes
-rw-r--r--doc/tutorials/left_sidebar/img/your_work_v16_0.pngbin0 -> 20880 bytes
-rw-r--r--doc/tutorials/left_sidebar/index.md72
-rw-r--r--doc/update/removals.md11
-rw-r--r--lib/gitlab/event_store.rb1
-rw-r--r--package.json2
-rw-r--r--spec/frontend/commons/nav/user_merge_requests_spec.js33
-rw-r--r--spec/frontend/super_sidebar/components/counter_spec.js11
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js23
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js17
-rw-r--r--spec/frontend/super_sidebar/mock_data.js10
-rw-r--r--spec/frontend/super_sidebar/user_counts_manager_spec.js166
-rw-r--r--spec/helpers/sidebars_helper_spec.rb30
-rw-r--r--spec/services/notes/create_service_spec.rb19
-rw-r--r--spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb43
-rw-r--r--spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb57
-rw-r--r--yarn.lock8
44 files changed, 789 insertions, 101 deletions
diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js
index b105273ece7..90dca0310f3 100644
--- a/app/assets/javascripts/commons/nav/user_merge_requests.js
+++ b/app/assets/javascripts/commons/nav/user_merge_requests.js
@@ -30,6 +30,12 @@ function updateMergeRequestCounts(newCount) {
* Refresh user counts (and broadcast if open)
*/
export function refreshUserMergeRequestCounts() {
+ if (gon?.use_new_navigation) {
+ // The new sidebar manages _all_ the counts in
+ // ~/super_sidebar/user_counts_manager.js
+ document.dispatchEvent(new CustomEvent('userCounts:fetch'));
+ return Promise.resolve();
+ }
return getUserCounts()
.then(({ data }) => {
const assignedMergeRequests = data.assigned_merge_requests;
@@ -67,6 +73,12 @@ export function closeUserCountsBroadcast() {
* no special functionality lost except cross tab notifications
*/
export function openUserCountsBroadcast() {
+ if (gon?.use_new_navigation) {
+ // The new sidebar broadcasts _all counts_ and updates
+ // them accordingly. Therefore we do not need this manager
+ // ~/super_sidebar/user_counts_manager.js
+ return;
+ }
closeUserCountsBroadcast();
if (window.BroadcastChannel) {
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index 9f0e1d4ee6f..a6f19ff95f3 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -1,5 +1,6 @@
<script>
import { GlIcon } from '@gitlab/ui';
+import { highCountTrim } from '~/lib/utils/text_utility';
export default {
components: {
@@ -31,6 +32,12 @@ export default {
component() {
return this.href ? 'a' : 'button';
},
+ formattedCount() {
+ if (Number.isFinite(this.count)) {
+ return highCountTrim(this.count);
+ }
+ return this.count;
+ },
},
};
</script>
@@ -43,6 +50,6 @@ export default {
class="counter gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-font-sm gl-hover-text-gray-900 gl-hover-text-decoration-none gl-focus--focus"
>
<gl-icon aria-hidden="true" :name="icon" />
- <span v-if="count" aria-hidden="true" class="gl-ml-1">{{ count }}</span>
+ <span v-if="count" aria-hidden="true" class="gl-ml-1">{{ formattedCount }}</span>
</component>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
index d37e863bed9..260c3906b93 100644
--- a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
@@ -1,5 +1,6 @@
<script>
import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
export default {
components: {
@@ -12,6 +13,11 @@ export default {
required: true,
},
},
+ methods: {
+ getCount(item) {
+ return userCounts[item.userCount] ?? item.count ?? 0;
+ },
+ },
};
</script>
@@ -28,7 +34,7 @@ export default {
<template #list-item="{ item }">
<span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
{{ item.text }}
- <gl-badge pill size="sm" variant="neutral">{{ item.count || 0 }}</gl-badge>
+ <gl-badge pill size="sm" variant="neutral">{{ getCount(item) }}</gl-badge>
</span>
</template>
</gl-disclosure-dropdown>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index a71cc2fd0f7..ab6a5a9c110 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -2,7 +2,11 @@
import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { highCountTrim } from '~/lib/utils/text_utility';
+import {
+ destroyUserCountsManager,
+ createUserCountsManager,
+ userCounts,
+} from '~/super_sidebar/user_counts_manager';
import logo from '../../../../views/shared/_logo.svg';
import { JS_TOGGLE_COLLAPSE_CLASS } from '../constants';
import CreateMenu from './create_menu.vue';
@@ -66,24 +70,29 @@ export default {
data() {
return {
mrMenuShown: false,
- todoCount: this.sidebarData.todos_pending_count,
searchTooltip: this.$options.i18n.searchKbdHelp,
+ userCounts,
};
},
computed: {
- formattedTodoCount() {
- return highCountTrim(this.todoCount);
+ mergeRequestTotalCount() {
+ return userCounts.assigned_merge_requests + userCounts.review_requested_merge_requests;
},
},
+ created() {
+ Object.assign(userCounts, this.sidebarData.user_counts);
+ createUserCountsManager();
+ },
mounted() {
document.addEventListener('todo:toggle', this.updateTodos);
},
beforeDestroy() {
document.removeEventListener('todo:toggle', this.updateTodos);
+ destroyUserCountsManager();
},
methods: {
updateTodos(e) {
- this.todoCount = e.detail.count || 0;
+ userCounts.todos = e.detail.count || 0;
},
hideSearchTooltip() {
this.searchTooltip = '';
@@ -166,7 +175,7 @@ export default {
v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
- :count="sidebarData.assigned_open_issues_count"
+ :count="userCounts.assigned_issues"
:href="sidebarData.issues_dashboard_path"
:label="$options.i18n.issues"
data-track-action="click_link"
@@ -183,7 +192,7 @@ export default {
v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
- :count="sidebarData.total_merge_requests_count"
+ :count="mergeRequestTotalCount"
:label="$options.i18n.mergeRequests"
data-track-action="click_dropdown"
data-track-label="merge_requests_menu"
@@ -194,7 +203,7 @@ export default {
v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
- :count="formattedTodoCount"
+ :count="userCounts.todos"
href="/dashboard/todos"
:label="$options.i18n.todoList"
data-qa-selector="todos_shortcut_button"
diff --git a/app/assets/javascripts/super_sidebar/user_counts_manager.js b/app/assets/javascripts/super_sidebar/user_counts_manager.js
new file mode 100644
index 00000000000..40c9fc43252
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/user_counts_manager.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import { getUserCounts } from '~/api/user_api';
+
+export const userCounts = Vue.observable({
+ last_update: 0,
+ // The following fields are part of
+ // https://docs.gitlab.com/ee/api/users.html#user-counts
+ todos: 0,
+ assigned_issues: 0,
+ assigned_merge_requests: 0,
+ review_requested_merge_requests: 0,
+});
+
+function updateCounts(payload = {}) {
+ if ((payload.last_update ?? 0) < userCounts.last_update) {
+ return;
+ }
+ for (const key in userCounts) {
+ if (Number.isInteger(payload[key])) {
+ userCounts[key] = payload[key];
+ }
+ }
+}
+
+let broadcastChannel = null;
+
+function broadcastUserCounts(data) {
+ broadcastChannel?.postMessage(data);
+}
+
+async function retrieveUserCountsFromApi() {
+ try {
+ const lastUpdate = Date.now();
+ const { data } = await getUserCounts();
+ const payload = { ...data, last_update: lastUpdate };
+ updateCounts(payload);
+ broadcastUserCounts(userCounts);
+ } catch (e) {
+ // eslint-disable-next-line no-console, @gitlab/require-i18n-strings
+ console.error('Error retrieving user counts', e);
+ }
+}
+
+export function destroyUserCountsManager() {
+ document.removeEventListener('userCounts:fetch', retrieveUserCountsFromApi);
+ broadcastChannel?.close();
+ broadcastChannel = null;
+}
+
+/**
+ * The createUserCountsManager does three things:
+ * 1. Set the initial state of userCounts
+ * 2. Create a broadcast channel to communicate user count updates across tabs
+ * 3. Add event listeners for other parts in the app which:
+ * - Update todos
+ * - Trigger a refetch of all counts
+ */
+export function createUserCountsManager() {
+ destroyUserCountsManager();
+ document.addEventListener('userCounts:fetch', retrieveUserCountsFromApi);
+
+ if (window.BroadcastChannel && gon?.current_user_id) {
+ broadcastChannel = new BroadcastChannel(`user_counts_${gon?.current_user_id}`);
+ broadcastChannel.onmessage = (ev) => {
+ updateCounts(ev.data);
+ };
+ broadcastUserCounts(userCounts);
+ }
+}
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 088405d458a..2ac4e27a051 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -4,8 +4,6 @@ module SidebarsHelper
include MergeRequestsHelper
include Nav::NewDropdownHelper
- USER_BAR_COUNT_LIMIT = 99
-
def sidebar_tracking_attributes_by_object(object)
sidebar_attributes_for_object(object).fetch(:tracking_attrs, {})
end
@@ -58,12 +56,16 @@ module SidebarsHelper
profile_path: profile_path,
profile_preferences_path: profile_preferences_path
},
+ user_counts: {
+ assigned_issues: user.assigned_open_issues_count,
+ assigned_merge_requests: user.assigned_open_merge_requests_count,
+ review_requested_merge_requests: user.review_requested_open_merge_requests_count,
+ todos: user.todos_pending_count,
+ last_update: time_in_milliseconds
+ },
can_sign_out: current_user_menu?(:sign_out),
sign_out_link: destroy_user_session_path,
- assigned_open_issues_count: format_user_bar_count(user.assigned_open_issues_count),
- todos_pending_count: user.todos_pending_count,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
- total_merge_requests_count: format_user_bar_count(user_merge_requests_counts[:total]),
create_new_menu_groups: create_new_menu_groups(group: group, project: project),
merge_request_menu: create_merge_request_menu(user),
projects_path: dashboard_projects_path,
@@ -176,7 +178,8 @@ module SidebarsHelper
{
text: _('Assigned'),
href: merge_requests_dashboard_path(assignee_username: user.username),
- count: user_merge_requests_counts[:assigned],
+ count: user.assigned_open_merge_requests_count,
+ userCount: 'assigned_merge_requests',
extraAttrs: {
'data-track-action': 'click_link',
'data-track-label': 'merge_requests_assigned',
@@ -187,7 +190,8 @@ module SidebarsHelper
{
text: _('Review requests'),
href: merge_requests_dashboard_path(reviewer_username: user.username),
- count: user_merge_requests_counts[:review_requested],
+ count: user.review_requested_open_merge_requests_count,
+ userCount: 'review_requested_merge_requests',
extraAttrs: {
'data-track-action': 'click_link',
'data-track-label': 'merge_requests_to_review',
@@ -322,17 +326,6 @@ module SidebarsHelper
links
end
- # Formats the counts to be shown in the super sidebar's top section (issues, MRs and todos).
- # We want to avoid printing huge numbers there, so when the count exceeds USER_BAR_COUNT_LIMIT,
- # we cap it to USER_BAR_COUNT_LIMIT and append a "+" to it.
- def format_user_bar_count(count)
- if count > USER_BAR_COUNT_LIMIT
- "#{USER_BAR_COUNT_LIMIT}+"
- else
- count.to_s
- end
- end
-
def impersonating?
!!session[:impersonator_id]
end
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index fab7a227e7d..9e1e381c568 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -59,7 +59,8 @@ module DraftNotes
note_params = draft.publish_params.merge(skip_keep_around_commits: skip_keep_around_commits)
note = Notes::CreateService.new(draft.project, draft.author, note_params).execute(
skip_capture_diff_note_position: skip_capture_diff_note_position,
- skip_merge_status_trigger: skip_merge_status_trigger
+ skip_merge_status_trigger: skip_merge_status_trigger,
+ skip_set_reviewed: true
)
set_discussion_resolve_status(note, draft)
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 7ebeb45c925..7dd6cd9a87c 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -4,7 +4,7 @@ module Notes
class CreateService < ::Notes::BaseService
include IncidentManagement::UsageData
- def execute(skip_capture_diff_note_position: false, skip_merge_status_trigger: false)
+ def execute(skip_capture_diff_note_position: false, skip_merge_status_trigger: false, skip_set_reviewed: false)
note = Notes::BuildService.new(project, current_user, params.except(:merge_request_diff_head_sha)).execute
# n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440
@@ -38,7 +38,8 @@ module Notes
when_saved(
note,
skip_capture_diff_note_position: skip_capture_diff_note_position,
- skip_merge_status_trigger: skip_merge_status_trigger
+ skip_merge_status_trigger: skip_merge_status_trigger,
+ skip_set_reviewed: skip_set_reviewed
)
end
end
@@ -79,7 +80,9 @@ module Notes
end
end
- def when_saved(note, skip_capture_diff_note_position: false, skip_merge_status_trigger: false)
+ def when_saved(
+ note, skip_capture_diff_note_position: false, skip_merge_status_trigger: false,
+ skip_set_reviewed: false)
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
@@ -87,6 +90,8 @@ module Notes
track_event(note, current_user)
if note.for_merge_request? && note.start_of_discussion?
+ set_reviewed(note) unless skip_set_reviewed
+
if !skip_capture_diff_note_position && note.diff_note?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
@@ -210,6 +215,11 @@ module Notes
def track_note_creation_visual_review(note)
Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note))
end
+
+ def set_reviewed(note)
+ ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
+ .execute(note.noteable)
+ end
end
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 78ab6b5909b..77b6bf573df 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2937,6 +2937,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: merge_requests_set_reviewer_reviewed
+ :worker_name: MergeRequests::SetReviewerReviewedWorker
+ :feature_category: :code_review_workflow
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: merge_requests_update_head_pipeline
:worker_name: MergeRequests::UpdateHeadPipelineWorker
:feature_category: :code_review_workflow
diff --git a/app/workers/merge_requests/set_reviewer_reviewed_worker.rb b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
new file mode 100644
index 00000000000..2f15bf3b879
--- /dev/null
+++ b/app/workers/merge_requests/set_reviewer_reviewed_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class SetReviewerReviewedWorker
+ include Gitlab::EventStore::Subscriber
+
+ data_consistency :always
+ feature_category :code_review_workflow
+ urgency :low
+ idempotent!
+
+ def handle_event(event)
+ current_user_id = event.data[:current_user_id]
+ merge_request_id = event.data[:merge_request_id]
+ current_user = User.find_by_id(current_user_id)
+ merge_request = MergeRequest.find_by_id(merge_request_id)
+
+ if !current_user
+ logger.info(structured_payload(message: 'Current user not found.', current_user_id: current_user_id))
+ elsif !merge_request
+ logger.info(structured_payload(message: 'Merge request not found.', merge_request_id: merge_request_id))
+ else
+ project = merge_request.source_project
+
+ ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user)
+ .execute(merge_request)
+ end
+ end
+ end
+end
diff --git a/config/feature_flags/development/ai_git_command_ff.yml b/config/feature_flags/development/ai_git_command_ff.yml
new file mode 100644
index 00000000000..08da04eaf2b
--- /dev/null
+++ b/config/feature_flags/development/ai_git_command_ff.yml
@@ -0,0 +1,8 @@
+---
+name: ai_git_command_ff
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118120
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408675
+milestone: '16.0'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 92f468fde85..ea3260d8260 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -337,6 +337,8 @@
- 1
- - merge_requests_resolve_todos_after_approval
- 1
+- - merge_requests_set_reviewer_reviewed
+ - 1
- - merge_requests_stream_approval_audit_event
- 1
- - merge_requests_sync_code_owner_approval_rules
diff --git a/data/removals/16_0/16-0-redis-5.yml b/data/removals/16_0/16-0-redis-5.yml
new file mode 100644
index 00000000000..2c539c5775e
--- /dev/null
+++ b/data/removals/16_0/16-0-redis-5.yml
@@ -0,0 +1,14 @@
+- title: "Redis 5 compatibility"
+ announcement_milestone: "15.3"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: twk3
+ stage: Enablement
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331468
+ body: |
+ In GitLab 13.9, we updated the Omnibus GitLab package and GitLab Helm chart 4.9 to Redis 6. Redis 5 reached end of life in April 2022 and is not supported.
+
+ GitLab 16.0, we have removed support for Redis 5. If you are using your own Redis 5.0 instance, you must upgrade it to Redis 6.0 or later before upgrading to GitLab 16.0
+ or later.
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/install/requirements.html
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
index 2902fa3c5da..663d4b206f7 100644
--- a/doc/administration/reference_architectures/10k_users.md
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -844,9 +844,9 @@ to be used with GitLab. The following IPs will be used as an example:
Managed Redis from cloud providers (such as AWS ElastiCache) will work. If these
services support high availability, be sure it _isn't_ of the Redis Cluster type.
-Redis version 5.0 or higher is required, which is included with Omnibus GitLab
-packages starting with GitLab 13.0. Older Redis versions don't support an
-optional count argument to SPOP, which is required for [Merge Trains](../../ci/pipelines/merge_trains.md).
+
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
+
Note the Redis node's IP address or hostname, port, and password (if required).
These will be necessary later when configuring the [GitLab application servers](#configure-gitlab-rails).
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 86ef5149b4d..2ff47eaa143 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -861,9 +861,9 @@ to be used with GitLab. The following IPs will be used as an example:
Managed Redis from cloud providers (such as AWS ElastiCache) will work. If these
services support high availability, be sure it _isn't_ of the Redis Cluster type.
-Redis version 5.0 or higher is required, which is included with Omnibus GitLab
-packages starting with GitLab 13.0. Older Redis versions don't support an
-optional count argument to SPOP, which is required for [Merge Trains](../../ci/pipelines/merge_trains.md).
+
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
+
Note the Redis node's IP address or hostname, port, and password (if required).
These will be necessary later when configuring the [GitLab application servers](#configure-gitlab-rails).
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index e94e7a162cb..7af577bd75a 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -341,13 +341,7 @@ to be used with GitLab.
### Provide your own Redis instance
-Redis version 5.0 or higher is required, as this is what ships with
-Omnibus GitLab packages starting with GitLab 13.0. Older Redis versions
-do not support an optional count argument to SPOP which is now required for
-[Merge Trains](../../ci/pipelines/merge_trains.md).
-
-In addition, GitLab makes use of certain commands like `UNLINK` and `USAGE` which
-were introduced only in Redis 4.
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
Managed Redis from cloud providers such as AWS ElastiCache will work. If these
services support high availability, be sure it is not the Redis Cluster type.
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index e26d954a11b..b5c5717e56d 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -455,10 +455,7 @@ to be used with GitLab. The following IPs will be used as an example:
Managed Redis from cloud providers such as AWS ElastiCache will work. If these
services support high availability, be sure it is **not** the Redis Cluster type.
-Redis version 5.0 or higher is required, as this is what ships with
-Omnibus GitLab packages starting with GitLab 13.0. Older Redis versions
-do not support an optional count argument to SPOP which is now required for
-[Merge Trains](../../ci/pipelines/merge_trains.md).
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
Note the Redis node's IP address or hostname, port, and password (if required).
These will be necessary when configuring the
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
index 8b76d254e5c..90aab6873b6 100644
--- a/doc/administration/reference_architectures/50k_users.md
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -854,9 +854,9 @@ to be used with GitLab. The following IPs will be used as an example:
Managed Redis from cloud providers (such as AWS ElastiCache) will work. If these
services support high availability, be sure it _isn't_ of the Redis Cluster type.
-Redis version 5.0 or higher is required, which is included with Omnibus GitLab
-packages starting with GitLab 13.0. Older Redis versions don't support an
-optional count argument to SPOP, which is required for [Merge Trains](../../ci/pipelines/merge_trains.md).
+
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
+
Note the Redis node's IP address or hostname, port, and password (if required).
These will be necessary later when configuring the [GitLab application servers](#configure-gitlab-rails).
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
index d205993d5ab..292cf537ba6 100644
--- a/doc/administration/reference_architectures/5k_users.md
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -452,10 +452,7 @@ to be used with GitLab. The following IPs are used as an example:
Managed Redis from cloud providers such as AWS ElastiCache works. If these
services support high availability, be sure it is **not** the Redis Cluster type.
-Redis version 5.0 or higher is required, as this is what ships with
-Omnibus GitLab packages starting with GitLab 13.0. Older Redis versions
-do not support an optional count argument to SPOP which is now required for
-[Merge Trains](../../ci/pipelines/merge_trains.md).
+Because Omnibus GitLab packages ship with Redis 6.0 or later, Redis 6.0 or higher is required. Older Redis versions have reached end-of-life.
Note the Redis node's IP address or hostname, port, and password (if required).
These are necessary when configuring the
diff --git a/doc/development/ai_architecture.md b/doc/development/ai_architecture.md
new file mode 100644
index 00000000000..e9994c8a6f4
--- /dev/null
+++ b/doc/development/ai_architecture.md
@@ -0,0 +1,108 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# AI Architecture (Experiment)
+
+GitLab has created a common set of tools to support our product groups and their utilization of AI. Our goals with this common architecture are:
+
+1. Increase the velocity of feature teams by providing a set of high quality, ready to use tools
+1. Ability to switch underlying technologies quickly and easily
+
+AI is moving very quickly, and we need to be able to keep pace with changes in the area. We have built an [abstraction layer](../../ee/development/ai_features.md) to do this, allowing us to take a more "pluggable" approach to the underlying models, data stores, and other technologies.
+
+The following diagram shows a simplified view of how the different components in GitLab interact. The abstraction layer helps avoid code duplication within the REST APIs within the `AI API` block.
+
+```plantuml
+@startuml
+skin rose
+
+package "Code Suggestions" {
+ node "Model Gateway"
+ node "Triton Inference Server" as Triton
+}
+
+package "Code Suggestions Models" as CSM {
+ node "codegen"
+ node "PaLM"
+}
+
+package "Suggested Reviewers" {
+ node "Model Gateway (SR)"
+ node "Extractor"
+ node "Serving Model"
+}
+
+package "AI API" as AIF {
+ node "OpenAI"
+ node "Vertex AI"
+}
+
+package GitLab {
+ node "Web IDE"
+
+ package "Web" {
+ node "REST API"
+ node "GraphQL"
+ }
+
+ package "Jobs" {
+ node "Sidekiq"
+ }
+}
+
+package Databases {
+ node "Vector Database"
+ node "PostgreSQL"
+}
+
+node "VSCode"
+
+"Model Gateway" --> Triton
+Triton --> CSM
+GitLab --> Databases
+VSCode --> "Model Gateway"
+"Web IDE" --> "Model Gateway"
+"Web IDE" --> "GraphQL"
+"Web IDE" --> "REST API"
+"Model Gateway" -[#blue]--> "REST API": user authorized?
+
+"Sidekiq" --> AIF
+Web --> AIF
+
+"Model Gateway (SR)" --> "REST API"
+"Model Gateway (SR)" --> "Serving Model"
+"Extractor" --> "GraphQL"
+"Sidekiq" --> "Model Gateway (SR)"
+
+@enduml
+```
+
+## SaaS-based AI abstraction layer
+
+GitLab currently operates a cloud-hosted AI architecture. We are exploring how self-managed instances integrate with it.
+
+There are two primary reasons for this: the best AI models are cloud-based as they often depend on specialized hardware designed for this purpose, and operating self-managed infrastructure capable of AI at-scale and with appropriate performance is a significant undertaking. We are actively [tracking self-managed customers interested in AI](https://gitlab.com/gitlab-org/gitlab/-/issues/409183).
+
+## Supported technologies
+
+As part of the AI working group, we have been investigating various technologies and vetting them. Below is a list of the tools which have been reviewed and already approved for use within the GitLab application.
+
+It is possible to utilize other models or technologies, however they will need to go through a review process prior to use. Use the [AI Project Proposal template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=AI%20Project%20Proposal) as part of your idea and include the new tools required to support it.
+
+### Models
+
+The following models have been approved for use:
+
+- [OpenAI models](https://platform.openai.com/docs/models)
+- Google's [Vertex AI](https://cloud.google.com/vertex-ai) and [model garden](https://cloud.google.com/model-garden)
+- [AI Code Suggestions](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/tree/main)
+- [Suggested reviewer](https://gitlab.com/gitlab-org/modelops/applied-ml/applied-ml-updates/-/issues/10)
+
+### Vector stores
+
+The following vector stores have been approved for use:
+
+- [`pgvector`](https://github.com/pgvector/pgvector) is a Postgres extension adding support for storing vector embeddings and calculating ANN (approximate nearest neighbor).
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index a8193fc819d..0573437b64d 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -1145,3 +1145,7 @@ they don't always work in RHEL.
The [GitLab.com architecture](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/)
is detailed for your reference, but this architecture is only useful if you have
millions of users.
+
+### AI architecture
+
+A [SaaS model gateway](ai_architecture.md) is available to enable AI-powered features.
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 8c6f469aca2..8d779ec978d 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -12,10 +12,7 @@ This page includes information about the minimum requirements you need to instal
### Redis versions
-GitLab 13.0 and later requires Redis version 5.0 or higher.
-
-Redis version 6.0 or higher is recommended, as this is what ships with
-[Omnibus GitLab](https://docs.gitlab.com/omnibus/) packages starting with GitLab 13.9.
+GitLab 16.0 and later requires Redis 6.0 or later.
## Hardware requirements
diff --git a/doc/tutorials/left_sidebar/img/explore_v16_0.png b/doc/tutorials/left_sidebar/img/explore_v16_0.png
new file mode 100644
index 00000000000..3cbe94e5de4
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/explore_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/pin_v16_0.png b/doc/tutorials/left_sidebar/img/pin_v16_0.png
new file mode 100644
index 00000000000..17dbcac4caf
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/pin_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/pinned_v16_0.png b/doc/tutorials/left_sidebar/img/pinned_v16_0.png
new file mode 100644
index 00000000000..da6eba82f86
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/pinned_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/project_selected_v16_0.png b/doc/tutorials/left_sidebar/img/project_selected_v16_0.png
new file mode 100644
index 00000000000..65f89968780
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/project_selected_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/search_projects_v16_0.png b/doc/tutorials/left_sidebar/img/search_projects_v16_0.png
new file mode 100644
index 00000000000..9b7b394c416
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/search_projects_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/shortcuts_v16_0.png b/doc/tutorials/left_sidebar/img/shortcuts_v16_0.png
new file mode 100644
index 00000000000..07094898117
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/shortcuts_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/img/your_work_v16_0.png b/doc/tutorials/left_sidebar/img/your_work_v16_0.png
new file mode 100644
index 00000000000..f7b5ed4217d
--- /dev/null
+++ b/doc/tutorials/left_sidebar/img/your_work_v16_0.png
Binary files differ
diff --git a/doc/tutorials/left_sidebar/index.md b/doc/tutorials/left_sidebar/index.md
new file mode 100644
index 00000000000..bbd63fbd4f7
--- /dev/null
+++ b/doc/tutorials/left_sidebar/index.md
@@ -0,0 +1,72 @@
+---
+stage: none
+group: Tutorials
+info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
+---
+
+# Tutorial: Use the left sidebar to navigate GitLab
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9044) in GitLab 16.0.
+
+Follow this tutorial to learn how to use the new left sidebar to navigate the UI.
+
+Provide feedback in
+[issue 409005](https://gitlab.com/gitlab-org/gitlab/-/issues/409005).
+
+## Enable the new left sidebar
+
+To view the new sidebar:
+
+1. On the top bar, in the upper-right corner, select your avatar.
+1. Turn on the **New navigation** toggle.
+
+To turn off this sidebar, return to your avatar and turn off the toggle.
+
+## Find your project and customize the sidebar
+
+Let's get started exploring the GitLab UI and left sidebar.
+
+1. Start by finding the project you want to work on.
+ To explore all available projects, on the left sidebar, select **Explore**:
+
+ ![Explore](img/explore_v16_0.png)
+
+1. On the right, above the list of projects, type search criteria.
+
+ ![Search projects](img/search_projects_v16_0.png)
+
+1. When you find the project you want, select the project name.
+ The left sidebar now shows project-specific options.
+
+ ![Project-specific options](img/project_selected_v16_0.png)
+
+Now, on the left sidebar:
+
+- Your issues, merge requests, and to-do items are listed in the shortcuts
+ at the top:
+
+ ![shortcuts](img/shortcuts_v16_0.png)
+
+- You can pin other items if you tend to use them frequently.
+ To do so:
+
+ 1. Expand the sections until you are viewing the item you want to pin.
+ 1. Hover over and select the pin (**{thumbtack}**).
+
+ ![pin](img/pin_v16_0.png)
+
+ The item is displayed in the **Pinned** section:
+
+ ![pinned item](img/pinned_v16_0.png)
+
+- If you need to find a merge request you created,
+ an issue you opened, or anything else, next to your avatar,
+ select the magnifying glass (**{search}**).
+
+## View everything assigned to you
+
+On the left sidebar, you can also choose a more focused view into the areas you have access to.
+
+- Change the view to **Your work**.
+
+ ![Your work](img/your_work_v16_0.png)
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 83a17c2e116..68ea25e9540 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -82,6 +82,17 @@ is removed in favor of more specialized fields like:
- `infrastructure_access_level`
- `monitor_access_level`
+### Redis 5 compatibility
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+In GitLab 13.9, we updated the Omnibus GitLab package and GitLab Helm chart 4.9 to Redis 6. Redis 5 reached end of life in April 2022 and is not supported.
+
+GitLab 16.0, we have removed support for Redis 5. If you are using your own Redis 5.0 instance, you must upgrade it to Redis 6.0 or later before upgrading to GitLab 16.0
+or later.
+
### Vulnerability confidence field
WARNING:
diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb
index c017396c8e8..ce71ee594f2 100644
--- a/lib/gitlab/event_store.rb
+++ b/lib/gitlab/event_store.rb
@@ -60,6 +60,7 @@ module Gitlab
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent
store.subscribe ::MergeRequests::ResolveTodosAfterApprovalWorker, to: ::MergeRequests::ApprovedEvent
store.subscribe ::MergeRequests::ExecuteApprovalHooksWorker, to: ::MergeRequests::ApprovedEvent
+ store.subscribe ::MergeRequests::SetReviewerReviewedWorker, to: ::MergeRequests::ApprovedEvent
store.subscribe ::Ml::ExperimentTracking::AssociateMlCandidateToPackageWorker,
to: ::Packages::PackageCreatedEvent,
if: -> (event) { ::Ml::ExperimentTracking::AssociateMlCandidateToPackageWorker.handles_event?(event) }
diff --git a/package.json b/package.json
index 9ffe1afd370..41a9d910fc3 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.43.0",
- "@gitlab/ui": "62.5.2",
+ "@gitlab/ui": "62.6.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230425040132",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js
index f660cc8e9de..114cbbf812c 100644
--- a/spec/frontend/commons/nav/user_merge_requests_spec.js
+++ b/spec/frontend/commons/nav/user_merge_requests_spec.js
@@ -16,7 +16,10 @@ describe('User Merge Requests', () => {
let newBroadcastChannelMock;
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent').mockReturnValue(false);
+
global.gon.current_user_id = 123;
+ global.gon.use_new_navigation = false;
channelMock = {
postMessage: jest.fn(),
@@ -73,6 +76,10 @@ describe('User Merge Requests', () => {
expect(channelMock.postMessage).not.toHaveBeenCalled();
});
});
+
+ it('does not emit event to refetch counts', () => {
+ expect(document.dispatchEvent).not.toHaveBeenCalled();
+ });
});
describe('openUserCountsBroadcast', () => {
@@ -85,6 +92,7 @@ describe('User Merge Requests', () => {
channelMock.onmessage({ data: TEST_COUNT });
+ expect(newBroadcastChannelMock).toHaveBeenCalled();
expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString());
});
@@ -93,6 +101,7 @@ describe('User Merge Requests', () => {
openUserCountsBroadcast();
+ expect(newBroadcastChannelMock).toHaveBeenCalled();
expect(channelMock.close).toHaveBeenCalled();
});
});
@@ -118,4 +127,28 @@ describe('User Merge Requests', () => {
});
});
});
+
+ describe('if new navigation is enabled', () => {
+ beforeEach(() => {
+ global.gon.use_new_navigation = true;
+ jest.spyOn(UserApi, 'getUserCounts');
+ });
+
+ it('openUserCountsBroadcast is a noop', () => {
+ openUserCountsBroadcast();
+ expect(newBroadcastChannelMock).not.toHaveBeenCalled();
+ });
+
+ describe('refreshUserMergeRequestCounts', () => {
+ it('does not call api', async () => {
+ await refreshUserMergeRequestCounts();
+ expect(UserApi.getUserCounts).not.toHaveBeenCalled();
+ });
+
+ it('emits event to refetch counts', async () => {
+ await refreshUserMergeRequestCounts();
+ expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('todo:toggle'));
+ });
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/counter_spec.js b/spec/frontend/super_sidebar/components/counter_spec.js
index 8f514540413..77f77eae1c2 100644
--- a/spec/frontend/super_sidebar/components/counter_spec.js
+++ b/spec/frontend/super_sidebar/components/counter_spec.js
@@ -49,4 +49,15 @@ describe('Counter component', () => {
expect(findButton().exists()).toBe(false);
});
});
+
+ it.each([
+ ['99+', '99+'],
+ ['110%', '110%'],
+ [100, '99+'],
+ [10, '10'],
+ [0, ''],
+ ])('formats count %p as %p', (count, result) => {
+ createWrapper({ count });
+ expect(findButton().text()).toBe(result);
+ });
});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
index 9c8fd0556f1..53d47397eb3 100644
--- a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -1,6 +1,7 @@
import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
import { mergeRequestMenuGroup } from '../mock_data';
describe('MergeRequestMenu component', () => {
@@ -10,17 +11,17 @@ describe('MergeRequestMenu component', () => {
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findLink = (name) => wrapper.findByRole('link', { name });
- const createWrapper = () => {
+ const createWrapper = (items) => {
wrapper = mountExtended(MergeRequestMenu, {
propsData: {
- items: mergeRequestMenuGroup,
+ items,
},
});
};
describe('default', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper(mergeRequestMenuGroup);
});
it('passes the items to the disclosure dropdown', () => {
@@ -49,5 +50,21 @@ describe('MergeRequestMenu component', () => {
it('renders 0 string when count is empty', () => {
expect(findGlBadge(1).text()).toBe(String(0));
});
+
+ it('renders value from userCounts if `userCount` prop is defined', () => {
+ userCounts.assigned_merge_requests = 5;
+ mergeRequestMenuGroup[0].items[0].userCount = 'assigned_merge_requests';
+ createWrapper(mergeRequestMenuGroup);
+
+ expect(findGlBadge(0).text()).toBe(String(userCounts.assigned_merge_requests));
+ });
+
+ it('renders item count if unknown `userCount` prop is defined', () => {
+ const { count } = mergeRequestMenuGroup[0].items[0];
+ mergeRequestMenuGroup[0].items[0].userCount = 'foobar';
+ createWrapper(mergeRequestMenuGroup);
+
+ expect(findGlBadge(0).text()).toBe(String(count));
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index 7abd64ca108..25f699bddb5 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -10,14 +10,10 @@ import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
-import { highCountTrim } from '~/lib/utils/text_utility';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
import { sidebarData } from '../mock_data';
import { MOCK_DEFAULT_SEARCH_OPTIONS } from './global_search/mock_data';
-jest.mock('~/lib/utils/text_utility', () => ({
- highCountTrim: jest.fn().mockReturnValue('99+'),
-}));
-
describe('UserBar component', () => {
let wrapper;
@@ -78,7 +74,7 @@ describe('UserBar component', () => {
it('renders issues counter', () => {
const isuesCounter = findIssuesCounter();
- expect(isuesCounter.props('count')).toBe(sidebarData.assigned_open_issues_count);
+ expect(isuesCounter.props('count')).toBe(userCounts.assigned_issues);
expect(isuesCounter.props('href')).toBe(sidebarData.issues_dashboard_path);
expect(isuesCounter.props('label')).toBe(__('Issues'));
expect(isuesCounter.attributes('data-track-action')).toBe('click_link');
@@ -89,7 +85,9 @@ describe('UserBar component', () => {
it('renders merge requests counter', () => {
const mrsCounter = findMRsCounter();
- expect(mrsCounter.props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(mrsCounter.props('count')).toBe(
+ userCounts.assigned_merge_requests + userCounts.review_requested_merge_requests,
+ );
expect(mrsCounter.props('label')).toBe(__('Merge requests'));
expect(mrsCounter.attributes('data-track-action')).toBe('click_dropdown');
expect(mrsCounter.attributes('data-track-label')).toBe('merge_requests_menu');
@@ -107,13 +105,12 @@ describe('UserBar component', () => {
expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
});
- it('should format and update todo counter when event is emitted', async () => {
+ it('should update todo counter when event is emitted', async () => {
createWrapper();
const count = 100;
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
await nextTick();
- expect(highCountTrim).toHaveBeenCalledWith(count);
- expect(findTodosCounter().props('count')).toBe('99+');
+ expect(findTodosCounter().props('count')).toBe(count);
});
});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index ecb662be481..d42bc24098d 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -81,10 +81,14 @@ export const sidebarData = {
username: 'root',
avatar_url: 'path/to/img_administrator',
logo_url: 'path/to/logo',
- assigned_open_issues_count: 1,
- todos_pending_count: 3,
+ user_counts: {
+ last_update: Date.now(),
+ todos: 3,
+ assigned_issues: 1,
+ assigned_merge_requests: 3,
+ review_requested_merge_requests: 1,
+ },
issues_dashboard_path: 'path/to/issues',
- total_merge_requests_count: 4,
create_new_menu_groups: createNewMenuGroups,
merge_request_menu: mergeRequestMenuGroup,
projects_path: 'path/to/projects',
diff --git a/spec/frontend/super_sidebar/user_counts_manager_spec.js b/spec/frontend/super_sidebar/user_counts_manager_spec.js
new file mode 100644
index 00000000000..b5074620195
--- /dev/null
+++ b/spec/frontend/super_sidebar/user_counts_manager_spec.js
@@ -0,0 +1,166 @@
+import waitForPromises from 'helpers/wait_for_promises';
+
+import * as UserApi from '~/api/user_api';
+import {
+ createUserCountsManager,
+ userCounts,
+ destroyUserCountsManager,
+} from '~/super_sidebar/user_counts_manager';
+
+jest.mock('~/api');
+
+const USER_ID = 123;
+const userCountDefaults = {
+ todos: 1,
+ assigned_issues: 2,
+ assigned_merge_requests: 3,
+ review_requested_merge_requests: 4,
+};
+
+const userCountUpdate = {
+ todos: 123,
+ assigned_issues: 456,
+ assigned_merge_requests: 789,
+ review_requested_merge_requests: 101112,
+};
+
+describe('User Merge Requests', () => {
+ let channelMock;
+ let newBroadcastChannelMock;
+
+ beforeEach(() => {
+ jest.spyOn(document, 'removeEventListener');
+ jest.spyOn(document, 'addEventListener');
+
+ global.gon.current_user_id = USER_ID;
+
+ channelMock = {
+ postMessage: jest.fn(),
+ close: jest.fn(),
+ };
+ newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock);
+
+ Object.assign(userCounts, userCountDefaults, { last_update: 0 });
+
+ global.BroadcastChannel = newBroadcastChannelMock;
+ });
+
+ describe('createUserCountsManager', () => {
+ beforeEach(() => {
+ createUserCountsManager();
+ });
+
+ it('creates BroadcastChannel which updates counts on message received', () => {
+ expect(newBroadcastChannelMock).toHaveBeenCalledWith(`user_counts_${USER_ID}`);
+ });
+
+ it('closes BroadCastchannel if called while already open', () => {
+ expect(channelMock.close).not.toHaveBeenCalled();
+
+ createUserCountsManager();
+
+ expect(channelMock.close).toHaveBeenCalled();
+ });
+
+ describe('BroadcastChannel onmessage handler', () => {
+ it('updates counts on message received', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ channelMock.onmessage({ data: { ...userCountUpdate, last_update: Date.now() } });
+
+ expect(userCounts).toMatchObject(userCountUpdate);
+ });
+
+ it('ignores updates with older data', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+ userCounts.last_update = Date.now();
+
+ channelMock.onmessage({
+ data: { ...userCountUpdate, last_update: userCounts.last_update - 1000 },
+ });
+
+ expect(userCounts).toMatchObject(userCountDefaults);
+ });
+
+ it('ignores unknown fields', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ channelMock.onmessage({ data: { ...userCountUpdate, i_am_unknown: 5 } });
+
+ expect(userCounts).toMatchObject(userCountUpdate);
+ expect(userCounts.i_am_unknown).toBeUndefined();
+ });
+ });
+
+ it('broadcasts user counts during initialization', () => {
+ expect(channelMock.postMessage).toHaveBeenCalledWith(
+ expect.objectContaining(userCountDefaults),
+ );
+ });
+
+ it('setups event listener without leaking them', () => {
+ expect(document.removeEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ expect(document.addEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ });
+ });
+
+ describe('Event listener userCounts:fetch', () => {
+ beforeEach(() => {
+ jest.spyOn(UserApi, 'getUserCounts').mockResolvedValue({
+ data: { ...userCountUpdate, merge_requests: 'FOO' },
+ });
+ createUserCountsManager();
+ });
+
+ it('fetches counts from API, stores and rebroadcasts them', async () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ document.dispatchEvent(new CustomEvent('userCounts:fetch'));
+ await waitForPromises();
+
+ expect(UserApi.getUserCounts).toHaveBeenCalled();
+ expect(userCounts).toMatchObject(userCountUpdate);
+ expect(channelMock.postMessage).toHaveBeenLastCalledWith(userCounts);
+ });
+ });
+
+ describe('destroyUserCountsManager', () => {
+ it('unregisters event handler', () => {
+ expect(document.removeEventListener).not.toHaveBeenCalledWith();
+
+ destroyUserCountsManager();
+
+ expect(document.removeEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ });
+
+ describe('when BroadcastChannel is not opened', () => {
+ it('does nothing', () => {
+ destroyUserCountsManager();
+ expect(channelMock.close).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when BroadcastChannel is opened', () => {
+ beforeEach(() => {
+ createUserCountsManager();
+ });
+
+ it('closes BroadcastChannel', () => {
+ expect(channelMock.close).not.toHaveBeenCalled();
+
+ destroyUserCountsManager();
+
+ expect(channelMock.close).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6b0b4ed150d..8426fd41368 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -73,6 +73,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
end
before do
+ allow(Time).to receive(:now).and_return(Time.utc(2021, 1, 1))
allow(helper).to receive(:current_user) { user }
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:header_search_context).and_return({ some: "search data" })
@@ -82,7 +83,6 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
allow(user).to receive(:assigned_open_merge_requests_count).and_return(4)
allow(user).to receive(:review_requested_open_merge_requests_count).and_return(0)
allow(user).to receive(:todos_pending_count).and_return(3)
- allow(user).to receive(:total_merge_requests_count).and_return(4)
allow(user).to receive(:pinned_nav_items).and_return({ panel_type => %w[foo bar], 'another_panel' => %w[baz] })
end
@@ -109,12 +109,16 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
profile_path: profile_path,
profile_preferences_path: profile_preferences_path
},
+ user_counts: {
+ assigned_issues: 1,
+ assigned_merge_requests: 4,
+ review_requested_merge_requests: 0,
+ todos: 3,
+ last_update: 1609459200000
+ },
can_sign_out: helper.current_user_menu?(:sign_out),
sign_out_link: destroy_user_session_path,
- assigned_open_issues_count: "1",
- todos_pending_count: 3,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
- total_merge_requests_count: "4",
projects_path: dashboard_projects_path,
groups_path: dashboard_groups_path,
support_path: helper.support_url,
@@ -209,6 +213,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
text: _('Assigned'),
href: merge_requests_dashboard_path(assignee_username: user.username),
count: 4,
+ userCount: 'assigned_merge_requests',
extraAttrs: {
'data-track-action': 'click_link',
'data-track-label': 'merge_requests_assigned',
@@ -220,6 +225,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
text: _('Review requests'),
href: merge_requests_dashboard_path(reviewer_username: user.username),
count: 0,
+ userCount: 'review_requested_merge_requests',
extraAttrs: {
'data-track-action': 'click_link',
'data-track-label': 'merge_requests_to_review',
@@ -307,21 +313,6 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
)
end
- context 'when counts are high' do
- before do
- allow(user).to receive(:assigned_open_issues_count).and_return(1000)
- allow(user).to receive(:assigned_open_merge_requests_count).and_return(50)
- allow(user).to receive(:review_requested_open_merge_requests_count).and_return(50)
- end
-
- it 'caps counts to USER_BAR_COUNT_LIMIT and appends a "+" to them' do
- expect(subject).to include(
- assigned_open_issues_count: "99+",
- total_merge_requests_count: "99+"
- )
- end
- end
-
describe 'current context' do
context 'when current context is a project' do
let_it_be(:project) { build(:project) }
@@ -448,7 +439,6 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 4)
Rails.cache.write(['users', user.id, 'review_requested_open_merge_requests_count'], 0)
Rails.cache.write(['users', user.id, 'todos_pending_count'], 3)
- Rails.cache.write(['users', user.id, 'total_merge_requests_count'], 4)
end
it 'returns Project Panel for project nav' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 1188e7e2b1c..56d9c8bcb57 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -172,6 +172,25 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
create(:merge_request, source_project: project_with_repo, target_project: project_with_repo)
end
+ let(:new_opts) { opts.merge(noteable_type: 'MergeRequest', noteable_id: merge_request.id) }
+
+ it 'calls MergeRequests::MarkReviewerReviewedService service' do
+ expect_next_instance_of(
+ MergeRequests::MarkReviewerReviewedService,
+ project: project_with_repo, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ described_class.new(project_with_repo, user, new_opts).execute
+ end
+
+ it 'does not call MergeRequests::MarkReviewerReviewedService service when skip_set_reviewed is true' do
+ expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+
+ described_class.new(project_with_repo, user, new_opts).execute(skip_set_reviewed: true)
+ end
+
context 'noteable highlight cache clearing' do
let(:position) do
Gitlab::Diff::Position.new(
diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
new file mode 100644
index 00000000000..3faa8f9c032
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'delegates AI request to Workhorse' do
+ context 'when openai_experimentation is disabled' do
+ before do
+ stub_feature_flags(openai_experimentation: false)
+ end
+
+ it 'responds as not found' do
+ post api(url, current_user), params: input_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when ai_experimentation_api is disabled' do
+ before do
+ stub_feature_flags(ai_experimentation_api: false)
+ end
+
+ it 'responds as not found' do
+ post api(url, current_user), params: input_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it 'responds with Workhorse send-url headers' do
+ post api(url, current_user), params: input_params
+
+ expect(response.body).to eq('""')
+ expect(response).to have_gitlab_http_status(:ok)
+
+ send_url_prefix, encoded_data = response.headers['Gitlab-Workhorse-Send-Data'].split(':')
+ data = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_data))
+
+ expect(send_url_prefix).to eq('send-url')
+ expect(data).to eq({
+ 'AllowRedirects' => false,
+ 'Method' => 'POST'
+ }.merge(expected_params))
+ end
+end
diff --git a/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
new file mode 100644
index 00000000000..942cf8e87e9
--- /dev/null
+++ b/spec/workers/merge_requests/set_reviewer_reviewed_worker_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::SetReviewerReviewedWorker, feature_category: :source_code_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id } }
+ let(:approved_event) { MergeRequests::ApprovedEvent.new(data: data) }
+
+ it_behaves_like 'subscribes to event' do
+ let(:event) { approved_event }
+ end
+
+ it 'calls MergeRequests::MarkReviewerReviewedService' do
+ expect_next_instance_of(
+ MergeRequests::MarkReviewerReviewedService,
+ project: project, current_user: user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ consume_event(subscriber: described_class, event: approved_event)
+ end
+
+ shared_examples 'when object does not exist' do
+ it 'logs and does not call MergeRequests::MarkReviewerReviewedService' do
+ expect(Sidekiq.logger).to receive(:info).with(hash_including(log_payload))
+ expect(MergeRequests::MarkReviewerReviewedService).not_to receive(:new)
+
+ expect { consume_event(subscriber: described_class, event: approved_event) }
+ .not_to raise_exception
+ end
+ end
+
+ context 'when the user does not exist' do
+ before do
+ user.destroy!
+ end
+
+ it_behaves_like 'when object does not exist' do
+ let(:log_payload) { { 'message' => 'Current user not found.', 'current_user_id' => user.id } }
+ end
+ end
+
+ context 'when the merge request does not exist' do
+ before do
+ merge_request.destroy!
+ end
+
+ it_behaves_like 'when object does not exist' do
+ let(:log_payload) { { 'message' => 'Merge request not found.', 'merge_request_id' => merge_request.id } }
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 589f4fe050b..07c81067f04 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1115,10 +1115,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.43.0.tgz#7789e6e5e8cd7d97489d9cfb021e0f25ddcfa829"
integrity sha512-o5P8T42qXh38DU0Px7rnVCV86cDfrsKHNczdNQAIGeyw5Ci7orsL/0f1M4BVtOSgU0VOoHuB0Yb/HyQjjmwt6A==
-"@gitlab/ui@62.5.2":
- version "62.5.2"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.5.2.tgz#2a476e04d86ba4c964779cadeb7f3c4acb85ab9d"
- integrity sha512-pTOjFRuy9KV2U1sdTC8gY3Ue4stfzuhxK9XUgs2ZieOb56XwDdDK3aP6l5JYEXsYHLpgyx7OyJuS3qJbMKvXvA==
+"@gitlab/ui@62.6.0":
+ version "62.6.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.6.0.tgz#390cba14810654ae4a22d4efd3f661d4a5d8135c"
+ integrity sha512-+vXuEy/6hoI7pecyVuSkdIbObzjql+4ZGRh/e87FsMMUQtQfDVpOeELtnEC7/HrvD5jhO5yTndduJKOanJKJFg==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.23.1"