summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-14 00:17:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-14 00:17:46 +0000
commit270353e1ff556a43333f82f171c3a485958126f0 (patch)
treec7bb4ac335b1e101b9bf92905ec2e8e170c6696c
parentb2e3da6a38f143a8c782dae4baceae3ed764733d (diff)
downloadgitlab-ce-270353e1ff556a43333f82f171c3a485958126f0.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue2
-rw-r--r--app/assets/javascripts/lib/graphql.js9
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js11
-rw-r--r--app/assets/javascripts/repository/index.js3
-rw-r--r--app/assets/javascripts/search/index.js1
-rw-r--r--app/assets/javascripts/search/store/constants.js12
-rw-r--r--app/assets/javascripts/search/store/getters.js13
-rw-r--r--app/assets/javascripts/search/store/index.js4
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/search/store/utils.js4
-rw-r--r--app/assets/javascripts/sidebar/utils.js2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue23
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue4
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb2
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/sidebars_helper.rb4
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/concerns/has_user_type.rb4
-rw-r--r--app/models/integrations/harbor.rb5
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb11
-rw-r--r--app/policies/base_policy.rb6
-rw-r--r--app/services/work_items/parent_links/create_service.rb13
-rw-r--r--app/services/work_items/parent_links/destroy_service.rb10
-rw-r--r--app/views/dashboard/todos/index.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml300
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/shared/nav/_admin_scope_header.html.haml6
-rw-r--r--db/migrate/20230406040908_add_system_note_metadata_id__to_resource_link_events.rb7
-rw-r--r--db/migrate/20230406042906_add_unique_index_to_resource_link_events_on_system_note_metadata_id.rb15
-rw-r--r--db/migrate/20230406043900_add_system_note_metadata_foreign_key_to_resource_link_events.rb16
-rw-r--r--db/migrate/20230406073847_validate_foreign_key_for_resource_link_events_on_system_note_metadata_id.rb11
-rw-r--r--db/schema_migrations/202304060409081
-rw-r--r--db/schema_migrations/202304060429061
-rw-r--r--db/schema_migrations/202304060439001
-rw-r--r--db/schema_migrations/202304060738471
-rw-r--r--db/structure.sql8
-rw-r--r--doc/ci/pipelines/cicd_minutes.md4
-rw-r--r--doc/subscriptions/choosing_subscription.md61
-rw-r--r--doc/subscriptions/community_programs.md80
-rw-r--r--doc/subscriptions/customers_portal.md109
-rw-r--r--doc/subscriptions/gitlab_com/index.md4
-rw-r--r--doc/subscriptions/index.md271
-rw-r--r--doc/subscriptions/self_managed/index.md66
-rw-r--r--lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb6
-rw-r--r--lib/gitlab/github_import/importer/pull_request_review_importer.rb6
-rw-r--r--lib/gitlab/github_import/user_finder.rb20
-rw-r--r--lib/gitlab/harbor/client.rb6
-rw-r--r--lib/sidebars/admin/base_menu.rb14
-rw-r--r--lib/sidebars/admin/menus/abuse_reports_menu.rb39
-rw-r--r--lib/sidebars/admin/menus/admin_overview_menu.rb94
-rw-r--r--lib/sidebars/admin/menus/admin_settings_menu.rb146
-rw-r--r--lib/sidebars/admin/menus/analytics_menu.rb53
-rw-r--r--lib/sidebars/admin/menus/applications_menu.rb29
-rw-r--r--lib/sidebars/admin/menus/ci_cd_menu.rb47
-rw-r--r--lib/sidebars/admin/menus/deploy_keys_menu.rb29
-rw-r--r--lib/sidebars/admin/menus/kubernetes_menu.rb34
-rw-r--r--lib/sidebars/admin/menus/labels_menu.rb29
-rw-r--r--lib/sidebars/admin/menus/messages_menu.rb29
-rw-r--r--lib/sidebars/admin/menus/monitoring_menu.rb73
-rw-r--r--lib/sidebars/admin/menus/spam_logs_menu.rb34
-rw-r--r--lib/sidebars/admin/menus/system_hooks_menu.rb29
-rw-r--r--lib/sidebars/admin/panel.rb48
-rw-r--r--locale/gitlab.pot66
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/page/admin/menu.rb57
-rw-r--r--spec/controllers/help_controller_spec.rb9
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js10
-rw-r--r--spec/frontend/search/mock_data.js75
-rw-r--r--spec/frontend/search/store/getters_spec.js8
-rw-r--r--spec/frontend/search/store/utils_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js35
-rw-r--r--spec/graphql/resolvers/paginated_tree_resolver_spec.rb12
-rw-r--r--spec/helpers/sidebars_helper_spec.rb8
-rw-r--r--spec/helpers/tree_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb35
-rw-r--r--spec/lib/gitlab/harbor/client_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb42
-rw-r--r--spec/lib/sidebars/admin/menus/admin_overview_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/analytics_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/applications_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/ci_cd_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/deploy_keys_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/labels_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/messages_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/menus/system_hooks_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/admin/panel_spec.rb15
-rw-r--r--spec/models/concerns/has_user_type_spec.rb2
-rw-r--r--spec/models/integrations/harbor_spec.rb2
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/policies/global_policy_spec.rb25
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb54
-rw-r--r--spec/services/work_items/parent_links/destroy_service_spec.rb36
-rw-r--r--spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb74
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb11
105 files changed, 1902 insertions, 735 deletions
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 85ad38c582c..9f28d5d9d5e 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-v15.11.0-rc2
+v15.11.0
diff --git a/Gemfile b/Gemfile
index 2d71fd1527d..ff1116f7504 100644
--- a/Gemfile
+++ b/Gemfile
@@ -505,7 +505,7 @@ gem 'net-ntp'
gem 'ssh_data', '~> 1.3'
# Spamcheck GRPC protocol definitions
-gem 'spamcheck', '~> 1.2.0'
+gem 'spamcheck', '~> 1.3.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 15.9.0-rc3'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index ac037cd00af..4a0022198f4 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -582,7 +582,7 @@
{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
{"name":"sorted_set","version":"1.0.3","platform":"java","checksum":"996283f2e5c6e838825bcdcee31d6306515ae5f24bcb0ee4ce09dfff32919b8c"},
{"name":"sorted_set","version":"1.0.3","platform":"ruby","checksum":"4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f"},
-{"name":"spamcheck","version":"1.2.0","platform":"ruby","checksum":"6aa33f8d6bf2d000fedaa9dbb91e5e96fbe252c682cc56c28c037e2476118d51"},
+{"name":"spamcheck","version":"1.3.0","platform":"ruby","checksum":"a46082752257838d8484c844736e309ec499f85dcc51283a5f973b33f1c994f5"},
{"name":"spring","version":"4.1.0","platform":"ruby","checksum":"f17f080fb0df558d663c897a6229ed3d5cc54819ab51876ea6eef49a67f0a3cb"},
{"name":"spring-commands-rspec","version":"1.0.4","platform":"ruby","checksum":"6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c"},
{"name":"sprite-factory","version":"1.7.1","platform":"ruby","checksum":"5586524a1aec003241f1abc6852b61433e988aba5ee2b55f906387bf49b01ba2"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 2bd006d7a97..dbd315129ec 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1466,7 +1466,7 @@ GEM
sorted_set (1.0.3)
rbtree
set (~> 1.0)
- spamcheck (1.2.0)
+ spamcheck (1.3.0)
grpc (~> 1.0)
spring (4.1.0)
spring-commands-rspec (1.0.4)
@@ -1924,7 +1924,7 @@ DEPENDENCIES
slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.8.0)
solargraph (~> 0.47.2)
- spamcheck (~> 1.2.0)
+ spamcheck (~> 1.3.0)
spring (~> 4.1.0)
spring-commands-rspec (~> 1.0.4)
sprite-factory (~> 1.7)
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index 81f9b1f0af5..55cf38dfcbb 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -80,7 +80,7 @@ export default {
<template>
<editor-state-observer @transaction="updateDiagramPreview">
<node-view-wrapper
- :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`"
+ :class="`content-editor-code-block gl-relative code highlight gl-p-3 ${$options.userColorScheme}`"
as="pre"
>
<div
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
index 7c6ff002014..373c5970e64 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
@@ -42,8 +42,8 @@ export default {
},
mounted() {
this.gitlabBasePath = retrieveBaseUrl();
- setApiBaseURL(this.gitlabBasePath);
if (this.gitlabBasePath !== GITLAB_COM_BASE_PATH) {
+ setApiBaseURL(this.gitlabBasePath);
this.showSetupInstructions = true;
}
},
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index dee8d11c065..2e6fcbea80d 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -53,6 +53,15 @@ export const typePolicies = {
TreeEntry: {
keyFields: ['webPath'],
},
+ Subscription: {
+ fields: {
+ aiCompletionResponse: {
+ read(value) {
+ return value ?? null;
+ },
+ },
+ },
+ },
};
export const stripWhitespaceFromQuery = (url, path) => {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a1539aba786..fd002e29afc 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -89,9 +89,11 @@ initRails();
function deferredInitialisation() {
const $body = $('body');
- if (!gon.use_new_navigation) initTopNav();
+ if (!gon.use_new_navigation) {
+ initTopNav();
+ initTodoToggle();
+ }
initBreadcrumbs();
- initTodoToggle();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
initServicePingConsent();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 6d5460da2e5..dee13f60008 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -61,7 +61,14 @@ const initRefSwitcher = () => {
initRefSwitcher();
if (viewBlobEl) {
- const { blobPath, projectPath, targetBranch, originalBranch } = viewBlobEl.dataset;
+ const {
+ blobPath,
+ projectPath,
+ targetBranch,
+ originalBranch,
+ resourceId,
+ userId,
+ } = viewBlobEl.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -72,6 +79,8 @@ if (viewBlobEl) {
provide: {
targetBranch,
originalBranch,
+ resourceId,
+ userId,
},
render(createElement) {
return createElement(BlobContentViewer, {
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index b5568393313..294c0c13648 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -32,7 +32,7 @@ Vue.use(PerformancePlugin, {
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
- const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
+ const { projectPath, projectShortPath, ref, escapedRef, fullName, resourceId, userId } = dataset;
const router = createRouter(projectPath, escapedRef);
apolloProvider.clients.defaultClient.cache.writeQuery({
@@ -281,6 +281,7 @@ export default function setupVueRepositoryList() {
store: createStore(),
router,
apolloProvider,
+ provide: { resourceId, userId },
render(h) {
return h(App);
},
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index d71785d7fac..1e4b1e36514 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -15,6 +15,7 @@ export const initSearchApp = () => {
const store = createStore({
query,
navigation,
+ useNewNavigation: gon.use_new_navigation,
});
initTopbar(store);
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index 3f586c5fed8..c8ee0a3f9d9 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -17,3 +17,15 @@ export const SIDEBAR_PARAMS = [
];
export const NUMBER_FORMATING_OPTIONS = { notation: 'compact', compactDisplay: 'short' };
+
+export const ICON_MAP = {
+ blobs: 'code',
+ issues: 'issues',
+ merge_requests: 'merge-request',
+ commits: 'commit',
+ notes: 'comments',
+ milestones: 'tag',
+ users: 'users',
+ projects: 'project',
+ wiki_blobs: 'overview',
+};
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index 0e387607af7..135c9a3d67c 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -1,7 +1,8 @@
import { findKey, has } from 'lodash';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
+import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
-import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
export const frequentGroups = (state) => {
return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
@@ -26,3 +27,13 @@ export const queryLanguageFilters = (state) => state.query[languageFilterData.fi
export const currentUrlQueryHasLanguageFilters = (state) =>
has(state.urlQuery, languageFilterData.filterParam) &&
state.urlQuery[languageFilterData.filterParam]?.length > 0;
+
+export const navigationItems = (state) =>
+ Object.values(state.navigation).map((item) => ({
+ title: item.label,
+ icon: ICON_MAP[item.scope] || '',
+ link: item.link,
+ is_active: Boolean(item?.active),
+ pill_count: `${formatSearchResultCount(item?.count)}${addCountOverLimit(item?.count)}` || '',
+ items: [],
+ }));
diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js
index e20a43808cf..634f8f7a7fa 100644
--- a/app/assets/javascripts/search/store/index.js
+++ b/app/assets/javascripts/search/store/index.js
@@ -7,11 +7,11 @@ import createState from './state';
Vue.use(Vuex);
-export const getStoreConfig = ({ query, navigation }) => ({
+export const getStoreConfig = ({ query, navigation, useNewNavigation }) => ({
actions,
getters,
mutations,
- state: createState({ query, navigation }),
+ state: createState({ query, navigation, useNewNavigation }),
});
const createStore = (config) => new Vuex.Store(getStoreConfig(config));
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index d85a135bb4e..a62b6728819 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
-const createState = ({ query, navigation }) => ({
+const createState = ({ query, navigation, useNewNavigation }) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
@@ -14,6 +14,7 @@ const createState = ({ query, navigation }) => ({
},
sidebarDirty: false,
navigation,
+ useNewNavigation,
aggregations: {
error: false,
fetching: false,
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index 9d1743e64ad..2f02ef3475c 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -144,3 +144,7 @@ export const prepareSearchAggregations = (state, aggregationData) =>
return item;
});
+
+export const addCountOverLimit = (count = '') => {
+ return count.includes('+') ? '+' : '';
+};
diff --git a/app/assets/javascripts/sidebar/utils.js b/app/assets/javascripts/sidebar/utils.js
index 6b90fb80abf..a61b4e4f066 100644
--- a/app/assets/javascripts/sidebar/utils.js
+++ b/app/assets/javascripts/sidebar/utils.js
@@ -12,7 +12,7 @@ export const updateGlobalTodoCount = (additionalTodoCount) => {
if (countContainer === null) return;
- const currentCount = parseInt(countContainer.innerText, 10);
+ const currentCount = parseInt(countContainer.innerText, 10) || 0;
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index b69ebc6be17..e96b896825a 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -2,6 +2,7 @@
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 logo from '../../../../views/shared/_logo.svg';
import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
import CreateMenu from './create_menu.vue';
@@ -59,12 +60,27 @@ export default {
data() {
return {
mrMenuShown: false,
+ todoCount: this.sidebarData.todos_pending_count,
};
},
+ computed: {
+ formattedTodoCount() {
+ return highCountTrim(this.todoCount);
+ },
+ },
+ mounted() {
+ document.addEventListener('todo:toggle', this.updateTodos);
+ },
+ beforeDestroy() {
+ document.removeEventListener('todo:toggle', this.updateTodos);
+ },
methods: {
collapseSidebar() {
toggleSuperSidebarCollapsed(true, true, true);
},
+ updateTodos(e) {
+ this.todoCount = e.detail.count || 0;
+ },
},
};
</script>
@@ -94,8 +110,9 @@ export default {
:href="sidebarData.canary_toggle_com_url"
size="sm"
class="gl-ml-2"
- >{{ $options.NEXT_LABEL }}</gl-badge
>
+ {{ $options.NEXT_LABEL }}
+ </gl-badge>
<div class="gl-flex-grow-1"></div>
<gl-button
v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.collapseSidebar"
@@ -165,9 +182,9 @@ export default {
</merge-request-menu>
<counter
v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
- class="gl-flex-basis-third shortcuts-todos"
+ class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
- :count="sidebarData.todos_pending_count"
+ :count="formattedTodoCount"
href="/dashboard/todos"
:label="$options.i18n.todoList"
data-qa-selector="todos_shortcut_button"
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index f2c27cf611e..0577279cdd0 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -53,11 +53,11 @@ export default {
},
methods: {
updateToDoCount(add) {
- const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
+ const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10) || 0;
const count = add ? oldCount + 1 : oldCount - 1;
const headerTodoEvent = new CustomEvent('todo:toggle', {
detail: {
- count,
+ count: Math.max(count, 0),
},
});
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
index 6c4e978125e..8fd80b1a9b9 100644
--- a/app/graphql/resolvers/paginated_tree_resolver.rb
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -22,7 +22,7 @@ module Resolvers
alias_method :repository, :object
def resolve(**args)
- return unless repository.exists?
+ return if repository.empty?
cursor = args.delete(:after)
args[:ref] ||= :head
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 88f1ef1a8a8..b101f184ca6 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -97,7 +97,7 @@ module NavHelper
def super_sidebar_supported?
return true if @nav.nil?
- %w(your_work explore project group profile user_profile search).include?(@nav)
+ %w(your_work explore project group profile user_profile search admin).include?(@nav)
end
def get_header_links
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 8af4204e5e1..bf223cfe41c 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -65,7 +65,7 @@ module SidebarsHelper
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: format_user_bar_count(user.todos_pending_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),
@@ -116,6 +116,8 @@ module SidebarsHelper
when 'search'
context = Sidebars::Context.new(current_user: user, container: nil, **context_adds)
Sidebars::Search::Panel.new(context)
+ when 'admin'
+ Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds))
else
context = your_work_sidebar_context(user, **context_adds)
Sidebars::YourWork::Panel.new(context)
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index b8d6434d9c9..010c88179df 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -97,7 +97,7 @@ module ApplicationSettingImplementation
group_import_limit: 6,
help_page_hide_commercial_content: false,
help_page_text: nil,
- help_page_documentation_base_url: nil,
+ help_page_documentation_base_url: 'https://docs.gitlab.com',
hide_third_party_offers: false,
housekeeping_enabled: true,
housekeeping_full_repack_period: 50,
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 795ba94776b..468ea26c51a 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -18,7 +18,8 @@ module HasUserType
security_policy_bot: 10, # Currently not in use. See https://gitlab.com/gitlab-org/gitlab/-/issues/384174
admin_bot: 11,
suggested_reviewers_bot: 12,
- service_account: 13
+ service_account: 13,
+ llm_bot: 14
}.with_indifferent_access.freeze
BOT_USER_TYPES = %w[
@@ -33,6 +34,7 @@ module HasUserType
admin_bot
suggested_reviewers_bot
service_account
+ llm_bot
].freeze
# `service_account` allows instance/namespaces to configure a user for external integrations/automations
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 01a04743d5d..079811e0df0 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -17,7 +17,8 @@ module Integrations
field :project_name,
title: -> { s_('HarborIntegration|Harbor project name') },
- help: -> { s_('HarborIntegration|The name of the project in Harbor.') }
+ help: -> { s_('HarborIntegration|The name of the project in Harbor.') },
+ required: true
field :username,
title: -> { s_('HarborIntegration|Harbor username') },
@@ -62,7 +63,7 @@ module Integrations
end
def test(*_args)
- client.ping
+ client.check_project_availability
end
def ci_variables
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 62252912c32..ac41b5d0b2c 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -76,7 +76,7 @@ class Todo < ApplicationRecord
scope :for_target, -> (id) { where(target_id: id) }
scope :for_commit, -> (id) { where(commit_id: id) }
scope :with_entity_associations, -> do
- preload(:target, :author, :note, group: :route, project: [:route, { namespace: [:route, :owner] }, :project_setting])
+ preload(:target, :author, :note, group: :route, project: [:route, :group, { namespace: [:route, :owner] }, :project_setting])
end
scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
scope :for_internal_notes, -> { joins(:note).where(note: { confidential: true }) }
diff --git a/app/models/user.rb b/app/models/user.rb
index 71ea185b6f1..044c9d3c24b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -924,6 +924,17 @@ class User < ApplicationRecord
end
end
+ def llm_bot
+ email_pattern = "llm-bot%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(user_type: :llm_bot), 'GitLab-Llm-Bot', email_pattern) do |u|
+ u.bio = 'The Gitlab LLM bot used for fetching LLM-generated content'
+ u.name = 'GitLab LLM Bot'
+ u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for llm-bot
+ u.confirmed_at = Time.zone.now
+ end
+ end
+
def admin_bot
email_pattern = "admin-bot%s@#{Settings.gitlab.host}"
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 1ce866bd910..7c745c5731f 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -39,6 +39,10 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:automation_bot) { @user&.automation_bot? }
+ desc "User is llm bot"
+ with_options scope: :user, score: 0
+ condition(:llm_bot) { @user&.llm_bot? }
+
desc "User email is unconfirmed or user account is locked"
with_options scope: :user, score: 0
condition(:inactive) { @user&.confirmation_required_on_sign_in? || @user&.access_locked? }
@@ -63,7 +67,7 @@ class BasePolicy < DeclarativePolicy::Base
end
rule { admin }.policy do
- # Only for actual administrator accounts, behaviour affected by admin mode application setting
+ # Only for actual administrator accounts, behavior affected by admin mode application setting
enable :admin_all_resources
# Policy extended in EE to also enable auditors
enable :read_all_resources
diff --git a/app/services/work_items/parent_links/create_service.rb b/app/services/work_items/parent_links/create_service.rb
index 60747daa5f8..4747d2f17e4 100644
--- a/app/services/work_items/parent_links/create_service.rb
+++ b/app/services/work_items/parent_links/create_service.rb
@@ -10,7 +10,18 @@ module WorkItems
link = set_parent(issuable, work_item)
link.move_to_end
- create_notes(work_item) if link.changed? && link.save
+
+ if link.changed? && link.save
+ relate_child_note = create_notes(work_item)
+
+ ResourceLinkEvent.create(
+ user: current_user,
+ work_item: link.work_item_parent,
+ child_work_item: link.work_item,
+ action: ResourceLinkEvent.actions[:add],
+ system_note_metadata_id: relate_child_note&.system_note_metadata&.id
+ )
+ end
link
end
diff --git a/app/services/work_items/parent_links/destroy_service.rb b/app/services/work_items/parent_links/destroy_service.rb
index 19770b3e4b5..97145d0b360 100644
--- a/app/services/work_items/parent_links/destroy_service.rb
+++ b/app/services/work_items/parent_links/destroy_service.rb
@@ -15,7 +15,15 @@ module WorkItems
private
def create_notes
- SystemNoteService.unrelate_work_item(parent, child, current_user)
+ unrelate_note = SystemNoteService.unrelate_work_item(parent, child, current_user)
+
+ ResourceLinkEvent.create(
+ user: @current_user,
+ work_item: @link.work_item_parent,
+ child_work_item: @link.work_item,
+ action: ResourceLinkEvent.actions[:remove],
+ system_note_metadata_id: unrelate_note&.system_note_metadata&.id
+ )
end
def not_found_message
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 10b1e257ac6..ca6b1071f03 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,6 +2,7 @@
= render_two_factor_auth_recovery_settings_check
= render_dashboard_ultimate_trial(current_user)
+= render_if_exists 'dashboard/todos/saml_reauth_notice'
- add_page_specific_style 'page_bundles/todos'
- add_page_specific_style 'page_bundles/issuable'
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 2d61e403623..bffc030dbd9 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,299 +1 @@
-%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation'), data: { qa_selector: 'admin_sidebar_content' } }
- .nav-sidebar-inner-scroll
- .context-header
- = link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
- %span{ class: ['avatar-container', 'settings-avatar', 'rect-avatar', 's32'] }
- = sprite_icon('admin', size: 18)
- %span.sidebar-context-title
- = _('Admin Area')
- %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_overview_submenu_content' } }
- = nav_link(controller: %w[dashboard admin admin/projects users groups admin/topics gitaly_servers cohorts], html_options: {class: 'home'}) do
- = link_to admin_root_path, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('overview')
- %span.nav-item-name
- = _('Overview')
- %ul.sidebar-sub-level-items
- = nav_link(controller: %w[dashboard admin admin/projects users groups gitaly_servers cohorts], html_options: { class: "fly-out-top-item" }) do
- = link_to admin_root_path do
- %strong.fly-out-top-item-name
- = _('Overview')
- %li.divider.fly-out-top-item
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: _('Overview') do
- %span
- = _('Dashboard')
- = nav_link(controller: [:admin, 'admin/projects']) do
- = link_to admin_projects_path, title: _('Projects') do
- %span
- = _('Projects')
- = nav_link(controller: %w[users cohorts]) do
- = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'admin_overview_users_link' } do
- %span
- = _('Users')
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'admin_overview_groups_link' } do
- %span
- = _('Groups')
- = nav_link(controller: [:admin, 'admin/topics']) do
- = link_to admin_topics_path, title: _('Topics') do
- %span
- = _('Topics')
- = nav_link(controller: :gitaly_servers) do
- = link_to admin_gitaly_servers_path, title: 'Gitaly Servers' do
- %span
- = _('Gitaly Servers')
-
- = nav_link(controller: %w[runners jobs]) do
- = link_to admin_runners_path, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('rocket')
- %span.nav-item-name
- = _('CI/CD')
- %ul.sidebar-sub-level-items
- = nav_link(controller: %w[runners jobs], html_options: { class: "fly-out-top-item" }) do
- = link_to admin_runners_path do
- %strong.fly-out-top-item-name
- = _('CI/CD')
- %li.divider.fly-out-top-item
- = nav_link(controller: :runners) do
- = link_to admin_runners_path, title: _('Runners') do
- %span
- = _('Runners')
- = nav_link(controller: :jobs) do
- = link_to admin_jobs_path, title: _('Jobs') do
- %span
- = _('Jobs')
-
- = nav_link(controller: admin_analytics_nav_links) do
- = link_to admin_dev_ops_reports_path, data: { qa_selector: 'admin_analytics_link' }, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('chart')
- %span.nav-item-name
- = _('Analytics')
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_analytics_submenu_content' } }
- = nav_link(controller: admin_analytics_nav_links, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_dev_ops_reports_path do
- %strong.fly-out-top-item-name
- = _('Analytics')
- %li.divider.fly-out-top-item
- = nav_link(controller: :dev_ops_report) do
- = link_to admin_dev_ops_reports_path, title: _('DevOps Reports') do
- %span
- = _('DevOps Reports')
- = nav_link(controller: :usage_trends) do
- = link_to admin_usage_trends_path, title: _('Usage Trends') do
- %span
- = _('Usage Trends')
-
- = nav_link(controller: admin_monitoring_nav_links) do
- = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_menu_link' }, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('monitor')
- %span.nav-item-name
- = _('Monitoring')
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
- = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_system_info_path do
- %strong.fly-out-top-item-name
- = _('Monitoring')
- %li.divider.fly-out-top-item
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: _('System Info') do
- %span
- = _('System Info')
- = nav_link(controller: :background_migrations) do
- = link_to admin_background_migrations_path, title: _('Background Migrations') do
- %span
- = _('Background Migrations')
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: _('Background Jobs') do
- %span
- = _('Background Jobs')
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: _('Health Check') do
- %span
- = _('Health Check')
- - if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
- = nav_link do
- = link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard'), rel: 'noopener noreferrer' do
- %span
- = _('Metrics Dashboard')
- = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'
-
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path do
- .nav-icon-container
- = sprite_icon('messages')
- %span.nav-item-name
- = _('Messages')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_broadcast_messages_path do
- %strong.fly-out-top-item-name
- = _('Messages')
-
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to admin_hooks_path do
- .nav-icon-container
- = sprite_icon('hook')
- %span.nav-item-name
- = _('System Hooks')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" }) do
- = link_to admin_hooks_path do
- %strong.fly-out-top-item-name
- = _('System Hooks')
-
- = nav_link(controller: :applications) do
- = link_to admin_applications_path do
- .nav-icon-container
- = sprite_icon('applications')
- %span.nav-item-name
- = _('Applications')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_applications_path do
- %strong.fly-out-top-item-name
- = _('Applications')
-
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path do
- .nav-icon-container
- = sprite_icon('slight-frown')
- %span.nav-item-name
- = _('Abuse Reports')
- = gl_badge_tag number_with_delimiter(AbuseReport.count(:all)), variant: :info, size: :sm
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_abuse_reports_path do
- %strong.fly-out-top-item-name
- = _('Abuse Reports')
- = gl_badge_tag number_with_delimiter(AbuseReport.count(:all)), variant: :info, size: :sm
-
- = render_if_exists 'layouts/nav/sidebar/licenses_link'
-
- - if instance_clusters_enabled?
- = nav_link(controller: :clusters) do
- = link_to admin_clusters_path do
- .nav-icon-container
- = sprite_icon('cloud-gear')
- %span.nav-item-name
- = _('Kubernetes')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_clusters_path do
- %strong.fly-out-top-item-name
- = _('Kubernetes')
-
- - if anti_spam_service_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path do
- .nav-icon-container
- = sprite_icon('spam')
- %span.nav-item-name
- = _('Spam Logs')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_spam_logs_path do
- %strong.fly-out-top-item-name
- = _('Spam Logs')
-
- = render_if_exists 'layouts/nav/sidebar/push_rules_link'
-
- = render_if_exists 'layouts/nav/ee/admin/geo_sidebar'
-
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path do
- .nav-icon-container
- = sprite_icon('key')
- %span.nav-item-name
- = _('Deploy Keys')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_deploy_keys_path do
- %strong.fly-out-top-item-name
- = _('Deploy Keys')
-
- = render_if_exists 'layouts/nav/sidebar/credentials_link'
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path do
- .nav-icon-container
- = sprite_icon('labels')
- %span.nav-item-name
- = _('Labels')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_labels_path do
- %strong.fly-out-top-item-name
- = _('Labels')
-
- = nav_link(controller: [:application_settings, :integrations, :appearances]) do
- = link_to general_admin_application_settings_path, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('settings')
- %span.nav-item-name{ data: { qa_selector: 'admin_settings_menu_link' } }
- = _('Settings')
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
- -# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
- = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" }) do
- = link_to general_admin_application_settings_path do
- %strong.fly-out-top-item-name
- = _('Settings')
- %li.divider.fly-out-top-item
- = nav_link(path: 'application_settings#general') do
- = link_to general_admin_application_settings_path, title: _('General'), data: { qa_selector: 'admin_settings_general_link' } do
- %span
- = _('General')
-
- = render_if_exists 'layouts/nav/sidebar/advanced_search', data: { qa_selector: 'admin_settings_advanced_search_link' }
-
- - if instance_level_integrations?
- = nav_link(path: ['application_settings#integrations', 'integrations#edit']) do
- = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'admin_settings_integrations_link' } do
- %span
- = _('Integrations')
- = nav_link(path: 'application_settings#repository') do
- = link_to repository_admin_application_settings_path, title: _('Repository'), data: { qa_selector: 'admin_settings_repository_link' } do
- %span
- = _('Repository')
- - if Gitlab.ee? && License.feature_available?(:custom_file_templates)
- = nav_link(path: 'application_settings#templates') do
- = link_to templates_admin_application_settings_path, title: _('Templates'), data: { qa_selector: 'admin_settings_templates_link' } do
- %span
- = _('Templates')
- = nav_link(path: 'application_settings#ci_cd') do
- = link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do
- %span
- = _('CI/CD')
- = render_if_exists 'layouts/nav/ee/admin/security_and_compliance_sidebar'
- = nav_link(path: 'application_settings#reporting') do
- = link_to reporting_admin_application_settings_path, title: _('Reporting') do
- %span
- = _('Reporting')
- = nav_link(path: 'application_settings#metrics_and_profiling') do
- = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), data: { qa_selector: 'admin_settings_metrics_and_profiling_link' } do
- %span
- = _('Metrics and profiling')
- = nav_link(path: ['application_settings#service_usage_data']) do
- = link_to service_usage_data_admin_application_settings_path, title: _('Service usage data') do
- %span
- = _('Service usage data')
- = nav_link(path: 'application_settings#network') do
- = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
- %span
- = _('Network')
- = nav_link(controller: :appearances) do
- = link_to admin_application_settings_appearances_path do
- %span
- = _('Appearance')
- = nav_link(path: 'application_settings#preferences') do
- = link_to preferences_admin_application_settings_path, title: _('Preferences'), data: { qa_selector: 'admin_settings_preferences_link' } do
- %span
- = _('Preferences')
-
- = render 'shared/sidebar_toggle_button'
+= render partial: 'shared/nav/sidebar', object: Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: current_user, container: nil))
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 6565cd223e8..d11bf36a610 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -24,6 +24,8 @@
-# Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/330406
#js-view-blob-app{ data: { blob_path: blob.path,
project_path: @project.full_path,
+ resource_id: @project.to_global_id,
+ user_id: current_user.present? ? current_user.to_global_id : '',
target_branch: project.empty_repo? ? ref : @ref,
original_branch: @ref } }
= gl_loading_icon(size: 'md')
diff --git a/app/views/shared/nav/_admin_scope_header.html.haml b/app/views/shared/nav/_admin_scope_header.html.haml
new file mode 100644
index 00000000000..3a18b3660d4
--- /dev/null
+++ b/app/views/shared/nav/_admin_scope_header.html.haml
@@ -0,0 +1,6 @@
+%li.context-header
+ = link_to admin_root_path, title: _('Admin Area'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
+ %span.avatar-container.icon-avatar.rect-avatar.s32
+ = sprite_icon('admin', size: 18)
+ %span.sidebar-context-title
+ = _('Admin Area')
diff --git a/db/migrate/20230406040908_add_system_note_metadata_id__to_resource_link_events.rb b/db/migrate/20230406040908_add_system_note_metadata_id__to_resource_link_events.rb
new file mode 100644
index 00000000000..042e588a5f5
--- /dev/null
+++ b/db/migrate/20230406040908_add_system_note_metadata_id__to_resource_link_events.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSystemNoteMetadataIdToResourceLinkEvents < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :resource_link_events, :system_note_metadata_id, :bigint
+ end
+end
diff --git a/db/migrate/20230406042906_add_unique_index_to_resource_link_events_on_system_note_metadata_id.rb b/db/migrate/20230406042906_add_unique_index_to_resource_link_events_on_system_note_metadata_id.rb
new file mode 100644
index 00000000000..97d2e89c80b
--- /dev/null
+++ b/db/migrate/20230406042906_add_unique_index_to_resource_link_events_on_system_note_metadata_id.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUniqueIndexToResourceLinkEventsOnSystemNoteMetadataId < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'unique_index_on_system_note_metadata_id'
+
+ def up
+ add_concurrent_index :resource_link_events, :system_note_metadata_id, unique: true, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :resource_link_events, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20230406043900_add_system_note_metadata_foreign_key_to_resource_link_events.rb b/db/migrate/20230406043900_add_system_note_metadata_foreign_key_to_resource_link_events.rb
new file mode 100644
index 00000000000..431e7ac0e7e
--- /dev/null
+++ b/db/migrate/20230406043900_add_system_note_metadata_foreign_key_to_resource_link_events.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddSystemNoteMetadataForeignKeyToResourceLinkEvents < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :resource_link_events, :system_note_metadata,
+ column: :system_note_metadata_id, on_delete: :cascade, validate: false
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :resource_link_events, column: :system_note_metadata_id
+ end
+ end
+end
diff --git a/db/migrate/20230406073847_validate_foreign_key_for_resource_link_events_on_system_note_metadata_id.rb b/db/migrate/20230406073847_validate_foreign_key_for_resource_link_events_on_system_note_metadata_id.rb
new file mode 100644
index 00000000000..f06657ccabc
--- /dev/null
+++ b/db/migrate/20230406073847_validate_foreign_key_for_resource_link_events_on_system_note_metadata_id.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ValidateForeignKeyForResourceLinkEventsOnSystemNoteMetadataId < Gitlab::Database::Migration[2.1]
+ def up
+ validate_foreign_key :resource_link_events, :system_note_metadata_id
+ end
+
+ def down
+ # No-op
+ end
+end
diff --git a/db/schema_migrations/20230406040908 b/db/schema_migrations/20230406040908
new file mode 100644
index 00000000000..e5ca15fe7e6
--- /dev/null
+++ b/db/schema_migrations/20230406040908
@@ -0,0 +1 @@
+33c9561c8f21f756095ff7396b78bf78bd7f41e18e508c89b759d367d5e920ba \ No newline at end of file
diff --git a/db/schema_migrations/20230406042906 b/db/schema_migrations/20230406042906
new file mode 100644
index 00000000000..a32d144741b
--- /dev/null
+++ b/db/schema_migrations/20230406042906
@@ -0,0 +1 @@
+b6608c1bd719c6fcfe701b11737ebfa7a8db8da795a0037d08bb7adc0c9b40e2 \ No newline at end of file
diff --git a/db/schema_migrations/20230406043900 b/db/schema_migrations/20230406043900
new file mode 100644
index 00000000000..e41be929522
--- /dev/null
+++ b/db/schema_migrations/20230406043900
@@ -0,0 +1 @@
+83c9ff25c312bd1c11e9be4fc8da12f89ec72174a0b303513899ee2fcd18e4d3 \ No newline at end of file
diff --git a/db/schema_migrations/20230406073847 b/db/schema_migrations/20230406073847
new file mode 100644
index 00000000000..8c854a9350c
--- /dev/null
+++ b/db/schema_migrations/20230406073847
@@ -0,0 +1 @@
+58b22a8ed1ea4f21b8a3b42ffdf4853c111d8c462ffbd97d5f7080e2c0587473 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d0bf9a5e91a..c274a315449 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -21751,7 +21751,8 @@ CREATE TABLE resource_link_events (
user_id bigint NOT NULL,
issue_id bigint NOT NULL,
child_work_item_id bigint NOT NULL,
- created_at timestamp with time zone NOT NULL
+ created_at timestamp with time zone NOT NULL,
+ system_note_metadata_id bigint
);
CREATE SEQUENCE resource_link_events_id_seq
@@ -32869,6 +32870,8 @@ CREATE UNIQUE INDEX unique_index_ci_build_pending_states_on_partition_id_build_i
CREATE UNIQUE INDEX unique_index_for_project_pages_unique_domain ON project_settings USING btree (pages_unique_domain) WHERE (pages_unique_domain IS NOT NULL);
+CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_events USING btree (system_note_metadata_id);
+
CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id);
CREATE UNIQUE INDEX unique_packages_project_id_and_name_and_version_when_debian ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 9) AND (status <> 4));
@@ -34441,6 +34444,9 @@ ALTER TABLE ONLY merge_requests_compliance_violations
ALTER TABLE ONLY coverage_fuzzing_corpuses
ADD CONSTRAINT fk_29f6f15f82 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY resource_link_events
+ ADD CONSTRAINT fk_2a039c40f4 FOREIGN KEY (system_note_metadata_id) REFERENCES system_note_metadata(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT fk_2a0421d824 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/ci/pipelines/cicd_minutes.md b/doc/ci/pipelines/cicd_minutes.md
index 2528a53088d..f23ee5fad10 100644
--- a/doc/ci/pipelines/cicd_minutes.md
+++ b/doc/ci/pipelines/cicd_minutes.md
@@ -223,8 +223,8 @@ The cost factors for jobs running on shared runners on GitLab.com are:
- `1` for internal, public, and private projects.
- Exceptions for public projects:
- - `0.5` for projects in the [GitLab for Open Source program](../../subscriptions/index.md#gitlab-for-open-source).
- - `0.008` for forks of projects in the [GitLab for Open Source program](../../subscriptions/index.md#gitlab-for-open-source). For every 125 minutes of job execution time,
+ - `0.5` for projects in the [GitLab for Open Source program](../../subscriptions/community_programs.md#gitlab-for-open-source).
+ - `0.008` for forks of projects in the [GitLab for Open Source program](../../subscriptions/community_programs.md#gitlab-for-open-source). For every 125 minutes of job execution time,
you use 1 CI/CD minute.
- Discounted dynamically for [community contributions to GitLab projects](#cost-factor-for-community-contributions-to-gitlab-projects).
diff --git a/doc/subscriptions/choosing_subscription.md b/doc/subscriptions/choosing_subscription.md
new file mode 100644
index 00000000000..07b04c83bd7
--- /dev/null
+++ b/doc/subscriptions/choosing_subscription.md
@@ -0,0 +1,61 @@
+---
+stage: Fulfillment
+group: Purchase
+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
+---
+
+# Choosing a GitLab subscription
+
+To choose the right GitLab subscription, choose an offering and a tier.
+
+## Choose a subscription
+
+Choose which GitLab subscription suits your needs:
+
+- [GitLab SaaS](gitlab_com/index.md): The GitLab software-as-a-service offering.
+ You don't need to install anything to use GitLab SaaS, you only need to
+ [sign up](https://gitlab.com/users/sign_up) and start using GitLab straight away.
+- [GitLab Dedicated](gitlab_dedicated/index.md): A single-tenant SaaS service for highly regulated and large enterprises.
+- [GitLab self-managed](self_managed/index.md): Install, administer, and maintain
+ your own GitLab instance.
+
+On a GitLab self-managed instance, a GitLab subscription provides the same set of
+features for _all_ users. On GitLab SaaS, you can apply a subscription to a group
+namespace. You cannot apply a subscription to a personal namespace.
+
+NOTE:
+Subscriptions cannot be transferred between GitLab SaaS and GitLab self-managed.
+A new subscription must be purchased and applied as needed.
+
+## Choose a GitLab tier
+
+Pricing is [tier-based](https://about.gitlab.com/pricing/), allowing you to choose
+the features which fit your budget. For information on what features are available
+at each tier for each product, see the [GitLab self-managed feature comparison](https://about.gitlab.com/pricing/feature-comparison/).
+
+## Find your subscription
+
+The following chart should help you determine your subscription model. Select
+the list item to go to the respective help page.
+
+```mermaid
+graph TD
+
+A(Is your user account on GitLab.com?)
+A --> B(Yes)
+A --> C(No)
+B --> D(fa:fa-link View your subscription on GitLab.com)
+C --> E(fa:fa-link View your self-hosted subscription)
+
+click D "./gitlab_com/index.html#view-your-gitlabcom-subscription"
+click E "./self_managed/index.html#view-your-subscription"
+```
+
+## Contact Support
+
+- See the tiers of [GitLab Support](https://about.gitlab.com/support/).
+- [Submit a request](https://support.gitlab.com/hc/en-us/requests/new) through the Support Portal.
+
+We also encourage all users to search our project trackers for known issues and existing feature requests in the [GitLab project](https://gitlab.com/gitlab-org/gitlab/-/issues/).
+
+These issues are the best avenue for getting updates on specific product plans and for communicating directly with the relevant GitLab team members.
diff --git a/doc/subscriptions/community_programs.md b/doc/subscriptions/community_programs.md
new file mode 100644
index 00000000000..05c5d66b0e5
--- /dev/null
+++ b/doc/subscriptions/community_programs.md
@@ -0,0 +1,80 @@
+---
+stage: Fulfillment
+group: Purchase
+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
+---
+
+# Community programs
+
+GitLab provides the following community program subscriptions.
+
+## GitLab for Education
+
+For qualifying non-profit educational institutions, the [GitLab for Education Program](https://about.gitlab.com/solutions/education/) provides GitLab Ultimate, plus 50,000 CI/CD minutes per month. The subscription granted under GitLab for Education can only be used for instructional use or non-commercial academic research. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Education Program page](https://about.gitlab.com/solutions/education/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/education-program/).
+
+## GitLab for Open Source
+
+For qualifying open source projects, the [GitLab for Open Source Program](https://about.gitlab.com/solutions/open-source/) provides GitLab Ultimate, plus 50,000 CI/CD minutes per month. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Open Source Program page](https://about.gitlab.com/solutions/open-source/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/opensource-program/).
+
+### Meeting GitLab for Open Source Program requirements
+
+To meet GitLab for Open Source Program requirements, first add an OSI-approved open source license to all projects in your namespace.
+
+To add a license to a project:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the overview page, select **Add LICENSE**. If the license you want is not available as a license template, manually copy the entire, unaltered [text of your chosen license](https://opensource.org/licenses/) into the `LICENSE` file. Note that GitLab defaults to **All rights reserved** if users do not perform this action.
+
+![Add license](img/add-license.png)
+
+Applicants must add the correct license to each project in their respective groups or namespaces. When you're sure you're using OSI-approved licenses for your projects, you can take your screenshots.
+
+NOTE:
+GitLab for Open Source Program benefits apply to an entire GitLab namespace. To qualify for the GitLab for Open Source Program, all projects in an applicant's namespace must meet program requirements. Applicants submit materials related to one project in the applying namespace, and the open source program team uses that project to verify eligibility of the entire namespace.
+
+### Verification for Open Source Program
+
+Next, take screenshots of your project to confirm that project's eligibility. You must upload three screenshots:
+
+- [OSI-approved license overview](#screenshot-1-license-overview)
+- [OSI-approved license contents](#screenshot-2-license-contents)
+- [Publicly visible settings](#screenshot-3-publicly-visible-settings)
+
+NOTE:
+Benefits of the GitLab Open Source Program apply to all projects in a GitLab namespace. All projects in an eligible namespace must meet program requirements.
+
+#### Screenshot 1: License overview
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select your project avatar. If you haven't specified an avatar for your project, the avatar displays as a single letter.
+1. Take a screenshot of the project overview that clearly displays the license you've chosen for your project.
+
+![License overview](img/license-overview.png)
+
+#### Screenshot 2: License contents
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Repository** and locate the project's `LICENSE` file.
+1. Take a screenshot of the contents of the file. Make sure the screenshot includes the title of the license.
+
+![License file](img/license-file.png)
+
+#### Screenshot 3: Publicly visible settings
+
+To be eligible for the GitLab Open Source Program, projects must be publicly visible. To check your project's public visibility settings:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. From the left sidebar, select **Settings > General**.
+1. Expand **Visibility, project features, permissions**.
+1. From the **Project visibility** dropdown list, select **Public**.
+1. Select the **Users can request access** checkbox.
+1. Take a screenshot of this view. Include as much of the publicly visible settings as possible. Make sure to include your project's name in the upper-left of the screenshot.
+
+![Publicly visible setting](img/publicly-visible.png)
+
+NOTE:
+Exceptions to this public visibility requirement apply in select circumstances (for example, in cases where a project in an applicant's namespace may hold sensitive data). Email `opensource@gitlab.com` with details of your use case to request written permission for exceptions.
+
+## GitLab for Startups
+
+For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides GitLab Ultimate, plus 50,000 CI/CD minutes per month for 12 months. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Startups Program page](https://about.gitlab.com/solutions/startups/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/startups-program/).
diff --git a/doc/subscriptions/customers_portal.md b/doc/subscriptions/customers_portal.md
new file mode 100644
index 00000000000..3f493f255dc
--- /dev/null
+++ b/doc/subscriptions/customers_portal.md
@@ -0,0 +1,109 @@
+---
+stage: Fulfillment
+group: Purchase
+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
+---
+
+# The Customers Portal
+
+For some management tasks for your subscription and account, you use the Customers Portal.
+
+The Customers Portal is available to customers who purchased their
+subscription from GitLab. If you made your purchase through a partner or
+reseller, contact them directly for assistance with your subscription.
+
+You can also specifically manage your [GitLab SaaS subscription](gitlab_com/index.md)
+or [self-managed subscription](self_managed/index.md).
+
+## Change account owner information
+
+Account owner personal details are used on invoices. The account owner email address is used for the Customers Portal legacy login and license-related email.
+
+If you have registered a Customers Portal account through a GitLab.com account, the GitLab.com account is used for login.
+
+To change account owner information, including name, billing address, and email address:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select **My account > Account details**.
+1. Expand the **Personal details** section.
+1. Edit the personal details.
+1. Select **Save changes**.
+
+If you want to transfer ownership of the Customers Portal account
+to another person, after you enter that person's personal details, you must also:
+
+- [Change the Customers Portal account password](#change-customers-portal-account-password).
+- [Change the linked GitLab.com account](#change-the-linked-account), if you have one linked.
+
+## Change your company details
+
+To change your company details, including company name and VAT number:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select **My account > Account details**.
+1. Expand the **Company details** section.
+1. Edit the company details.
+1. Select **Save changes**.
+
+## Change your payment method
+
+Purchases in the Customers Portal require a credit card on record as a payment method. You can add
+multiple credit cards to your account, so that purchases for different products are charged to the
+correct card.
+
+If you would like to use an alternative method to pay, please
+[contact our Sales team](https://about.gitlab.com/sales/).
+
+To change your payment method:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select **My account > Payment methods**.
+1. **Edit** an existing payment method's information or **Add new payment method**.
+1. Select **Save Changes**.
+
+### Set a default payment method
+
+Automatic renewal of a subscription is charged to your default payment method. To mark a payment
+method as the default:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select **My account > Payment methods**.
+1. **Edit** the selected payment method and check the **Make default payment method** checkbox.
+1. Select **Save Changes**.
+
+## Link a GitLab.com account
+
+Follow this guideline if you have a legacy Customers Portal account and use an email and password to log in.
+
+To link a GitLab.com account to your Customers Portal account:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in?legacy=true) using email and password.
+1. On the Customers Portal page, select **My account > Account details**.
+1. Under **Your GitLab.com account**, select **Link account**.
+1. Log in to the [GitLab.com](https://gitlab.com/users/sign_in) account you want to link to the Customers Portal account.
+
+## Change the linked account
+
+Customers are required to use their GitLab.com account to register for a new Customers Portal account.
+
+If you have a legacy Customers Portal account that is not linked to a GitLab.com account, you may still [sign in](https://customers.gitlab.com/customers/sign_in?legacy=true) using an email and password. However, you should [create](https://gitlab.com/users/sign_up) and [link a GitLab.com account](#change-the-linked-account) to ensure continued access to the Customers Portal.
+
+Customers of resellers do not have access to this portal and should contact their reseller for any
+changes to their subscription.
+
+To change the GitLab.com account linked to your Customers Portal account:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. In a separate browser tab, go to [GitLab.com](https://gitlab.com/users/sign_in) and ensure you are not logged in.
+1. On the Customers Portal page, select **My account > Account details**.
+1. Under **Your GitLab.com account**, select **Change linked account**.
+1. Log in to the [GitLab.com](https://gitlab.com/users/sign_in) account you want to link to the Customers Portal account.
+
+## Change Customers Portal account password
+
+To change the password for this customers portal account:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select the **My account** dropdown list and select **Account details**.
+1. Make the required changes to the **Your password** section.
+1. Select **Save changes**.
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index cd36e426852..4d11a6fffc8 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -237,7 +237,7 @@ amounts at which the alert displays.
To change the namespace linked to a subscription:
1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) with a
- [linked](../index.md#link-a-gitlabcom-account) GitLab.com account.
+ [linked](../customers_portal.md#link-a-gitlabcom-account) GitLab.com account.
1. Go to the **Manage Purchases** page.
1. Select **Change linked namespace**.
1. Select the desired group from the **This subscription is for** dropdown list. For a group to appear here, you must have the Owner role for that group.
@@ -312,7 +312,7 @@ For details on upgrading your subscription tier, see
### Automatic subscription renewal
When a subscription is set to auto-renew, it renews automatically on the
-expiration date without a gap in available service. Subscriptions purchased through Customers Portal or GitLab.com are set to auto-renew by default. The number of seats is adjusted to fit the [number of billable users in your group](#view-seat-usage) at the time of renewal. You can view and download your renewal invoice on the Customers Portal [View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../index.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your access is downgraded.
+expiration date without a gap in available service. Subscriptions purchased through Customers Portal or GitLab.com are set to auto-renew by default. The number of seats is adjusted to fit the [number of billable users in your group](#view-seat-usage) at the time of renewal. You can view and download your renewal invoice on the Customers Portal [View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your access is downgraded.
#### Email notifications
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index f1a960a6aa9..9f329700c74 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -5,259 +5,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: index, reference
---
-# GitLab subscription **(PREMIUM)**
-
-GitLab offers tiers of features. Your subscription determines which tier you
-have access to. Subscriptions are valid for 12 months.
-
-GitLab provides special subscriptions to participants in:
-
-- [Education](#gitlab-for-education)
-- [Open Source](#gitlab-for-open-source)
-
-## Choose a GitLab subscription
-
-When choosing a subscription, there are two factors to consider:
-
-- [GitLab SaaS or GitLab self-managed](#choose-between-gitlab-saas-or-gitlab-self-managed)
-- [GitLab tier](#choose-a-gitlab-tier)
-
-### Choose between GitLab SaaS or GitLab self-managed
-
-There are some differences in how a subscription applies, depending if you use
-GitLab SaaS or GitLab self-managed:
-
-- [GitLab SaaS](gitlab_com/index.md): The GitLab software-as-a-service offering.
- You don't need to install anything to use GitLab SaaS, you only need to
- [sign up](https://gitlab.com/users/sign_up) and start using GitLab straight away.
-- [GitLab Dedicated](gitlab_dedicated/index.md): a single-tenant SaaS service for highly regulated and large enterprises.
-- [GitLab self-managed](self_managed/index.md): Install, administer, and maintain
- your own GitLab instance.
-
-On a GitLab self-managed instance, a GitLab subscription provides the same set of
-features for _all_ users. On GitLab SaaS, you can apply a subscription to a group
-namespace. You cannot apply a subscription to a personal namespace.
-
-NOTE:
-Subscriptions cannot be transferred between GitLab SaaS and GitLab self-managed.
-A new subscription must be purchased and applied as needed.
-
-### Choose a GitLab tier
-
-Pricing is [tier-based](https://about.gitlab.com/pricing/), allowing you to choose
-the features which fit your budget. For information on what features are available
-at each tier for each product, see: [GitLab self-managed feature comparison](https://about.gitlab.com/pricing/feature-comparison/)
-
-## Find your subscription
-
-The following chart should help you determine your subscription model. Select
-the list item to go to the respective help page.
-
-```mermaid
-graph TD
-
-A(Is your user account on GitLab.com?)
-A --> B(Yes)
-A --> C(No)
-B --> D(fa:fa-link View your subscription on GitLab.com)
-C --> E(fa:fa-link View your self-hosted subscription)
-
-click D "./gitlab_com/index.html#view-your-gitlabcom-subscription"
-click E "./self_managed/index.html#view-your-subscription"
-```
-
-## Customers Portal
-
-With the [Customers Portal](https://customers.gitlab.com/) you can:
-
-- [Change account owner information](#change-account-owner-information)
-- [Change your company details](#change-your-company-details)
-- [Change your payment method](#change-your-payment-method)
-- [Link a GitLab.com account](#link-a-gitlabcom-account)
-- [Change the linked account](#change-the-linked-account)
-- [Change the namespace the subscription is linked to](gitlab_com/index.md#change-the-linked-namespace)
-- [Change customers portal account password](#change-customers-portal-account-password)
-
-The Customers Portal is available only to customers who purchased their
-subscription from GitLab. If you made your purchase through a partner or
-reseller, you must contact them directly for assistance with your subscription.
-
-### Change account owner information
-
-Account owner personal details are used on invoices. The account owner email address is used for the Customers Portal legacy login and license-related email.
-
-If you have registered a Customers Portal account through a GitLab.com account, the GitLab.com account is used for login.
-
-To change account owner information, including name, billing address, and email address:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. Select **My account > Account details**.
-1. Expand the **Personal details** section.
-1. Edit the personal details.
-1. Select **Save changes**.
-
-If you want to transfer ownership of the Customers Portal account
-to another person, after you enter that person's personal details, you must also:
-
-- [Change the Customers Portal account password](#change-customers-portal-account-password).
-- [Change the linked GitLab.com account](#change-the-linked-account), if you have one linked.
-
-### Change your company details
-
-To change your company details, including company name and VAT number:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. Select **My account > Account details**.
-1. Expand the **Company details** section.
-1. Edit the company details.
-1. Select **Save changes**.
-
-### Change your payment method
-
-Purchases in the Customers Portal require a credit card on record as a payment method. You can add
-multiple credit cards to your account, so that purchases for different products are charged to the
-correct card.
-
-If you would like to use an alternative method to pay, please
-[contact our Sales team](https://about.gitlab.com/sales/).
-
-To change your payment method:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. Select **My account > Payment methods**.
-1. **Edit** an existing payment method's information or **Add new payment method**.
-1. Select **Save Changes**.
-
-#### Set a default payment method
-
-Automatic renewal of a subscription is charged to your default payment method. To mark a payment
-method as the default:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. Select **My account > Payment methods**.
-1. **Edit** the selected payment method and check the **Make default payment method** checkbox.
-1. Select **Save Changes**.
-
-### Link a GitLab.com account
-
-Follow this guideline if you have a legacy Customers Portal account and use an email and password to log in.
-
-To link a GitLab.com account to your Customers Portal account:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in?legacy=true) using email and password.
-1. On the Customers Portal page, select **My account > Account details**.
-1. Under **Your GitLab.com account**, select **Link account**.
-1. Log in to the [GitLab.com](https://gitlab.com/users/sign_in) account you want to link to the Customers Portal account.
-
-### Change the linked account
-
-To change the GitLab.com account linked to your Customers Portal account:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. In a separate browser tab, go to [GitLab.com](https://gitlab.com/users/sign_in) and ensure you are not logged in.
-1. On the Customers Portal page, select **My account > Account details**.
-1. Under **Your GitLab.com account**, select **Change linked account**.
-1. Log in to the [GitLab.com](https://gitlab.com/users/sign_in) account you want to link to the Customers Portal account.
-
-### Change Customers Portal account password
-
-To change the password for this customers portal account:
-
-1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
-1. Select the **My account** dropdown list and select **Account details**.
-1. Make the required changes to the **Your password** section.
-1. Select **Save changes**.
-
-## Community program subscriptions
-
-### GitLab for Education
-
-For qualifying non-profit educational institutions, the [GitLab for Education Program](https://about.gitlab.com/solutions/education/) provides GitLab Ultimate, plus 50,000 CI/CD minutes per month. The subscription granted under GitLab for Education can only be used for instructional use or non-commercial academic research. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Education Program page](https://about.gitlab.com/solutions/education/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/education-program/).
-
-### GitLab for Open Source
-
-For qualifying open source projects, the [GitLab for Open Source Program](https://about.gitlab.com/solutions/open-source/) provides GitLab Ultimate, plus 50,000 CI/CD minutes per month. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Open Source Program page](https://about.gitlab.com/solutions/open-source/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/opensource-program/).
-
-#### Meeting GitLab for Open Source Program requirements
-
-To meet GitLab for Open Source Program requirements, first add an OSI-approved open source license to all projects in your namespace.
-
-To add a license to a project:
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. On the overview page, select **Add LICENSE**. If the license you want is not available as a license template, manually copy the entire, unaltered [text of your chosen license](https://opensource.org/licenses/) into the `LICENSE` file. Note that GitLab defaults to **All rights reserved** if users do not perform this action.
-
-![Add license](img/add-license.png)
-
-Applicants must add the correct license to each project in their respective groups or namespaces. When you're sure you're using OSI-approved licenses for your projects, you can take your screenshots.
-
-NOTE:
-GitLab for Open Source Program benefits apply to an entire GitLab namespace. To qualify for the GitLab for Open Source Program, all projects in an applicant's namespace must meet program requirements. Applicants submit materials related to one project in the applying namespace, and the open source program team uses that project to verify eligibility of the entire namespace.
-
-#### Verification for Open Source Program
-
-Next, take screenshots of your project to confirm that project's eligibility. You must upload three screenshots:
-
-- [OSI-approved license overview](#screenshot-1-license-overview)
-- [OSI-approved license contents](#screenshot-2-license-contents)
-- [Publicly visible settings](#screenshot-3-publicly-visible-settings)
-
-NOTE:
-Benefits of the GitLab Open Source Program apply to all projects in a GitLab namespace. All projects in an eligible namespace must meet program requirements.
-
-##### Screenshot 1: License overview
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. On the left sidebar, select your project avatar. If you haven't specified an avatar for your project, the avatar displays as a single letter.
-1. Take a screenshot of the project overview that clearly displays the license you've chosen for your project.
-
-![License overview](img/license-overview.png)
-
-##### Screenshot 2: License contents
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. On the left sidebar, select **Repository** and locate the project's `LICENSE` file.
-1. Take a screenshot of the contents of the file. Make sure the screenshot includes the title of the license.
-
-![License file](img/license-file.png)
-
-##### Screenshot 3: Publicly visible settings
-
-To be eligible for the GitLab Open Source Program, projects must be publicly visible. To check your project's public visibility settings:
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. From the left sidebar, select **Settings > General**.
-1. Expand **Visibility, project features, permissions**.
-1. From the **Project visibility** dropdown list, select **Public**.
-1. Select the **Users can request access** checkbox.
-1. Take a screenshot of this view. Include as much of the publicly visible settings as possible. Make sure to include your project's name in the upper-left of the screenshot.
-
-![Publicly visible setting](img/publicly-visible.png)
-
-NOTE:
-Exceptions to this public visibility requirement apply in select circumstances (for example, in cases where a project in an applicant's namespace may hold sensitive data). Email `opensource@gitlab.com` with details of your use case to request written permission for exceptions.
-
-### GitLab for Startups
-
-For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides GitLab Ultimate, plus 50,000 CI/CD minutes per month for 12 months. For more information—including instructions for applying to the program and renewing program membership—see the [GitLab for Startups Program page](https://about.gitlab.com/solutions/startups/) and the [GitLab handbook](https://about.gitlab.com/handbook/marketing/community-relations/community-programs/startups-program/).
-
-## Contact Support
-
-- See the tiers of [GitLab Support](https://about.gitlab.com/support/).
-- [Submit a request](https://support.gitlab.com/hc/en-us/requests/new) through the Support Portal.
-
-We also encourage all users to search our project trackers for known issues and existing feature requests in the [GitLab project](https://gitlab.com/gitlab-org/gitlab/-/issues/).
-
-These issues are the best avenue for getting updates on specific product plans and for communicating directly with the relevant GitLab team members.
-
-<!-- ## Troubleshooting
-
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
-
-Each scenario can be a third-level heading, for example `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+# Subscribe to GitLab **(PREMIUM)**
+
+Choose and manage the subscription that's right for you and your organization.
+
+- [Choose a subscription](choosing_subscription.md)
+- [Compare self-managed to SaaS](../install/migrate/compare_sm_to_saas.md)
+- [GitLab SaaS](gitlab_com/index.md)
+- [GitLab self-managed](self_managed/index.md)
+- [GitLab Dedicated](gitlab_dedicated/index.md)
+- [Community programs](community_programs.md)
+- [Customers Portal](customers_portal.md)
+- [Quarterly reconciliation](quarterly_reconciliation.md)
+- [Storage usage quota](../user/usage_quotas.md)
+- [CI/CD minutes quota](../ci/pipelines/cicd_minutes.md)
+- [Features available to Starter and Bronze subscribers](../subscriptions/bronze_starter.md)
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index 0eb6a2fe495..b5faf3bb0f3 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -7,39 +7,22 @@ type: index, reference
# GitLab self-managed subscription **(PREMIUM SELF)**
-You can install, administer, and maintain your own GitLab instance.
+After you subscribe to GitLab, you can manage the details of your self-managed subscription.
-This page covers the details of your GitLab self-managed subscription.
+## Obtain a self-managed subscription
-GitLab subscription management requires access to the Customers Portal.
+To subscribe to GitLab for a GitLab self-managed installation:
-## Customers Portal
-
-GitLab provides the [Customers Portal](../index.md#customers-portal) where you can
-manage your subscriptions and your account details.
-
-Customers are required to use their GitLab.com account to register for a new Customers Portal account.
-
-If you have a legacy Customers Portal account that is not linked to a GitLab.com account, you may still [sign in](https://customers.gitlab.com/customers/sign_in?legacy=true) using an email and password. However, you should [create](https://gitlab.com/users/sign_up) and [link a GitLab.com account](../index.md#change-the-linked-account) to ensure continued access to the Customers Portal.
-
-Customers of resellers do not have access to this portal and should contact their reseller for any
-changes to their subscription.
-
-## Subscription
-
-The cost of a GitLab self-managed subscription is determined by the following:
-
-- [GitLab tier](https://about.gitlab.com/pricing/)
-- [Subscription seats](#subscription-seats)
-
-## Choose a GitLab tier
+1. Go to the [Customers Portal](https://customers.gitlab.com/) and purchase a GitLab self-managed plan.
+1. After purchase, an activation code is sent to the email address associated with the Customers Portal account.
+ You must [add this code to your GitLab instance](../../user/admin_area/license.md).
-Pricing is [tier-based](https://about.gitlab.com/pricing/), so you can choose
-the features that fit your budget. For information on the features available
-for each tier, see the
-[GitLab self-managed feature comparison](https://about.gitlab.com/pricing/feature-comparison/).
+NOTE:
+If you're purchasing a subscription for an existing **Free** GitLab self-managed
+instance, ensure you're purchasing enough seats to
+[cover your users](../../user/admin_area/index.md#administering-users).
-## Subscription seats
+### Subscription seats
A GitLab self-managed subscription uses a hybrid model. You pay for a subscription
according to the [maximum number](#maximum-users) of users enabled during the subscription period.
@@ -49,7 +32,7 @@ simultaneous users in the GitLab self-managed installation is checked each quart
If an instance is unable to generate a quarterly usage report, the existing [true up model](#users-over-subscription) is used.
Prorated charges are not possible without a quarterly usage report.
-### View user totals
+## View user totals
You can view users for your license and determine if you've gone over your subscription.
@@ -58,7 +41,7 @@ You can view users for your license and determine if you've gone over your subsc
The lists of users are displayed.
-#### Billable users
+### Billable users
A _billable user_ counts against the number of subscription seats. Every user is considered a
billable user, with the following exceptions:
@@ -80,11 +63,11 @@ billable user, with the following exceptions:
**Billable users** as reported in the `/admin` section is updated once per day.
-#### Maximum users
+### Maximum users
The number of _maximum users_ reflects the highest number of billable users for the current license period.
-#### Users over subscription
+### Users over subscription
The number of _users over subscription_ shows how many users are in excess of the number allowed by the subscription. This number reflects the current subscription period.
@@ -103,7 +86,7 @@ If you add more users to your GitLab instance than you are licensed for, payment
If you do not add these users during the renewal process, your license key will not work.
-#### Free Guest users **(ULTIMATE)**
+### Free Guest users **(ULTIMATE)**
In the **Ultimate** tier, users who are assigned the Guest role do not consume a seat.
The user must not be assigned any other role, anywhere in the instance.
@@ -119,7 +102,7 @@ If a user creates a project, they are assigned the Maintainer or Owner role.
To prevent a user from creating projects, as an administrator, you can mark the user
as [external](../../user/admin_area/external_users.md).
-### Tips for managing users and subscription seats
+## Tips for managing users and subscription seats
Managing the number of users against the number of subscription seats can be a challenge:
@@ -239,19 +222,6 @@ You can manually synchronize your subscription details at any time.
A job is queued. When the job finishes, the subscription details are updated.
-## Obtain a subscription
-
-To subscribe to GitLab through a GitLab self-managed installation:
-
-1. Go to the [Customers Portal](https://customers.gitlab.com/) and purchase a GitLab self-managed plan.
-1. After purchase, an activation code is sent to the email address associated with the Customers Portal account.
- You must [add this code to your GitLab instance](../../user/admin_area/license.md).
-
-NOTE:
-If you're purchasing a subscription for an existing **Free** GitLab self-managed
-instance, ensure you're purchasing enough seats to
-[cover your users](../../user/admin_area/index.md#administering-users).
-
## View your subscription
If you are an administrator, you can view the status of your subscription:
@@ -406,7 +376,7 @@ Before auto-renewal you should [prepare for the renewal](#prepare-for-renewal-by
you must have enabled the [synchronization of subscription data](#subscription-data-synchronization).
You can view and download your renewal invoice on the Customers Portal
-[View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../index.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your GitLab tier is downgraded.
+[View invoices](https://customers.gitlab.com/receipts) page. If your account has a [saved credit card](../customers_portal.md#change-your-payment-method), the card is charged for the invoice amount. If we are unable to process a payment or the auto-renewal fails for any other reason, you have 14 days to renew your subscription, after which your GitLab tier is downgraded.
#### Email notifications
diff --git a/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb
index f05aa26a449..51a72a80268 100644
--- a/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb
@@ -17,11 +17,7 @@ module Gitlab
def execute
user_finder = GithubImport::UserFinder.new(project, client)
- gitlab_user_id = begin
- user_finder.user_id_for(pull_request.merged_by)
- rescue ::Octokit::NotFound
- nil
- end
+ gitlab_user_id = user_finder.user_id_for(pull_request.merged_by)
metrics_upsert(gitlab_user_id)
diff --git a/lib/gitlab/github_import/importer/pull_request_review_importer.rb b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
index b1e259fe940..a711f83ce92 100644
--- a/lib/gitlab/github_import/importer/pull_request_review_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
@@ -17,11 +17,7 @@ module Gitlab
def execute
user_finder = GithubImport::UserFinder.new(project, client)
- gitlab_user_id = begin
- user_finder.user_id_for(review.author)
- rescue ::Octokit::NotFound
- nil
- end
+ gitlab_user_id = user_finder.user_id_for(review.author)
if gitlab_user_id
add_review_note!(gitlab_user_id)
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index b8751def08f..dd71edbd205 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -28,6 +28,9 @@ module Gitlab
EMAIL_FOR_USERNAME_CACHE_KEY =
'github-import/user-finder/email-for-username/%s'
+ # The base cache key to use for caching inexistence of GitHub usernames.
+ INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY = 'github-import/user-finder/inexistence-of-username/%s'
+
# project - An instance of `Project`
# client - An instance of `Gitlab::GithubImport::Client`
def initialize(project, client)
@@ -113,12 +116,15 @@ module Gitlab
cache_key = EMAIL_FOR_USERNAME_CACHE_KEY % username
email = Gitlab::Cache::Import::Caching.read(cache_key)
- unless email
+ if email.blank? && !github_username_inexists?(username)
user = client.user(username)
email = Gitlab::Cache::Import::Caching.write(cache_key, user[:email], timeout: timeout(user[:email])) if user
end
email
+ rescue ::Octokit::NotFound
+ cache_github_username_inexistence(username)
+ nil
end
def cached_id_for_github_id(id)
@@ -190,6 +196,18 @@ module Gitlab
Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT
end
end
+
+ def github_username_inexists?(username)
+ cache_key = INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % username
+
+ Gitlab::Cache::Import::Caching.read(cache_key) == 'true'
+ end
+
+ def cache_github_username_inexistence(username)
+ cache_key = INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % username
+
+ Gitlab::Cache::Import::Caching.write(cache_key, true)
+ end
end
end
end
diff --git a/lib/gitlab/harbor/client.rb b/lib/gitlab/harbor/client.rb
index ee40725ba95..380e4e42bc7 100644
--- a/lib/gitlab/harbor/client.rb
+++ b/lib/gitlab/harbor/client.rb
@@ -14,9 +14,9 @@ module Gitlab
@integration = integration
end
- def ping
- options = { headers: headers.merge!('Accept': 'text/plain') }
- response = Gitlab::HTTP.get(url('ping'), options)
+ def check_project_availability
+ options = { headers: headers.merge!('Accept': 'application/json') }
+ response = Gitlab::HTTP.head(url("projects?project_name=#{integration.project_name}"), options)
{ success: response.success? }
end
diff --git a/lib/sidebars/admin/base_menu.rb b/lib/sidebars/admin/base_menu.rb
new file mode 100644
index 00000000000..897a193f672
--- /dev/null
+++ b/lib/sidebars/admin/base_menu.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ class BaseMenu < ::Sidebars::Menu
+ override :render?
+ def render?
+ return false unless context.current_user
+
+ context.current_user.can_admin_all_resources?
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/abuse_reports_menu.rb b/lib/sidebars/admin/menus/abuse_reports_menu.rb
new file mode 100644
index 00000000000..72f4d6e6590
--- /dev/null
+++ b/lib/sidebars/admin/menus/abuse_reports_menu.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class AbuseReportsMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_abuse_reports_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Abuse Reports')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'slight-frown'
+ end
+
+ override :has_pill?
+ def has_pill?
+ true
+ end
+
+ override :pill_count
+ def pill_count
+ @pill_count ||= AbuseReport.count(:all)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :abuse_reports }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/admin_overview_menu.rb b/lib/sidebars/admin/menus/admin_overview_menu.rb
new file mode 100644
index 00000000000..57c9ff4dcb0
--- /dev/null
+++ b/lib/sidebars/admin/menus/admin_overview_menu.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class AdminOverviewMenu < ::Sidebars::Admin::BaseMenu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(dashboard_menu_item)
+ add_item(projects_menu_item)
+ add_item(users_menu_item)
+ add_item(groups_menu_item)
+ add_item(topics_menu_item)
+ add_item(gitaly_servers_menu_item)
+
+ true
+ end
+
+ override :title
+ def title
+ s_('Admin|Overview')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'overview'
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ { 'data-qa-selector': 'admin_overview_submenu_content' }
+ end
+
+ private
+
+ def dashboard_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Dashboard'),
+ link: admin_root_path,
+ active_routes: { controller: 'dashboard' },
+ item_id: :dashboard
+ )
+ end
+
+ def projects_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Projects'),
+ link: admin_projects_path,
+ active_routes: { controller: 'admin/projects' },
+ item_id: :projects
+ )
+ end
+
+ def users_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Users'),
+ link: admin_users_path,
+ active_routes: { controller: 'users' },
+ item_id: :users,
+ container_html_options: { 'data-qa-selector': 'admin_overview_users_link' }
+ )
+ end
+
+ def groups_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Groups'),
+ link: admin_groups_path,
+ active_routes: { controller: 'groups' },
+ item_id: :groups,
+ container_html_options: { 'data-qa-selector': 'admin_overview_groups_link' }
+ )
+ end
+
+ def topics_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Topics'),
+ link: admin_topics_path,
+ active_routes: { controller: 'admin/topics' },
+ item_id: :topics
+ )
+ end
+
+ def gitaly_servers_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Gitaly Servers'),
+ link: admin_gitaly_servers_path,
+ active_routes: { controller: 'gitaly_servers' },
+ item_id: :gitaly_servers
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/admin_settings_menu.rb b/lib/sidebars/admin/menus/admin_settings_menu.rb
new file mode 100644
index 00000000000..163c32ad0a9
--- /dev/null
+++ b/lib/sidebars/admin/menus/admin_settings_menu.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class AdminSettingsMenu < ::Sidebars::Admin::BaseMenu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(general_settings_menu_item)
+ add_item(integrations_menu_item)
+ add_item(repository_menu_item)
+ add_item(ci_cd_menu_item)
+ add_item(reporting_menu_item)
+ add_item(metrics_and_profiling_menu_item)
+ add_item(service_usage_data_menu_item)
+ add_item(network_settings_menu_item)
+ add_item(appearance_menu_item)
+ add_item(preferences_menu_item)
+
+ true
+ end
+
+ override :title
+ def title
+ s_('Admin|Settings')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'settings'
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ { 'data-qa-selector': 'admin_settings_menu_link' }
+ end
+
+ private
+
+ def general_settings_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('General'),
+ link: general_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#general' },
+ item_id: :general_settings,
+ container_html_options: { 'data-qa-selector': 'admin_settings_general_link' }
+ )
+ end
+
+ def integrations_menu_item
+ return ::Sidebars::NilMenuItem.new(item_id: :admin_integrations) unless instance_level_integrations?
+
+ ::Sidebars::MenuItem.new(
+ title: _('Integrations'),
+ link: integrations_admin_application_settings_path,
+ active_routes: { path: %w[application_settings#integrations integrations#edit] },
+ item_id: :admin_integrations,
+ container_html_options: { 'data-qa-selector': 'admin_settings_integrations_link' }
+ )
+ end
+
+ def repository_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Repository'),
+ link: repository_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#repository' },
+ item_id: :admin_repository,
+ container_html_options: { 'data-qa-selector': 'admin_settings_repository_link' }
+ )
+ end
+
+ def ci_cd_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('CI/CD'),
+ link: ci_cd_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#ci_cd' },
+ item_id: :admin_ci_cd
+ )
+ end
+
+ def reporting_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Reporting'),
+ link: reporting_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#reporting' },
+ item_id: :admin_reporting
+ )
+ end
+
+ def metrics_and_profiling_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Metrics and profiling'),
+ link: metrics_and_profiling_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#metrics_and_profiling' },
+ item_id: :admin_metrics,
+ container_html_options: { 'data-qa-selector': 'admin_settings_metrics_and_profiling_link' }
+ )
+ end
+
+ def service_usage_data_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Service usage data'),
+ link: service_usage_data_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#service_usage_data' },
+ item_id: :admin_service_usage
+ )
+ end
+
+ def network_settings_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Network'),
+ link: network_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#network' },
+ item_id: :admin_network,
+ container_html_options: { 'data-qa-selector': 'admin_settings_network_link' }
+ )
+ end
+
+ def appearance_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Appearance'),
+ link: admin_application_settings_appearances_path,
+ active_routes: { path: 'admin/application_settings/appearances#show' },
+ item_id: :admin_appearance
+ )
+ end
+
+ def preferences_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Preferences'),
+ link: preferences_admin_application_settings_path,
+ active_routes: { path: 'admin/application_settings#preferences' },
+ item_id: :admin_preferences,
+ container_html_options: { 'data-qa-selector': 'admin_settings_preferences_link' }
+ )
+ end
+
+ def instance_level_integrations?
+ !Gitlab.com?
+ end
+ end
+ end
+ end
+end
+
+Sidebars::Admin::Menus::AdminSettingsMenu.prepend_mod_with('Sidebars::Admin::Menus::AdminSettingsMenu')
diff --git a/lib/sidebars/admin/menus/analytics_menu.rb b/lib/sidebars/admin/menus/analytics_menu.rb
new file mode 100644
index 00000000000..944f7f6bba7
--- /dev/null
+++ b/lib/sidebars/admin/menus/analytics_menu.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class AnalyticsMenu < ::Sidebars::Admin::BaseMenu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(dev_ops_reports_menu_item)
+ add_item(usage_trends_menu_item)
+
+ true
+ end
+
+ override :title
+ def title
+ s_('Admin|Analytics')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'chart'
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ { 'data-qa-selector': 'admin_sidebar_analytics_submenu_content' }
+ end
+
+ private
+
+ def dev_ops_reports_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('DevOps Reports'),
+ link: admin_dev_ops_reports_path,
+ active_routes: { controller: 'dev_ops_report' },
+ item_id: :dev_ops_reports,
+ container_html_options: { 'data-qa-selector': 'admin_analytics_link' }
+ )
+ end
+
+ def usage_trends_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Usage Trends'),
+ link: admin_usage_trends_path,
+ active_routes: { controller: 'usage_trends' },
+ item_id: :usage_trends
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/applications_menu.rb b/lib/sidebars/admin/menus/applications_menu.rb
new file mode 100644
index 00000000000..74116076735
--- /dev/null
+++ b/lib/sidebars/admin/menus/applications_menu.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class ApplicationsMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_applications_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Applications')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'applications'
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :applications }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/ci_cd_menu.rb b/lib/sidebars/admin/menus/ci_cd_menu.rb
new file mode 100644
index 00000000000..e6e8e77a448
--- /dev/null
+++ b/lib/sidebars/admin/menus/ci_cd_menu.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class CiCdMenu < ::Sidebars::Admin::BaseMenu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(runners_menu_item)
+ add_item(jobs_menu_item)
+
+ true
+ end
+
+ override :title
+ def title
+ s_('Admin|CI/CD')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'rocket'
+ end
+
+ private
+
+ def runners_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Runners'),
+ link: admin_runners_path,
+ active_routes: { controller: 'runners' },
+ item_id: :runners
+ )
+ end
+
+ def jobs_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Jobs'),
+ link: admin_jobs_path,
+ active_routes: { controller: 'jobs' },
+ item_id: :jobs
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/deploy_keys_menu.rb b/lib/sidebars/admin/menus/deploy_keys_menu.rb
new file mode 100644
index 00000000000..4ffc6635f27
--- /dev/null
+++ b/lib/sidebars/admin/menus/deploy_keys_menu.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class DeployKeysMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_deploy_keys_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Deploy Keys')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'key'
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :deploy_keys }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/kubernetes_menu.rb b/lib/sidebars/admin/menus/kubernetes_menu.rb
new file mode 100644
index 00000000000..88b184290f1
--- /dev/null
+++ b/lib/sidebars/admin/menus/kubernetes_menu.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class KubernetesMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_clusters_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Kubernetes')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'cloud-gear'
+ end
+
+ override :render?
+ def render?
+ current_user && current_user.can_admin_all_resources? && instance_clusters_enabled?
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :clusters }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/labels_menu.rb b/lib/sidebars/admin/menus/labels_menu.rb
new file mode 100644
index 00000000000..32b4b53960a
--- /dev/null
+++ b/lib/sidebars/admin/menus/labels_menu.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class LabelsMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_labels_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Labels')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'labels'
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :labels }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/messages_menu.rb b/lib/sidebars/admin/menus/messages_menu.rb
new file mode 100644
index 00000000000..0d7110f42bf
--- /dev/null
+++ b/lib/sidebars/admin/menus/messages_menu.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class MessagesMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_broadcast_messages_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Messages')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'messages'
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :broadcast_messages }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/monitoring_menu.rb b/lib/sidebars/admin/menus/monitoring_menu.rb
new file mode 100644
index 00000000000..71a9d4b8a03
--- /dev/null
+++ b/lib/sidebars/admin/menus/monitoring_menu.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class MonitoringMenu < ::Sidebars::Admin::BaseMenu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(system_info_menu_item)
+ add_item(background_migrations_menu_item)
+ add_item(background_jobs_menu_item)
+ add_item(health_check_menu_item)
+ true
+ end
+
+ override :title
+ def title
+ s_('Admin|Monitoring')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'monitor'
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ { 'data-qa-selector': 'admin_monitoring_menu_link' }
+ end
+
+ private
+
+ def system_info_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('System Info'),
+ link: admin_system_info_path,
+ active_routes: { controller: 'system_info' },
+ item_id: :system_info
+ )
+ end
+
+ def background_migrations_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Background Migrations'),
+ link: admin_background_migrations_path,
+ active_routes: { controller: 'background_migrations' },
+ item_id: :usage_trends
+ )
+ end
+
+ def background_jobs_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Background Jobs'),
+ link: admin_background_jobs_path,
+ active_routes: { controller: 'background_jobs' },
+ item_id: :background_jobs
+ )
+ end
+
+ def health_check_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Health Check'),
+ link: admin_health_check_path,
+ active_routes: { controller: 'health_check' },
+ item_id: :health_check
+ )
+ end
+ end
+ end
+ end
+end
+
+Sidebars::Admin::Menus::MonitoringMenu.prepend_mod_with('Sidebars::Admin::Menus::MonitoringMenu')
diff --git a/lib/sidebars/admin/menus/spam_logs_menu.rb b/lib/sidebars/admin/menus/spam_logs_menu.rb
new file mode 100644
index 00000000000..d01cd636e13
--- /dev/null
+++ b/lib/sidebars/admin/menus/spam_logs_menu.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class SpamLogsMenu < ::Sidebars::Menu
+ override :link
+ def link
+ admin_spam_logs_path
+ end
+
+ override :title
+ def title
+ s_('Admin|Spam Logs')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'spam'
+ end
+
+ override :render?
+ def render?
+ current_user && current_user.can_admin_all_resources? && anti_spam_service_enabled?
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :spam_logs }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/menus/system_hooks_menu.rb b/lib/sidebars/admin/menus/system_hooks_menu.rb
new file mode 100644
index 00000000000..494b0392400
--- /dev/null
+++ b/lib/sidebars/admin/menus/system_hooks_menu.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ module Menus
+ class SystemHooksMenu < ::Sidebars::Admin::BaseMenu
+ override :link
+ def link
+ admin_hooks_path
+ end
+
+ override :title
+ def title
+ s_('Admin|System Hooks')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'hook'
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :hooks }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/admin/panel.rb b/lib/sidebars/admin/panel.rb
new file mode 100644
index 00000000000..95a5c183e96
--- /dev/null
+++ b/lib/sidebars/admin/panel.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Admin
+ class Panel < ::Sidebars::Panel
+ override :configure_menus
+ def configure_menus
+ super
+ add_menus
+ end
+
+ override :render_raw_scope_menu_partial
+ def render_raw_scope_menu_partial
+ "shared/nav/admin_scope_header"
+ end
+
+ override :aria_label
+ def aria_label
+ s_("Admin|Admin Area")
+ end
+
+ override :super_sidebar_context_header
+ def super_sidebar_context_header
+ @super_sidebar_context_header ||= {
+ title: aria_label
+ }
+ end
+
+ def add_menus
+ add_menu(Sidebars::Admin::Menus::AdminOverviewMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::CiCdMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::AnalyticsMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::MonitoringMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::MessagesMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::SystemHooksMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::ApplicationsMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::AbuseReportsMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::KubernetesMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::SpamLogsMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::DeployKeysMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::LabelsMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::AdminSettingsMenu.new(context))
+ end
+ end
+ end
+end
+
+Sidebars::Admin::Panel.prepend_mod_with('Sidebars::Admin::Panel')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e8e6a120c77..fd6492a33c3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1857,6 +1857,9 @@ msgstr ""
msgid "AI|Explain the code from %{filePath} in human understandable language presented in Markdown format. In the response add neither original code snippet nor any title. `%{text}`"
msgstr ""
+msgid "AI|Something went wrong. Please try again later"
+msgstr ""
+
msgid "AI|The container element wasn't found, stopping AI Genie."
msgstr ""
@@ -2670,9 +2673,6 @@ msgstr ""
msgid "Admin Notifications"
msgstr ""
-msgid "Admin Overview"
-msgstr ""
-
msgid "Admin Section"
msgstr ""
@@ -2685,9 +2685,6 @@ msgstr ""
msgid "Admin mode enabled"
msgstr ""
-msgid "Admin navigation"
-msgstr ""
-
msgid "Admin notes"
msgstr ""
@@ -3711,21 +3708,75 @@ msgstr ""
msgid "Administrators are not permitted to connect applications with these scopes: %{code_open}api%{code_close}, %{code_open}read_api%{code_close}, %{code_open}read_repository%{code_close}, %{code_open}write_repository%{code_close}, %{code_open}write_registry%{code_close}, %{code_open}read_registry%{code_close}, and %{code_open}sudo%{code_close}. To permit this, change the %{code_open}disable_admin_oauth_scopes%{code_close} setting using the API."
msgstr ""
+msgid "Admin|Abuse Reports"
+msgstr ""
+
msgid "Admin|Additional users must be reviewed and approved by a system administrator. Learn more about %{help_link_start}usage caps%{help_link_end}."
msgstr ""
+msgid "Admin|Admin Area"
+msgstr ""
+
msgid "Admin|Admin notes"
msgstr ""
+msgid "Admin|Analytics"
+msgstr ""
+
+msgid "Admin|Applications"
+msgstr ""
+
+msgid "Admin|CI/CD"
+msgstr ""
+
+msgid "Admin|Credentials"
+msgstr ""
+
+msgid "Admin|Deploy Keys"
+msgstr ""
+
+msgid "Admin|Geo"
+msgstr ""
+
+msgid "Admin|Kubernetes"
+msgstr ""
+
+msgid "Admin|Labels"
+msgstr ""
+
msgid "Admin|Learn more about quarterly reconciliation"
msgstr ""
+msgid "Admin|Messages"
+msgstr ""
+
+msgid "Admin|Monitoring"
+msgstr ""
+
msgid "Admin|Note"
msgstr ""
+msgid "Admin|Overview"
+msgstr ""
+
+msgid "Admin|Push Rules"
+msgstr ""
+
msgid "Admin|Quarterly reconciliation will occur on %{qrtlyDate}"
msgstr ""
+msgid "Admin|Settings"
+msgstr ""
+
+msgid "Admin|Spam Logs"
+msgstr ""
+
+msgid "Admin|Subscription"
+msgstr ""
+
+msgid "Admin|System Hooks"
+msgstr ""
+
msgid "Admin|The number of max seats in your namespace exceeds the number of seats in your subscription. On %{qrtlyDate}, quarterly reconciliation occurs and you are automatically billed a prorated amount for the overage. No action is needed from you. If you have a credit card on file, it will be charged. Otherwise, you will receive an invoice. For more information about the timing of the invoicing process, view the documentation."
msgstr ""
@@ -20637,6 +20688,9 @@ msgstr ""
msgid "GroupSAML|SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
msgstr ""
+msgid "GroupSAML|Some todos may be hidden because your SAML session has expired. Click to reauthenticate with the following groups to view hidden todos:"
+msgstr ""
+
msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to %{linkStart}reset it%{linkEnd}."
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 3c1dd90f8f3..ef66b1327eb 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
-gem 'gitlab-qa', '~> 10', '>= 10.1.0', require: 'gitlab/qa'
+gem 'gitlab-qa', '~> 10', '>= 10.2.0', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.20.0'
gem 'capybara', '~> 3.39.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 9cdf54e0920..1cd898903b8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -102,7 +102,7 @@ GEM
gitlab (4.18.0)
httparty (~> 0.18)
terminal-table (>= 1.5.1)
- gitlab-qa (10.1.0)
+ gitlab-qa (10.2.0)
activesupport (~> 6.1)
gitlab (~> 4.18.0)
http (~> 5.0)
@@ -318,7 +318,7 @@ DEPENDENCIES
faraday-retry (~> 2.1)
fog-core (= 2.1.0)
fog-google (~> 1.19)
- gitlab-qa (~> 10, >= 10.1.0)
+ gitlab-qa (~> 10, >= 10.2.0)
influxdb-client (~> 2.9)
knapsack (~> 4.0)
nokogiri (~> 1.14, >= 1.14.3)
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index 42dd1083bbe..dab1b93a841 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -4,81 +4,60 @@ module QA
module Page
module Admin
class Menu < Page::Base
- view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
- element :admin_sidebar_content
- element :admin_monitoring_menu_link
- element :admin_monitoring_submenu_content
+ view 'lib/sidebars/admin/menus/admin_overview_menu.rb' do
element :admin_overview_submenu_content
- element :admin_overview_users_link
- element :admin_overview_groups_link
- element :admin_settings_menu_link
- element :admin_settings_submenu_content
- element :admin_settings_general_link
- element :admin_settings_integrations_link
- element :admin_settings_metrics_and_profiling_link
- element :admin_settings_network_link
- element :admin_settings_preferences_link
- element :admin_settings_repository_link
+ end
+
+ view 'lib/sidebars/admin/menus/analytics_menu.rb' do
+ element :admin_sidebar_analytics_submenu_content
+ end
+
+ view 'lib/sidebars/admin/menus/monitoring_menu.rb' do
+ element :admin_monitoring_menu_link
end
def go_to_preferences_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_preferences_link
- end
+ click_element :admin_settings_preferences_link
end
end
def go_to_repository_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_repository_link
- end
+ click_element :admin_settings_repository_link
end
end
def go_to_integration_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_integrations_link
- end
+ click_element :admin_settings_integrations_link
end
end
def go_to_general_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_general_link
- end
+ click_element :admin_settings_general_link
end
end
def go_to_metrics_and_profiling_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_metrics_and_profiling_link
- end
+ click_element :admin_settings_metrics_and_profiling_link
end
end
def go_to_network_settings
hover_element(:admin_settings_menu_link) do
- within_submenu(:admin_settings_submenu_content) do
- click_element :admin_settings_network_link
- end
+ click_element :admin_settings_network_link
end
end
def go_to_users_overview
- within_submenu(:admin_overview_submenu_content) do
- click_element :admin_overview_users_link
- end
+ click_element :admin_overview_users_link
end
def go_to_groups_overview
- within_submenu(:admin_overview_submenu_content) do
- click_element :admin_overview_groups_link
- end
+ click_element :admin_overview_groups_link
end
private
@@ -93,7 +72,7 @@ module QA
end
def within_sidebar(&block)
- within_element(:admin_sidebar_content, &block)
+ page.within('.sidebar-top-level-items', &block)
end
def within_submenu(element, &block)
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index ac6715bacd5..056df213209 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -181,6 +181,7 @@ RSpec.describe HelpController do
context 'when requested file exists' do
before do
stub_doc_file_read(file_name: 'user/ssh.md', content: fixture_file('blockquote_fence_after.md'))
+ stub_application_setting(help_page_documentation_base_url: '')
subject
end
@@ -223,13 +224,13 @@ RSpec.describe HelpController do
context 'when gitlab_docs is disabled' do
let(:docs_enabled) { false }
- it_behaves_like 'documentation pages local render'
+ it_behaves_like 'documentation pages redirect', 'https://docs.gitlab.com'
end
context 'when host is missing' do
let(:host) { nil }
- it_behaves_like 'documentation pages local render'
+ it_behaves_like 'documentation pages redirect', 'https://docs.gitlab.com'
end
end
@@ -251,6 +252,10 @@ RSpec.describe HelpController do
end
context 'when requested file is missing' do
+ before do
+ stub_application_setting(help_page_documentation_base_url: '')
+ end
+
it 'renders not found' do
get :show, params: { path: 'foo/bar' }, format: :md
expect(response).to be_not_found
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 10de7bc3b5b..368623b9aff 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -72,6 +72,10 @@ FactoryBot.define do
user_type { :security_bot }
end
+ trait :llm_bot do
+ user_type { :llm_bot }
+ end
+
trait :external do
external { true }
end
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index d99d8986296..ce6861ff460 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -6,7 +6,7 @@ import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
-import { updateInstallation } from '~/jira_connect/subscriptions/api';
+import { updateInstallation, setApiBaseURL } from '~/jira_connect/subscriptions/api';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
@@ -72,6 +72,10 @@ describe('SignInGitlabMultiversion', () => {
expect(findSetupInstructions().exists()).toBe(true);
});
+ it('calls setApiBaseURL with correct params', () => {
+ expect(setApiBaseURL).toHaveBeenCalledWith(mockBasePath);
+ });
+
describe('when SetupInstructions emits `next` event', () => {
beforeEach(async () => {
findSetupInstructions().vm.$emit('next');
@@ -102,6 +106,10 @@ describe('SignInGitlabMultiversion', () => {
expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH);
});
+ it('does not call setApiBaseURL', () => {
+ expect(setApiBaseURL).not.toHaveBeenCalled();
+ });
+
describe('when button emits `sign-in` event', () => {
it('emits `sign-in-oauth` event', () => {
const button = findSignInOauthButton();
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 9ca170d34e2..f8dd6f6df27 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -467,3 +467,78 @@ export const SMALL_MOCK_AGGREGATIONS = [
buckets: TEST_RAW_BUCKETS,
},
];
+
+export const MOCK_NAVIGATION_ITEMS = [
+ {
+ title: 'Projects',
+ icon: 'project',
+ link: '/search?scope=projects&search=et',
+ is_active: false,
+ pill_count: '10K+',
+ items: [],
+ },
+ {
+ title: 'Code',
+ icon: 'code',
+ link: '/search?scope=blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Issues',
+ icon: 'issues',
+ link: '/search?scope=issues&search=et',
+ is_active: true,
+ pill_count: '2.4K',
+ items: [],
+ },
+ {
+ title: 'Merge requests',
+ icon: 'merge-request',
+ link: '/search?scope=merge_requests&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Wiki',
+ icon: 'overview',
+ link: '/search?scope=wiki_blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Commits',
+ icon: 'commit',
+ link: '/search?scope=commits&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Comments',
+ icon: 'comments',
+ link: '/search?scope=notes&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Milestones',
+ icon: 'tag',
+ link: '/search?scope=milestones&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Users',
+ icon: 'users',
+ link: '/search?scope=users&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+];
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 0ef0922c4b0..e3b8e7575a4 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -10,6 +10,7 @@ import {
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
TEST_FILTER_DATA,
MOCK_NAVIGATION,
+ MOCK_NAVIGATION_ITEMS,
} from '../mock_data';
describe('Global Search Store Getters', () => {
@@ -68,4 +69,11 @@ describe('Global Search Store Getters', () => {
expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result);
});
});
+
+ describe('navigationItems', () => {
+ it('returns the re-mapped navigation data', () => {
+ state.navigation = MOCK_NAVIGATION;
+ expect(getters.navigationItems(state)).toStrictEqual(MOCK_NAVIGATION_ITEMS);
+ });
+ });
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index dfe4e801f11..802c5219799 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -8,6 +8,7 @@ import {
formatSearchResultCount,
getAggregationsUrl,
prepareSearchAggregations,
+ addCountOverLimit,
} from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
@@ -288,4 +289,18 @@ describe('Global Search Store Utils', () => {
expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result);
});
});
+
+ describe('addCountOverLimit', () => {
+ it("should return '+' if count includes '+'", () => {
+ expect(addCountOverLimit('10+')).toEqual('+');
+ });
+
+ it("should return empty string if count does not include '+'", () => {
+ expect(addCountOverLimit('10')).toEqual('');
+ });
+
+ it('should return empty string if count is not provided', () => {
+ expect(addCountOverLimit()).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index 2ff731aa102..cc0ca90dc39 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import Vuex from 'vuex';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
@@ -10,9 +10,14 @@ 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 { 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;
@@ -85,15 +90,25 @@ describe('UserBar component', () => {
expect(mrsCounter.attributes('data-track-property')).toBe('nav_core_menu');
});
- it('renders todos counter', () => {
- const todosCounter = findTodosCounter();
- expect(todosCounter.props('count')).toBe(sidebarData.todos_pending_count);
- expect(todosCounter.props('href')).toBe('/dashboard/todos');
- expect(todosCounter.props('label')).toBe(__('To-Do list'));
- expect(todosCounter.attributes('data-track-action')).toBe('click_link');
- expect(todosCounter.attributes('data-track-label')).toBe('todos_link');
- expect(todosCounter.attributes('data-track-property')).toBe('nav_core_menu');
- expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
+ describe('Todos counter', () => {
+ it('renders it', () => {
+ const todosCounter = findTodosCounter();
+ expect(todosCounter.props('href')).toBe('/dashboard/todos');
+ expect(todosCounter.props('label')).toBe(__('To-Do list'));
+ expect(todosCounter.attributes('data-track-action')).toBe('click_link');
+ expect(todosCounter.attributes('data-track-label')).toBe('todos_link');
+ expect(todosCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
+ });
+
+ it('should format and 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+');
+ });
});
it('renders branding logo', () => {
diff --git a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
index 9a04b716001..931d4ba132c 100644
--- a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::PaginatedTreeResolver do
+RSpec.describe Resolvers::PaginatedTreeResolver, feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -61,6 +61,16 @@ RSpec.describe Resolvers::PaginatedTreeResolver do
end
end
+ context 'when repository is empty' do
+ before do
+ allow(repository).to receive(:empty?).and_return(true)
+ end
+
+ it 'returns nil' do
+ is_expected.to be(nil)
+ end
+ end
+
describe 'Cursor pagination' do
context 'when cursor is invalid' do
let(:args) { super().merge(after: 'invalid') }
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 3ebfc2c2bb5..7bb5c3557fd 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -115,7 +115,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
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",
+ todos_pending_count: 3,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
total_merge_requests_count: "4",
projects_path: projects_path,
@@ -261,7 +261,6 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
context 'when counts are high' do
before do
allow(user).to receive(:assigned_open_issues_count).and_return(1000)
- allow(user).to receive(:todos_pending_count).and_return(3000)
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
@@ -269,7 +268,6 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
it 'caps counts to USER_BAR_COUNT_LIMIT and appends a "+" to them' do
expect(subject).to include(
assigned_open_issues_count: "99+",
- todos_pending_count: "99+",
total_merge_requests_count: "99+"
)
end
@@ -420,6 +418,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
expect(helper.super_sidebar_nav_panel(nav: 'user_profile')).to be_a(Sidebars::UserProfile::Panel)
end
+ it 'returns Admin Panel for admin nav' do
+ expect(helper.super_sidebar_nav_panel(nav: 'admin')).to be_a(Sidebars::Admin::Panel)
+ end
+
it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do
expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel)
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c40284ee933..01dacf5fcad 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe TreeHelper do
+ include Devise::Test::ControllerHelpers
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' }
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index d77aaa0e846..b6e369cb35b 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -259,6 +259,41 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
expect(finder.email_for_github_username('kittens')).to be_nil
end
+
+ context 'when a username does not exist on GitHub' do
+ context 'when github username inexistence is not cached' do
+ it 'caches github username inexistence' do
+ expect(client)
+ .to receive(:user)
+ .with('kittens')
+ .and_raise(::Octokit::NotFound)
+
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write).with(
+ described_class::INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % 'kittens', true
+ )
+
+ expect(finder.email_for_github_username('kittens')).to be_nil
+ end
+ end
+
+ context 'when github username inexistence is already cached' do
+ it 'does not make request to the client' do
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:read).with(described_class::EMAIL_FOR_USERNAME_CACHE_KEY % 'kittens')
+
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:read).with(
+ described_class::INEXISTENCE_OF_GITHUB_USERNAME_CACHE_KEY % 'kittens'
+ ).and_return('true')
+
+ expect(client)
+ .not_to receive(:user)
+
+ expect(finder.email_for_github_username('kittens')).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/harbor/client_spec.rb b/spec/lib/gitlab/harbor/client_spec.rb
index 4e80b8b53e3..745e22191bd 100644
--- a/spec/lib/gitlab/harbor/client_spec.rb
+++ b/spec/lib/gitlab/harbor/client_spec.rb
@@ -265,18 +265,20 @@ RSpec.describe Gitlab::Harbor::Client do
end
end
- describe '#ping' do
+ describe '#check_project_availability' do
before do
- stub_request(:get, "https://demo.goharbor.io/api/v2.0/ping")
+ stub_request(:head, "https://demo.goharbor.io/api/v2.0/projects?project_name=testproject")
.with(
headers: {
+ 'Accept': 'application/json',
+ 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=',
'Content-Type': 'application/json'
})
- .to_return(status: 200, body: 'pong')
+ .to_return(status: 200, body: '', headers: {})
end
- it "calls api/v2.0/ping successfully" do
- expect(client.ping).to eq(success: true)
+ it "calls api/v2.0/projects successfully" do
+ expect(client.check_project_availability).to eq(success: true)
end
end
diff --git a/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb b/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb
new file mode 100644
index 00000000000..5926852ff57
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::AbuseReportsMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/abuse_reports',
+ title: _('Abuse Reports'),
+ icon: 'slight-frown'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :abuse_reports }
+
+ describe '#pill_count' do
+ let_it_be(:user) { create(:user, :admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'returns zero when there are no abuse reports' do
+ expect(subject.pill_count).to eq 0
+ end
+
+ it 'memoizes the query' do
+ subject.pill_count
+
+ control = ActiveRecord::QueryRecorder.new do
+ subject.pill_count
+ end
+
+ expect(control.count).to eq 0
+ end
+
+ context 'when there are abuse reports' do
+ it 'returns the number of abuse reports' do
+ create_list(:abuse_report, 2)
+
+ expect(subject.pill_count).to eq 2
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/admin/menus/admin_overview_menu_spec.rb b/spec/lib/sidebars/admin/menus/admin_overview_menu_spec.rb
new file mode 100644
index 00000000000..d076e73fdd1
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/admin_overview_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::AdminOverviewMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin',
+ title: s_('Admin|Overview'),
+ icon: 'overview'
+
+ it_behaves_like 'Admin menu with sub menus'
+end
diff --git a/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb b/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb
new file mode 100644
index 00000000000..be23dd4d25b
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::AdminSettingsMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/application_settings/general',
+ title: s_('Admin|Settings'),
+ icon: 'settings'
+
+ it_behaves_like 'Admin menu with sub menus'
+end
diff --git a/spec/lib/sidebars/admin/menus/analytics_menu_spec.rb b/spec/lib/sidebars/admin/menus/analytics_menu_spec.rb
new file mode 100644
index 00000000000..b4aa6e9aeb6
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/analytics_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::AnalyticsMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/dev_ops_reports',
+ title: s_('Admin|Analytics'),
+ icon: 'chart'
+
+ it_behaves_like 'Admin menu with sub menus'
+end
diff --git a/spec/lib/sidebars/admin/menus/applications_menu_spec.rb b/spec/lib/sidebars/admin/menus/applications_menu_spec.rb
new file mode 100644
index 00000000000..0346fa4adfa
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/applications_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::ApplicationsMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/applications',
+ title: s_('Admin|Applications'),
+ icon: 'applications'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :applications }
+end
diff --git a/spec/lib/sidebars/admin/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/admin/menus/ci_cd_menu_spec.rb
new file mode 100644
index 00000000000..b0d46abbee2
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/ci_cd_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::CiCdMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/runners',
+ title: s_('Admin|CI/CD'),
+ icon: 'rocket'
+
+ it_behaves_like 'Admin menu with sub menus'
+end
diff --git a/spec/lib/sidebars/admin/menus/deploy_keys_menu_spec.rb b/spec/lib/sidebars/admin/menus/deploy_keys_menu_spec.rb
new file mode 100644
index 00000000000..f0ee846fb42
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/deploy_keys_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::DeployKeysMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/deploy_keys',
+ title: s_('Admin|Deploy Keys'),
+ icon: 'key'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :deploy_keys }
+end
diff --git a/spec/lib/sidebars/admin/menus/labels_menu_spec.rb b/spec/lib/sidebars/admin/menus/labels_menu_spec.rb
new file mode 100644
index 00000000000..63e4927ab0d
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/labels_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::LabelsMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/labels',
+ title: s_('Admin|Labels'),
+ icon: 'labels'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :labels }
+end
diff --git a/spec/lib/sidebars/admin/menus/messages_menu_spec.rb b/spec/lib/sidebars/admin/menus/messages_menu_spec.rb
new file mode 100644
index 00000000000..14979b7e47a
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/messages_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::MessagesMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/broadcast_messages',
+ title: s_('Admin|Messages'),
+ icon: 'messages'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :broadcast_messages }
+end
diff --git a/spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb b/spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb
new file mode 100644
index 00000000000..0483159da7a
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::MonitoringMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/system_info',
+ title: s_('Admin|Monitoring'),
+ icon: 'monitor'
+
+ it_behaves_like 'Admin menu with sub menus'
+end
diff --git a/spec/lib/sidebars/admin/menus/system_hooks_menu_spec.rb b/spec/lib/sidebars/admin/menus/system_hooks_menu_spec.rb
new file mode 100644
index 00000000000..a2d0b851091
--- /dev/null
+++ b/spec/lib/sidebars/admin/menus/system_hooks_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Menus::SystemHooksMenu, feature_category: :navigation do
+ it_behaves_like 'Admin menu',
+ link: '/admin/hooks',
+ title: s_('Admin|System Hooks'),
+ icon: 'hook'
+
+ it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :hooks }
+end
diff --git a/spec/lib/sidebars/admin/panel_spec.rb b/spec/lib/sidebars/admin/panel_spec.rb
new file mode 100644
index 00000000000..a12fc8f8d2a
--- /dev/null
+++ b/spec/lib/sidebars/admin/panel_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Admin::Panel, feature_category: :navigation do
+ let_it_be(:user) { build(:admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq({ title: 'Admin Area' })
+ end
+end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index 1b8199fec55..e7f041296b7 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe User, feature_category: :system_access do
expect(described_class::USER_TYPES.keys)
.to match_array(%w[human human_deprecated ghost alert_bot project_bot support_bot service_user security_bot
visual_review_bot migration_bot automation_bot security_policy_bot admin_bot suggested_reviewers_bot
- service_account])
+ service_account llm_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
diff --git a/spec/models/integrations/harbor_spec.rb b/spec/models/integrations/harbor_spec.rb
index b4580028112..c4da876a0dd 100644
--- a/spec/models/integrations/harbor_spec.rb
+++ b/spec/models/integrations/harbor_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Integrations::Harbor do
before do
allow_next_instance_of(Gitlab::Harbor::Client) do |client|
- allow(client).to receive(:ping).and_return(test_response)
+ allow(client).to receive(:check_project_availability).and_return(test_response)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 403a1404bad..5b4ba097dec 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -6821,7 +6821,8 @@ RSpec.describe User, feature_category: :user_profile do
{ user_type: :support_bot },
{ user_type: :security_bot },
{ user_type: :automation_bot },
- { user_type: :admin_bot }
+ { user_type: :admin_bot },
+ { user_type: :llm_bot }
]
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 2949320c571..0d91c288bbc 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
+ let_it_be(:llm_bot) { create(:user, :llm_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
@@ -238,6 +239,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:access_api) }
end
+ context 'llm bot' do
+ let(:current_user) { llm_bot }
+
+ it { is_expected.to be_disallowed(:access_api) }
+ end
+
context 'user blocked pending approval' do
before do
current_user.block_pending_approval
@@ -617,6 +624,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:log_in) }
end
+ context 'llm bot' do
+ let(:current_user) { llm_bot }
+
+ it { is_expected.to be_disallowed(:log_in) }
+ end
+
context 'user blocked pending approval' do
before do
current_user.block_pending_approval
@@ -657,6 +670,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
+ context 'with llm_bot' do
+ let(:current_user) { llm_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
context 'with regular user' do
let(:current_user) { user }
@@ -704,6 +723,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:create_instance_runner) }
end
+ context 'with llm_bot' do
+ let(:current_user) { llm_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
context 'with regular user' do
let(:current_user) { user }
diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb
index 4f2be7111ac..41ae6398614 100644
--- a/spec/services/work_items/parent_links/create_service_spec.rb
+++ b/spec/services/work_items/parent_links/create_service_spec.rb
@@ -118,26 +118,74 @@ RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfol
expect(subject[:created_references].map(&:work_item_id)).to match_array([task1.id, task2.id])
end
- it 'creates notes', :aggregate_failures do
- subject
+ it 'creates notes and records the events', :aggregate_failures do
+ expect { subject }.to change(WorkItems::ResourceLinkEvent, :count).by(2)
work_item_notes = work_item.notes.last(2)
+ resource_link_events = WorkItems::ResourceLinkEvent.last(2)
expect(work_item_notes.first.note).to eq("added #{task1.to_reference} as child task")
expect(work_item_notes.last.note).to eq("added #{task2.to_reference} as child task")
expect(task1.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
+ expect(resource_link_events.first).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task1.id,
+ action: "add",
+ system_note_metadata_id: task1.notes.last.system_note_metadata.id
+ )
+ expect(resource_link_events.last).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task2.id,
+ action: "add",
+ system_note_metadata_id: task2.notes.last.system_note_metadata.id
+ )
+ end
+
+ context 'when note creation fails for some reason' do
+ let(:params) { { issuable_references: [task1] } }
+
+ [Note.new, nil].each do |relate_child_note|
+ it 'still records the link event', :aggregate_failures do
+ allow_next_instance_of(WorkItems::ParentLinks::CreateService) do |instance|
+ allow(instance).to receive(:create_notes).and_return(relate_child_note)
+ end
+
+ expect { subject }
+ .to change(WorkItems::ResourceLinkEvent, :count).by(1)
+ .and not_change(Note, :count)
+
+ expect(WorkItems::ResourceLinkEvent.last).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task1.id,
+ action: "add",
+ system_note_metadata_id: nil
+ )
+ end
+ end
end
context 'when task is already assigned' do
let(:params) { { issuable_references: [task, task2] } }
it 'creates links only for non related tasks', :aggregate_failures do
- expect { subject }.to change(parent_link_class, :count).by(1)
+ expect { subject }
+ .to change(parent_link_class, :count).by(1)
+ .and change(WorkItems::ResourceLinkEvent, :count).by(1)
expect(subject[:created_references].map(&:work_item_id)).to match_array([task2.id])
expect(work_item.notes.last.note).to eq("added #{task2.to_reference} as child task")
expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
expect(task.notes).to be_empty
+ expect(WorkItems::ResourceLinkEvent.last).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task2.id,
+ action: "add",
+ system_note_metadata_id: task2.notes.last.system_note_metadata.id
+ )
end
end
diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb
index c77546f6ca1..7e2e3949b73 100644
--- a/spec/services/work_items/parent_links/destroy_service_spec.rb
+++ b/spec/services/work_items/parent_links/destroy_service_spec.rb
@@ -24,23 +24,53 @@ RSpec.describe WorkItems::ParentLinks::DestroyService, feature_category: :team_p
let(:user) { reporter }
it 'removes relation and creates notes', :aggregate_failures do
- expect { subject }.to change(parent_link_class, :count).by(-1)
+ expect { subject }
+ .to change(parent_link_class, :count).by(-1)
+ .and change(WorkItems::ResourceLinkEvent, :count).by(1)
expect(work_item.notes.last.note).to eq("removed child task #{task.to_reference}")
expect(task.notes.last.note).to eq("removed parent issue #{work_item.to_reference}")
+ expect(WorkItems::ResourceLinkEvent.last).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task.id,
+ action: "remove",
+ system_note_metadata_id: task.notes.last.system_note_metadata.id
+ )
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
+
+ context 'when note creation fails for some reason' do
+ [Note.new, nil].each do |unrelate_child_note|
+ it 'still records the link event', :aggregate_failures do
+ allow(SystemNoteService).to receive(:unrelate_work_item).and_return(unrelate_child_note)
+
+ expect { subject }
+ .to change(WorkItems::ResourceLinkEvent, :count).by(1)
+ .and not_change(Note, :count)
+
+ expect(WorkItems::ResourceLinkEvent.last).to have_attributes(
+ user_id: user.id,
+ issue_id: work_item.id,
+ child_work_item_id: task.id,
+ action: "remove",
+ system_note_metadata_id: nil
+ )
+ end
+ end
+ end
end
context 'when user has insufficient permissions' do
let(:user) { guest }
it 'does not remove relation', :aggregate_failures do
- expect { subject }.not_to change(parent_link_class, :count).from(1)
-
+ expect { subject }
+ .to not_change(parent_link_class, :count).from(1)
+ .and not_change(WorkItems::ResourceLinkEvent, :count)
expect(SystemNoteService).not_to receive(:unrelate_work_item)
end
diff --git a/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb
new file mode 100644
index 00000000000..a9fd66528bd
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Admin menu' do |link:, title:, icon:|
+ let_it_be(:user) { build(:user, :admin) }
+
+ before do
+ allow(user).to receive(:can_admin_all_resources?).and_return(true)
+ end
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to be icon
+ end
+
+ describe '#render?' do
+ context 'when user is admin' do
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not admin' do
+ it 'does not render' do
+ expect(described_class.new(Sidebars::Context.new(current_user: build(:user),
+ container: nil)).render?).to be false
+ end
+ end
+
+ context 'when user is not logged in' do
+ it 'does not render' do
+ expect(described_class.new(Sidebars::Context.new(current_user: nil, container: nil)).render?).to be false
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Admin menu without sub menus' do |active_routes:|
+ let_it_be(:user) { build(:user, :admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu(s)' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes).to eq active_routes
+ end
+end
+
+RSpec.shared_examples 'Admin menu with sub menus' do
+ let_it_be(:user) { build(:user, :admin) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'contains submemus' do
+ expect(subject.has_items?).to be true
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index 163f39568e5..3097598aaca 100644
--- a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -2,7 +2,14 @@
require 'spec_helper'
-RSpec.describe 'layouts/nav/sidebar/_admin' do
+RSpec.describe 'layouts/nav/sidebar/_admin', feature_category: :navigation do
+ let(:user) { build(:admin) }
+
+ before do
+ allow(user).to receive(:can_admin_all_resources?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
render
@@ -32,7 +39,7 @@ RSpec.describe 'layouts/nav/sidebar/_admin' do
context 'on projects' do
before do
- allow(controller).to receive(:controller_name).and_return('projects')
+ allow(controller).to receive(:controller_name).and_return('admin/projects')
allow(controller).to receive(:controller_path).and_return('admin/projects')
end