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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_manual_todo.yml20
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js8
-rw-r--r--app/assets/javascripts/delete_label_modal.js16
-rw-r--r--app/assets/javascripts/jira_connect/api.js3
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue96
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list_item.vue13
-rw-r--r--app/assets/javascripts/pages/groups/labels/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/delete_label_modal.vue81
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss3
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb37
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb3
-rw-r--r--app/graphql/resolvers/issues_resolver.rb3
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb2
-rw-r--r--app/graphql/types/issue_type.rb3
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/graphql/types/repository_type.rb2
-rw-r--r--app/helpers/profiles_helper.rb14
-rw-r--r--app/helpers/sidebars_helper.rb12
-rw-r--r--app/models/blob.rb1
-rw-r--r--app/models/concerns/has_timelogs_report.rb3
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/sidebars/panel.rb20
-rw-r--r--app/models/sidebars/projects/panel.rb22
-rw-r--r--app/services/projects/update_pages_service.rb6
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml472
-rw-r--r--app/views/layouts/nav/sidebar/_project_menus.html.haml458
-rw-r--r--app/views/layouts/nav/sidebar/_project_scope_menu.html.haml6
-rw-r--r--app/views/profiles/keys/_form.html.haml3
-rw-r--r--app/views/profiles/keys/_key.html.haml14
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/shared/_delete_label_modal.html.haml20
-rw-r--r--app/views/shared/_label.html.haml10
-rw-r--r--app/views/shared/nav/_sidebar.html.haml10
-rw-r--r--app/workers/concerns/each_shard_worker.rb8
-rw-r--r--changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml5
-rw-r--r--changelogs/unreleased/324100-update-default-initial-branch-name.yml5
-rw-r--r--changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml5
-rw-r--r--changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml5
-rw-r--r--changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml (renamed from 57952- Update GIicon in geo_node_header.vue.yml)2
-rw-r--r--changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml5
-rw-r--r--changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml5
-rw-r--r--changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml5
-rw-r--r--changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml5
-rw-r--r--config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml8
-rw-r--r--doc/api/graphql/reference/index.md4
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb12
-rw-r--r--locale/gitlab.pot29
-rw-r--r--qa/qa/page/project/menu.rb2
-rw-r--r--qa/qa/page/project/sub_menus/ci_cd.rb2
-rw-r--r--qa/qa/page/project/sub_menus/issues.rb2
-rw-r--r--qa/qa/page/project/sub_menus/operations.rb2
-rw-r--r--qa/qa/page/project/sub_menus/project.rb2
-rw-r--r--qa/qa/page/project/sub_menus/repository.rb2
-rw-r--r--qa/qa/page/project/sub_menus/settings.rb2
-rw-r--r--spec/factories/timelogs.rb19
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb8
-rw-r--r--spec/frontend/delete_label_modal_spec.js83
-rw-r--r--spec/frontend/jira_connect/components/groups_list_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/delete_label_modal_spec.js64
-rw-r--r--spec/graphql/resolvers/blobs_resolver_spec.rb74
-rw-r--r--spec/graphql/resolvers/project_pipeline_resolver_spec.rb15
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb57
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/graphql/types/repository_type_spec.rb2
-rw-r--r--spec/helpers/profiles_helper_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb70
-rw-r--r--spec/lib/gitlab/database_spec.rb4
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/hook_data/release_builder_spec.rb1
-rw-r--r--spec/models/concerns/has_timelogs_report_spec.rb8
-rw-r--r--spec/models/project_spec.rb32
-rw-r--r--spec/models/timelog_spec.rb12
-rw-r--r--spec/requests/api/graphql/group/timelogs_spec.rb19
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb71
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb9
-rw-r--r--spec/services/groups/auto_devops_service_spec.rb1
-rw-r--r--spec/services/groups/group_links/update_service_spec.rb1
-rw-r--r--spec/services/groups/transfer_service_spec.rb3
-rw-r--r--spec/services/groups/update_shared_runners_service_spec.rb1
-rw-r--r--spec/services/ide/base_config_service_spec.rb1
-rw-r--r--spec/services/ide/schemas_config_service_spec.rb1
-rw-r--r--spec/services/ide/terminal_config_service_spec.rb1
-rw-r--r--spec/services/issues/build_service_spec.rb1
-rw-r--r--spec/services/issues/clone_service_spec.rb1
-rw-r--r--spec/services/issues/create_service_spec.rb1
-rw-r--r--spec/services/issues/export_csv_service_spec.rb1
-rw-r--r--spec/services/issues/move_service_spec.rb1
-rw-r--r--spec/services/issues/related_branches_service_spec.rb1
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/update_pages_service_spec.rb6
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb50
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb109
-rw-r--r--spec/workers/projects/post_creation_worker_spec.rb5
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb7
101 files changed, 1689 insertions, 717 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 0bda1b60623..4d8d5545a14 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -604,10 +604,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb
- ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb
- ee/spec/services/merge_request_approval_settings/update_service_spec.rb
- - ee/spec/services/merge_trains/check_status_service_spec.rb
- - ee/spec/services/merge_trains/create_pipeline_service_spec.rb
- - ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb
- - ee/spec/services/merge_trains/refresh_service_spec.rb
- ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb
- ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb
- ee/spec/services/projects/after_rename_service_spec.rb
@@ -779,9 +775,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/lib/gitlab/gitaly_client/operation_service_spec.rb
- spec/lib/gitlab/gl_repository/repo_type_spec.rb
- spec/lib/gitlab/group_search_results_spec.rb
- - spec/lib/gitlab/hook_data/issue_builder_spec.rb
- - spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
- - spec/lib/gitlab/hook_data/release_builder_spec.rb
- spec/lib/gitlab/json_cache_spec.rb
- spec/lib/gitlab/language_detection_spec.rb
- spec/lib/gitlab/project_search_results_spec.rb
@@ -1010,25 +1003,12 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/services/feature_flags/enable_service_spec.rb
- spec/services/feature_flags/update_service_spec.rb
- spec/services/git/branch_push_service_spec.rb
- - spec/services/groups/auto_devops_service_spec.rb
- - spec/services/groups/group_links/update_service_spec.rb
- - spec/services/groups/transfer_service_spec.rb
- - spec/services/groups/update_shared_runners_service_spec.rb
- - spec/services/ide/base_config_service_spec.rb
- - spec/services/ide/schemas_config_service_spec.rb
- - spec/services/ide/terminal_config_service_spec.rb
- spec/services/import/bitbucket_server_service_spec.rb
- spec/services/incident_management/incidents/create_service_spec.rb
- spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
- spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
- spec/services/integrations/test/project_service_spec.rb
- spec/services/issuable/bulk_update_service_spec.rb
- - spec/services/issues/build_service_spec.rb
- - spec/services/issues/clone_service_spec.rb
- - spec/services/issues/create_service_spec.rb
- - spec/services/issues/export_csv_service_spec.rb
- - spec/services/issues/move_service_spec.rb
- - spec/services/issues/related_branches_service_spec.rb
- spec/services/jira_connect/sync_service_spec.rb
- spec/services/jira_import/start_import_service_spec.rb
- spec/services/jira_import/users_importer_spec.rb
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index ca019bc4178..66e8d982113 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -4,13 +4,13 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import DivergenceGraph from './components/divergence_graph.vue';
-export function createGraphVueApp(el, data, maxCommits) {
+export function createGraphVueApp(el, data, maxCommits, defaultBranch) {
return new Vue({
el,
render(h) {
return h(DivergenceGraph, {
props: {
- defaultBranch: 'master',
+ defaultBranch,
distance: data.distance ? parseInt(data.distance, 10) : null,
aheadCount: parseInt(data.ahead, 10),
behindCount: parseInt(data.behind, 10),
@@ -21,7 +21,7 @@ export function createGraphVueApp(el, data, maxCommits) {
});
}
-export default (endpoint) => {
+export default (endpoint, defaultBranch) => {
const names = [...document.querySelectorAll('.js-branch-item')].map(
({ dataset }) => dataset.name,
);
@@ -47,7 +47,7 @@ export default (endpoint) => {
if (!el) return;
- createGraphVueApp(el, val, maxCommits);
+ createGraphVueApp(el, val, maxCommits, defaultBranch);
});
})
.catch(() =>
diff --git a/app/assets/javascripts/delete_label_modal.js b/app/assets/javascripts/delete_label_modal.js
new file mode 100644
index 00000000000..cf7c9e7734f
--- /dev/null
+++ b/app/assets/javascripts/delete_label_modal.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
+
+const mountDeleteLabelModal = (optionalProps) =>
+ new Vue({
+ render(h) {
+ return h(DeleteLabelModal, {
+ props: {
+ selector: '.js-delete-label-modal-button',
+ ...optionalProps,
+ },
+ });
+ },
+ }).$mount();
+
+export default (optionalProps = {}) => mountDeleteLabelModal(optionalProps);
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js
index d78aba0a3f7..8da2ca73f9a 100644
--- a/app/assets/javascripts/jira_connect/api.js
+++ b/app/assets/javascripts/jira_connect/api.js
@@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => {
});
};
-export const fetchGroups = async (groupsPath, { page, perPage }) => {
+export const fetchGroups = async (groupsPath, { page, perPage, search }) => {
return axios.get(groupsPath, {
params: {
page,
per_page: perPage,
+ search,
},
});
};
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index 69f2903388c..275ff820419 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
import { fetchGroups } from '~/jira_connect/api';
import { defaultPerPage } from '~/jira_connect/constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
@@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
export default {
components: {
- GlTabs,
- GlTab,
GlLoadingIcon,
GlPagination,
GlAlert,
+ GlSearchBoxByType,
GroupsListItem,
},
inject: {
@@ -23,7 +22,8 @@ export default {
data() {
return {
groups: [],
- isLoading: false,
+ isLoadingInitial: true,
+ isLoadingMore: false,
page: 1,
perPage: defaultPerPage,
totalItems: 0,
@@ -31,15 +31,18 @@ export default {
};
},
mounted() {
- this.loadGroups();
+ return this.loadGroups().finally(() => {
+ this.isLoadingInitial = false;
+ });
},
methods: {
- loadGroups() {
- this.isLoading = true;
+ loadGroups({ searchTerm } = {}) {
+ this.isLoadingMore = true;
- fetchGroups(this.groupsPath, {
+ return fetchGroups(this.groupsPath, {
page: this.page,
perPage: this.perPage,
+ search: searchTerm,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
@@ -51,50 +54,61 @@ export default {
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
- this.isLoading = false;
+ this.isLoadingMore = false;
});
},
+ onGroupSearch(searchTerm) {
+ return this.loadGroups({ searchTerm });
+ },
},
};
</script>
<template>
<div>
- <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
+ <gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
- <gl-tabs>
- <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
- <gl-loading-icon v-if="isLoading" size="md" />
- <div v-else-if="groups.length === 0" class="gl-text-center">
- <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
- <p class="gl-mt-5">
- {{
- s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
- }}
- </p>
- </div>
- <ul v-else class="gl-list-style-none gl-pl-0">
- <groups-list-item
- v-for="group in groups"
- :key="group.id"
- :group="group"
- @error="errorMessage = $event"
- />
- </ul>
+ <gl-search-box-by-type
+ class="gl-mb-5"
+ debounce="500"
+ :placeholder="__('Search by name')"
+ :is-loading="isLoadingMore"
+ @input="onGroupSearch"
+ />
+
+ <gl-loading-icon v-if="isLoadingInitial" size="md" />
+ <div v-else-if="groups.length === 0" class="gl-text-center">
+ <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
+ <p class="gl-mt-5">
+ {{ s__('Integrations|You must have owner or maintainer permissions to link namespaces.') }}
+ </p>
+ </div>
+ <ul
+ v-else
+ class="gl-list-style-none gl-pl-0 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
+ :class="{ 'gl-opacity-5': isLoadingMore }"
+ data-testid="groups-list"
+ >
+ <groups-list-item
+ v-for="group in groups"
+ :key="group.id"
+ :group="group"
+ :disabled="isLoadingMore"
+ @error="errorMessage = $event"
+ />
+ </ul>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-pagination
- v-if="totalItems > perPage && groups.length > 0"
- v-model="page"
- class="gl-mb-0"
- :per-page="perPage"
- :total-items="totalItems"
- @input="loadGroups"
- />
- </div>
- </gl-tab>
- </gl-tabs>
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-pagination
+ v-if="totalItems > perPage && groups.length > 0"
+ v-model="page"
+ class="gl-mb-0"
+ :per-page="perPage"
+ :total-items="totalItems"
+ @input="loadGroups"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
index b8959a2a505..9c5722c44c7 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
@@ -21,6 +21,11 @@ export default {
type: Object,
required: true,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -60,7 +65,7 @@ export default {
</script>
<template>
- <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
+ <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<gl-icon name="folder-o" class="gl-mr-3" />
<div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
@@ -83,11 +88,13 @@ export default {
<gl-button
category="secondary"
- variant="success"
+ variant="confirm"
:loading="isLoading"
+ :disabled="disabled"
@click.prevent="onClick"
- >{{ __('Link') }}</gl-button
>
+ {{ __('Link') }}
+ </gl-button>
</div>
</div>
</li>
diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js
index 87d522d7654..95c2c7cd7d0 100644
--- a/app/assets/javascripts/pages/groups/labels/index/index.js
+++ b/app/assets/javascripts/pages/groups/labels/index/index.js
@@ -1,3 +1,5 @@
+import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
initLabels();
+initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 52f9cbea370..45b1cfecc5a 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -5,5 +5,10 @@ import initDiverganceGraph from '~/branches/divergence_graph';
AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new
-initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint);
+
+const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
+ '.js-branch-list',
+).dataset;
+
+initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown();
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 9f782c07101..94ab0d64de4 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
@@ -9,6 +10,7 @@ Vue.use(Translate);
const initLabelIndex = () => {
initLabels();
+ initDeleteLabelModal();
const onRequestFinished = ({ labelUrl, successful }) => {
const button = document.querySelector(
diff --git a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue
new file mode 100644
index 00000000000..1ff0938d086
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlModal, GlSprintf, GlButton } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+export default {
+ components: {
+ GlModal,
+ GlSprintf,
+ GlButton,
+ },
+ props: {
+ selector: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ labelName: '',
+ subjectName: '',
+ destroyPath: '',
+ modalId: uniqueId('modal-delete-label-'),
+ };
+ },
+ mounted() {
+ document.querySelectorAll(this.selector).forEach((button) => {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const { labelName, subjectName, destroyPath } = button.dataset;
+ this.labelName = labelName;
+ this.subjectName = subjectName;
+ this.destroyPath = destroyPath;
+ this.openModal();
+ });
+ });
+ },
+ methods: {
+ openModal() {
+ this.$refs.modal.show();
+ },
+ closeModal() {
+ this.$refs.modal.hide();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal ref="modal" :modal-id="modalId">
+ <template #modal-title>
+ <gl-sprintf :message="__('Delete label: %{labelName}')">
+ <template #labelName>
+ {{ labelName }}
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-sprintf
+ :message="
+ __(
+ `%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`,
+ )
+ "
+ >
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <template #modal-footer>
+ <gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button>
+ <gl-button
+ category="primary"
+ variant="danger"
+ :href="destroyPath"
+ data-method="delete"
+ data-testid="delete-button"
+ >{{ __('Delete label') }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index eb2dd6e578e..346cdbb9a8d 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -4,7 +4,6 @@
@import 'bootstrap-vue/src/index';
@import '@gitlab/ui/src/scss/utilities';
-@import '@gitlab/ui/src/components/base/alert/alert';
// We should only import styles that we actually use.
@import '@gitlab/ui/src/components/base/alert/alert';
@@ -16,8 +15,8 @@
@import '@gitlab/ui/src/components/base/loading_icon/loading_icon';
@import '@gitlab/ui/src/components/base/modal/modal';
@import '@gitlab/ui/src/components/base/pagination/pagination';
-@import '@gitlab/ui/src/components/base/tabs/tabs/tabs';
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
+@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type';
$atlaskit-border-color: #dfe1e6;
$header-height: 40px;
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
new file mode 100644
index 00000000000..521e0482759
--- /dev/null
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BlobsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Tree::BlobType.connection_type, null: true
+ authorize :download_code
+ calls_gitaly!
+
+ alias_method :repository, :object
+
+ argument :paths, [GraphQL::STRING_TYPE],
+ required: true,
+ description: 'Array of desired blob paths.'
+ argument :ref, GraphQL::STRING_TYPE,
+ required: false,
+ default_value: nil,
+ description: 'The commit ref to get the blobs from. Default value is HEAD.'
+
+ # We fetch blobs from Gitaly efficiently but it still scales O(N) with the
+ # number of paths being fetched, so apply a scaling limit to that.
+ def self.resolver_complexity(args, child_complexity:)
+ super + args.fetch(:paths, []).size
+ end
+
+ def resolve(paths:, ref:)
+ authorize!(repository.container)
+
+ return [] if repository.empty?
+
+ ref ||= repository.root_ref
+
+ repository.blobs_at(paths.map { |path| [ref, path] })
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 31444b0c592..75f1ee478a8 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -50,7 +50,8 @@ module ResolvesMergeRequests
approved_by: [:approved_by_users],
milestone: [:milestone],
security_auto_fix: [:author],
- head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
+ head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
+ timelogs: [:timelogs]
}
end
end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index ac3bdda0f12..7a67f115abf 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -44,7 +44,8 @@ module Resolvers
{
alert_management_alert: [:alert_management_alert],
labels: [:labels],
- assignees: [:assignees]
+ assignees: [:assignees],
+ timelogs: [:timelogs]
}
end
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index 8fca6b829c0..aa8808b15ac 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -31,7 +31,7 @@ module Resolvers
end
else
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
- finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas)
+ finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas)
finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index f15ab69f2d4..34c824fe9fb 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -124,6 +124,9 @@ module Types
field :create_note_email, GraphQL::STRING_TYPE, null: true,
description: 'User specific email address for the issue.'
+ field :timelogs, Types::TimelogType.connection_type, null: false,
+ description: 'Timelogs on the issue.'
+
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index b7a50c4931a..c8ccf9d8aff 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -186,6 +186,8 @@ module Types
description: 'Selected auto merge strategy.'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request.'
+ field :timelogs, Types::TimelogType.connection_type, null: false,
+ description: 'Timelogs on the merge request.'
def approved_by
object.approved_by_users
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index e319a5f3124..fc835cdf642 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -14,5 +14,7 @@ module Types
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
+ field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
+ description: 'Blobs contained within the repository'
end
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 87187e97df4..3219620de71 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -37,4 +37,18 @@ module ProfilesHelper
def user_status_set_to_busy?(status)
status&.availability == availability_values[:busy]
end
+
+ # Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip
+ def ssh_key_expiration_tooltip(key)
+ return key.errors.full_messages.join(', ') if key.errors.full_messages.any?
+
+ s_('Profiles|Key usable beyond expiration date.') if key.expired?
+ end
+
+ # Overridden in EE::ProfilesHelper#ssh_key_expires_field_description
+ def ssh_key_expires_field_description
+ s_('Profiles|Key can still be used after expiration.')
+ end
end
+
+ProfilesHelper.prepend_ee_mod
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index b7f31ea86a7..b91fa4762bb 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -14,6 +14,10 @@ module SidebarsHelper
end
end
+ def project_sidebar_context(project, user)
+ Sidebars::Context.new(**project_sidebar_context_data(project, user))
+ end
+
private
def sidebar_project_tracking_attrs
@@ -27,4 +31,12 @@ module SidebarsHelper
def sidebar_user_profile_tracking_attrs
tracking_attrs('user_side_navigation', 'render', 'user_side_navigation')
end
+
+ def project_sidebar_context_data(project, user)
+ {
+ current_user: user,
+ container: project,
+ project: project
+ }
+ end
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 8a9db8b45ea..2185233a1ac 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -2,6 +2,7 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
+ include GlobalID::Identification
include Presentable
include BlobLanguageFromGitAttributes
include BlobActiveModel
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
index dc6d4e6ac6c..90f9876de95 100644
--- a/app/models/concerns/has_timelogs_report.rb
+++ b/app/models/concerns/has_timelogs_report.rb
@@ -2,9 +2,10 @@
module HasTimelogsReport
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
def timelogs(start_time, end_time)
- @timelogs ||= timelogs_for(start_time, end_time)
+ strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
end
def user_can_access_group_timelogs?(current_user)
diff --git a/app/models/project.rb b/app/models/project.rb
index 0f2b3538cb0..e4a9d7568f6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1378,7 +1378,7 @@ class Project < ApplicationRecord
def find_or_initialize_service(name)
return if disabled_services.include?(name)
- find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
+ find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
end
# rubocop: disable CodeReuse/ServiceClass
@@ -2596,6 +2596,10 @@ class Project < ApplicationRecord
return Service.build_from_integration(template, project_id: id) if template
end
+ def build_service(name)
+ "#{name}_service".classify.constantize.new(project_id: id)
+ end
+
def services_templates
@services_templates ||= Service.for_template
end
diff --git a/app/models/sidebars/panel.rb b/app/models/sidebars/panel.rb
index 8fa421defc6..5c8191ebda3 100644
--- a/app/models/sidebars/panel.rb
+++ b/app/models/sidebars/panel.rb
@@ -51,5 +51,25 @@ module Sidebars
def renderable_menus
@renderable_menus ||= @menus.select(&:render?)
end
+
+ def container
+ context.container
+ end
+
+ # Auxiliar method that helps with the migration from
+ # regular views to the new logic
+ def render_raw_scope_menu_partial
+ # No-op
+ end
+
+ # Auxiliar method that helps with the migration from
+ # regular views to the new logic.
+ #
+ # Any menu inside this partial will be added after
+ # all the menus added in the `configure_menus`
+ # method.
+ def render_raw_menus_partial
+ # No-op
+ end
end
end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
new file mode 100644
index 00000000000..d301d2b4c3c
--- /dev/null
+++ b/app/models/sidebars/projects/panel.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ class Panel < ::Sidebars::Panel
+ override :render_raw_menus_partial
+ def render_raw_scope_menu_partial
+ 'layouts/nav/sidebar/project_scope_menu'
+ end
+
+ override :render_raw_menus_partial
+ def render_raw_menus_partial
+ 'layouts/nav/sidebar/project_menus'
+ end
+
+ override :aria_label
+ def aria_label
+ _('Project navigation')
+ end
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 3915b62aa65..d7e2e678dac 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -250,13 +250,17 @@ module Projects
def make_secure_tmp_dir(tmp_path)
FileUtils.mkdir_p(tmp_path)
- path = Dir.mktmpdir(nil, tmp_path)
+ path = Dir.mktmpdir(tmp_dir_prefix, tmp_path)
begin
yield(path)
ensure
FileUtils.remove_entry_secure(path)
end
end
+
+ def tmp_dir_prefix
+ "project-#{project.id}-build-#{build.id}-"
+ end
end
end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 022bbd39723..5cb109cf275 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,469 +1,3 @@
-%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@project), 'aria-label': _('Project navigation') }
- .nav-sidebar-inner-scroll
- .context-header
- = link_to project_path(@project), title: @project.name do
- .avatar-container.rect-avatar.s40.project-avatar
- = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
- .sidebar-context-title
- = @project.name
- %ul.sidebar-top-level-items.qa-project-sidebar
- = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
- = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
- .nav-icon-container
- = sprite_icon('home')
- %span.nav-item-name
- = _('Project overview')
-
- %ul.sidebar-sub-level-items
- = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
- = link_to project_path(@project) do
- %strong.fly-out-top-item-name
- = _('Project overview')
- %li.divider.fly-out-top-item
- = nav_link(path: 'projects#show') do
- = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
- %span= _('Details')
-
- = nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
- %span= _('Activity')
-
- - if project_nav_tab?(:releases)
- = nav_link(controller: :releases) do
- = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
- %span= _('Releases')
-
- - if project_nav_tab? :learn_gitlab
- = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
- = link_to project_learn_gitlab_path(@project) do
- .nav-icon-container
- = sprite_icon('home')
- %span.nav-item-name
- = _('Learn GitLab')
-
- - if project_nav_tab? :files
- = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
- = link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
- .nav-icon-container
- = sprite_icon('doc-text')
- %span.nav-item-name#js-onboarding-repo-link
- = _('Repository')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_tree_path(@project) do
- %strong.fly-out-top-item-name
- = _('Repository')
- %li.divider.fly-out-top-item
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
- = link_to project_tree_path(@project) do
- = _('Files')
-
- = nav_link(controller: [:commit, :commits]) do
- = link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
- = _('Commits')
-
- = nav_link(html_options: {class: branches_tab_class}) do
- = link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
- = _('Branches')
-
- = nav_link(controller: [:tags]) do
- = link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
- = _('Tags')
-
- = nav_link(path: 'graphs#show') do
- = link_to project_graph_path(@project, current_ref) do
- = _('Contributors')
-
- = nav_link(controller: %w(network)) do
- = link_to project_network_path(@project, current_ref) do
- = _('Graph')
-
- = nav_link(controller: :compare) do
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
- = _('Compare')
-
- = render_if_exists 'projects/sidebar/repository_locked_files'
-
- - if project_nav_tab? :issues
- = nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
- = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
- .nav-icon-container
- = sprite_icon('issues')
- %span.nav-item-name#js-onboarding-issues-link
- = _('Issues')
- - if @project.issues_enabled?
- %span.badge.badge-pill.count.issue_counter
- = number_with_delimiter(@project.open_issues_count(current_user))
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_issues_path(@project) do
- %strong.fly-out-top-item-name
- = _('Issues')
- - if @project.issues_enabled?
- %span.badge.badge-pill.count.issue_counter.fly-out-badge
- = number_with_delimiter(@project.open_issues_count(current_user))
- %li.divider.fly-out-top-item
- = nav_link(controller: :issues, action: :index) do
- = link_to project_issues_path(@project), title: _('Issues') do
- %span
- = _('List')
-
- = nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
- %span
- = boards_link_text
-
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
- %span
- = _('Labels')
-
- = render 'projects/sidebar/issues_service_desk'
-
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
- %span
- = _('Milestones')
-
- = render_if_exists 'layouts/nav/sidebar/project_iterations_link'
-
- - if project_nav_tab?(:external_issue_tracker)
- - issue_tracker = @project.external_issue_tracker
- - if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
- = render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
- - else
- = nav_link do
- = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
- .nav-icon-container
- = sprite_icon('external-link')
- %span.nav-item-name
- = issue_tracker.title
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: "fly-out-top-item" } ) do
- = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
- %strong.fly-out-top-item-name
- = issue_tracker.title
-
- - if (project_nav_tab? :labels) && !@project.issues_enabled?
- = nav_link(controller: [:labels]) do
- = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
- .nav-icon-container
- = sprite_icon('label')
- %span.nav-item-name#js-onboarding-labels-link
- = _('Labels')
-
- - if project_nav_tab? :merge_requests
- = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
- = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
- .nav-icon-container
- = sprite_icon('git-merge')
- %span.nav-item-name#js-onboarding-mr-link
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter
- = number_with_delimiter(@project.open_merge_requests_count)
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_merge_requests_path(@project) do
- %strong.fly-out-top-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
- = number_with_delimiter(@project.open_merge_requests_count)
-
- = render_if_exists "layouts/nav/requirements_link", project: @project
-
- - if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
- = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
- .nav-icon-container
- = sprite_icon('rocket')
- %span.nav-item-name#js-onboarding-pipelines-link
- = _('CI/CD')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
- = link_to project_pipelines_path(@project) do
- %strong.fly-out-top-item-name
- = _('CI/CD')
- %li.divider.fly-out-top-item
- - if project_nav_tab? :pipelines
- = nav_link(path: ['pipelines#index', 'pipelines#show']) do
- = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
- %span
- = _('Pipelines')
-
- - if can_view_pipeline_editor?(@project)
- = nav_link(controller: :pipeline_editor, action: :show) do
- = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
- %span
- = s_('Pipelines|Editor')
-
- - if project_nav_tab? :builds
- = nav_link(controller: :jobs) do
- = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
- %span
- = _('Jobs')
-
- - if Feature.enabled?(:artifacts_management_page, @project)
- = nav_link(controller: :artifacts, action: :index) do
- = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
- %span
- = _('Artifacts')
-
- - if project_nav_tab?(:pipelines)
- = nav_link(controller: :pipeline_schedules) do
- = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
- %span
- = _('Schedules')
-
- = render_if_exists "layouts/nav/test_cases_link", project: @project
-
- - if project_nav_tab? :security_and_compliance
- = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
-
- - if project_nav_tab? :operations
- = nav_link(controller: sidebar_operations_paths) do
- = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
- .nav-icon-container
- = sprite_icon('cloud-gear')
- %span.nav-item-name
- = _('Operations')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to sidebar_operations_link_path do
- %strong.fly-out-top-item-name
- = _('Operations')
- %li.divider.fly-out-top-item
-
- - if project_nav_tab? :metrics_dashboards
- = nav_link(controller: :metrics_dashboard, action: [:show]) do
- = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
- %span
- = _('Metrics')
-
- - if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
- = nav_link(controller: :logs, action: [:index]) do
- = link_to project_logs_path(@project), title: _('Logs') do
- %span
- = _('Logs')
-
- - if project_nav_tab? :environments
- = render "layouts/nav/sidebar/tracing_link"
-
- - if project_nav_tab?(:error_tracking)
- = nav_link(controller: :error_tracking) do
- = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
- %span
- = _('Error Tracking')
-
- - if project_nav_tab?(:alert_management)
- = nav_link(controller: :alert_management) do
- = link_to project_alert_management_index_path(@project), title: _('Alerts') do
- %span
- = _('Alerts')
-
- - if project_nav_tab?(:incidents)
- = nav_link(controller: :incidents) do
- = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
- %span
- = _('Incidents')
-
- = render_if_exists 'projects/sidebar/oncall_schedules'
-
- - if project_nav_tab? :serverless
- = nav_link(controller: :functions) do
- = link_to project_serverless_functions_path(@project), title: _('Serverless') do
- %span
- = _('Serverless')
-
- - if project_nav_tab? :terraform
- = nav_link(controller: :terraform) do
- = link_to project_terraform_index_path(@project), title: _('Terraform') do
- %span
- = _('Terraform')
-
- - if project_nav_tab? :clusters
- - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
- = nav_link(controller: [:cluster_agents, :clusters]) do
- = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
- %span
- = _('Kubernetes')
- - if show_cluster_hint
- .js-feature-highlight{ disabled: true,
- data: { trigger: 'manual',
- container: 'body',
- placement: 'right',
- highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
- highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
- dismiss_endpoint: user_callouts_path,
- auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
- - if project_nav_tab? :environments
- = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
- = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
- %span
- = _('Environments')
-
- - if project_nav_tab? :feature_flags
- = nav_link(controller: :feature_flags) do
- = link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
- %span
- = _('Feature Flags')
-
- - if project_nav_tab?(:product_analytics)
- = nav_link(controller: :product_analytics) do
- = link_to project_product_analytics_path(@project), title: _('Product Analytics') do
- %span
- = _('Product Analytics')
-
- = render_if_exists 'layouts/nav/sidebar/project_packages_link'
-
- - if project_nav_tab? :analytics
- = render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
-
- - if project_nav_tab?(:confluence)
- - confluence_url = project_wikis_confluence_path(@project)
- = nav_link do
- = link_to confluence_url, class: 'shortcuts-confluence' do
- .nav-icon-container
- = image_tag 'confluence.svg', alt: _('Confluence')
- %span.nav-item-name
- = _('Confluence')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: 'fly-out-top-item' } ) do
- = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
- %strong.fly-out-top-item-name
- = _('Confluence')
-
- - if project_nav_tab? :wiki
- = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
-
- - if project_nav_tab?(:external_wiki)
- - external_wiki_url = @project.external_wiki.external_wiki_url
- = nav_link do
- = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
- .nav-icon-container
- = sprite_icon('external-link')
- %span.nav-item-name
- = s_('ExternalWikiService|External wiki')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: "fly-out-top-item" } ) do
- = link_to external_wiki_url do
- %strong.fly-out-top-item-name
- = s_('ExternalWikiService|External wiki')
-
- - if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
- .nav-icon-container
- = sprite_icon('snippet')
- %span.nav-item-name
- = _('Snippets')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_snippets_path(@project) do
- %strong.fly-out-top-item-name
- = _('Snippets')
-
- = nav_link(controller: :project_members) do
- = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name
- = _('Members')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
- = link_to project_project_members_path(@project) do
- %strong.fly-out-top-item-name
- = _('Members')
-
- - if project_nav_tab? :settings
- = nav_link(path: sidebar_settings_paths) do
- = link_to edit_project_path(@project) do
- .nav-icon-container
- = sprite_icon('settings')
- %span.nav-item-name.qa-settings-item#js-onboarding-settings-link
- = _('Settings')
-
- %ul.sidebar-sub-level-items
- - can_edit = can?(current_user, :admin_project, @project)
- - if can_edit
- = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to edit_project_path(@project) do
- %strong.fly-out-top-item-name
- = _('Settings')
- %li.divider.fly-out-top-item
- = nav_link(path: %w[projects#edit]) do
- = link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
- %span
- = _('General')
- - if can_edit
- = nav_link(controller: [:integrations, :services]) do
- = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
- %span
- = _('Integrations')
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
- %span
- = _('Webhooks')
- - if can?(current_user, :read_resource_access_tokens, @project)
- = nav_link(controller: [:access_tokens]) do
- = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
- %span
- = _('Access Tokens')
- = nav_link(controller: :repository) do
- = link_to project_settings_repository_path(@project), title: _('Repository') do
- %span
- = _('Repository')
- - if !@project.archived? && @project.feature_available?(:builds, current_user)
- = nav_link(controller: :ci_cd) do
- = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
- %span
- = _('CI/CD')
- - if settings_operations_available?
- = nav_link(controller: [:operations]) do
- = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
- = _('Operations')
- - if @project.pages_available?
- = nav_link(controller: :pages) do
- = link_to project_pages_path(@project), title: _('Pages') do
- %span
- = _('Pages')
-
- -# Shortcut to Project > Activity
- %li.hidden
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
- %span
- = _('Activity')
-
- -# Shortcut to Repository > Graph (formerly, Network)
- - if project_nav_tab? :network
- %li.hidden
- = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
- = _('Graph')
-
- -# Shortcut to Issues > New Issue
- - if project_nav_tab?(:issues)
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- = _('Create a new issue')
-
- -# Shortcut to Pipelines > Jobs
- - if project_nav_tab? :builds
- %li.hidden
- = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
- = _('Jobs')
-
- -# Shortcut to commits page
- - if project_nav_tab? :commits
- %li.hidden
- = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
- = _('Commits')
-
- -# Shortcut to issue boards
- - if project_nav_tab?(:issues)
- %li.hidden
- = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
-
- = render 'shared/sidebar_toggle_button'
+-# We're migration the project sidebar to a logical model based structure. If you need to update
+-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_project_menus.html.haml.
+= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user))
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
new file mode 100644
index 00000000000..79a7150e030
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml
@@ -0,0 +1,458 @@
+= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
+ = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
+ .nav-icon-container
+ = sprite_icon('home')
+ %span.nav-item-name
+ = _('Project overview')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Project overview')
+ %li.divider.fly-out-top-item
+ = nav_link(path: 'projects#show') do
+ = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
+ %span= _('Details')
+
+ = nav_link(path: 'projects#activity') do
+ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
+ %span= _('Activity')
+
+ - if project_nav_tab?(:releases)
+ = nav_link(controller: :releases) do
+ = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
+ %span= _('Releases')
+
+- if project_nav_tab? :learn_gitlab
+ = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
+ = link_to project_learn_gitlab_path(@project) do
+ .nav-icon-container
+ = sprite_icon('home')
+ %span.nav-item-name
+ = _('Learn GitLab')
+
+- if project_nav_tab? :files
+ = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
+ = link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
+ .nav-icon-container
+ = sprite_icon('doc-text')
+ %span.nav-item-name#js-onboarding-repo-link
+ = _('Repository')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_tree_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Repository')
+ %li.divider.fly-out-top-item
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
+ = link_to project_tree_path(@project) do
+ = _('Files')
+
+ = nav_link(controller: [:commit, :commits]) do
+ = link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
+ = _('Commits')
+
+ = nav_link(html_options: {class: branches_tab_class}) do
+ = link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
+ = _('Branches')
+
+ = nav_link(controller: [:tags]) do
+ = link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
+ = _('Tags')
+
+ = nav_link(path: 'graphs#show') do
+ = link_to project_graph_path(@project, current_ref) do
+ = _('Contributors')
+
+ = nav_link(controller: %w(network)) do
+ = link_to project_network_path(@project, current_ref) do
+ = _('Graph')
+
+ = nav_link(controller: :compare) do
+ = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
+ = _('Compare')
+
+ = render_if_exists 'projects/sidebar/repository_locked_files'
+
+- if project_nav_tab? :issues
+ = nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
+ = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
+ .nav-icon-container
+ = sprite_icon('issues')
+ %span.nav-item-name#js-onboarding-issues-link
+ = _('Issues')
+ - if @project.issues_enabled?
+ %span.badge.badge-pill.count.issue_counter
+ = number_with_delimiter(@project.open_issues_count(current_user))
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_issues_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Issues')
+ - if @project.issues_enabled?
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge
+ = number_with_delimiter(@project.open_issues_count(current_user))
+ %li.divider.fly-out-top-item
+ = nav_link(controller: :issues, action: :index) do
+ = link_to project_issues_path(@project), title: _('Issues') do
+ %span
+ = _('List')
+
+ = nav_link(controller: :boards) do
+ = link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
+ %span
+ = boards_link_text
+
+ = nav_link(controller: :labels) do
+ = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
+ %span
+ = _('Labels')
+
+ = render 'projects/sidebar/issues_service_desk'
+
+ = nav_link(controller: :milestones) do
+ = link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
+ %span
+ = _('Milestones')
+
+ = render_if_exists 'layouts/nav/sidebar/project_iterations_link'
+
+- if project_nav_tab?(:external_issue_tracker)
+ - issue_tracker = @project.external_issue_tracker
+ - if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
+ = render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
+ - else
+ = nav_link do
+ = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
+ .nav-icon-container
+ = sprite_icon('external-link')
+ %span.nav-item-name
+ = issue_tracker.title
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: "fly-out-top-item" } ) do
+ = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
+ %strong.fly-out-top-item-name
+ = issue_tracker.title
+
+- if (project_nav_tab? :labels) && !@project.issues_enabled?
+ = nav_link(controller: [:labels]) do
+ = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
+ .nav-icon-container
+ = sprite_icon('label')
+ %span.nav-item-name#js-onboarding-labels-link
+ = _('Labels')
+
+- if project_nav_tab? :merge_requests
+ = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
+ .nav-icon-container
+ = sprite_icon('git-merge')
+ %span.nav-item-name#js-onboarding-mr-link
+ = _('Merge requests')
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter
+ = number_with_delimiter(@project.open_merge_requests_count)
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_merge_requests_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Merge requests')
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
+ = number_with_delimiter(@project.open_merge_requests_count)
+
+= render_if_exists "layouts/nav/requirements_link", project: @project
+
+- if project_nav_tab? :pipelines
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
+ .nav-icon-container
+ = sprite_icon('rocket')
+ %span.nav-item-name#js-onboarding-pipelines-link
+ = _('CI/CD')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
+ = link_to project_pipelines_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('CI/CD')
+ %li.divider.fly-out-top-item
+ - if project_nav_tab? :pipelines
+ = nav_link(path: ['pipelines#index', 'pipelines#show']) do
+ = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
+ %span
+ = _('Pipelines')
+
+ - if can_view_pipeline_editor?(@project)
+ = nav_link(controller: :pipeline_editor, action: :show) do
+ = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
+ %span
+ = s_('Pipelines|Editor')
+
+ - if project_nav_tab? :builds
+ = nav_link(controller: :jobs) do
+ = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
+ %span
+ = _('Jobs')
+
+ - if Feature.enabled?(:artifacts_management_page, @project)
+ = nav_link(controller: :artifacts, action: :index) do
+ = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
+ %span
+ = _('Artifacts')
+
+ - if project_nav_tab?(:pipelines)
+ = nav_link(controller: :pipeline_schedules) do
+ = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
+ %span
+ = _('Schedules')
+
+ = render_if_exists "layouts/nav/test_cases_link", project: @project
+
+- if project_nav_tab? :security_and_compliance
+ = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
+
+- if project_nav_tab? :operations
+ = nav_link(controller: sidebar_operations_paths) do
+ = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Operations')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
+ = link_to sidebar_operations_link_path do
+ %strong.fly-out-top-item-name
+ = _('Operations')
+ %li.divider.fly-out-top-item
+
+ - if project_nav_tab? :metrics_dashboards
+ = nav_link(controller: :metrics_dashboard, action: [:show]) do
+ = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
+ %span
+ = _('Metrics')
+
+ - if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
+ = nav_link(controller: :logs, action: [:index]) do
+ = link_to project_logs_path(@project), title: _('Logs') do
+ %span
+ = _('Logs')
+
+ - if project_nav_tab? :environments
+ = render "layouts/nav/sidebar/tracing_link"
+
+ - if project_nav_tab?(:error_tracking)
+ = nav_link(controller: :error_tracking) do
+ = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
+ %span
+ = _('Error Tracking')
+
+ - if project_nav_tab?(:alert_management)
+ = nav_link(controller: :alert_management) do
+ = link_to project_alert_management_index_path(@project), title: _('Alerts') do
+ %span
+ = _('Alerts')
+
+ - if project_nav_tab?(:incidents)
+ = nav_link(controller: :incidents) do
+ = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
+ %span
+ = _('Incidents')
+
+ = render_if_exists 'projects/sidebar/oncall_schedules'
+
+ - if project_nav_tab? :serverless
+ = nav_link(controller: :functions) do
+ = link_to project_serverless_functions_path(@project), title: _('Serverless') do
+ %span
+ = _('Serverless')
+
+ - if project_nav_tab? :terraform
+ = nav_link(controller: :terraform) do
+ = link_to project_terraform_index_path(@project), title: _('Terraform') do
+ %span
+ = _('Terraform')
+
+ - if project_nav_tab? :clusters
+ - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
+ = nav_link(controller: [:cluster_agents, :clusters]) do
+ = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
+ %span
+ = _('Kubernetes')
+ - if show_cluster_hint
+ .js-feature-highlight{ disabled: true,
+ data: { trigger: 'manual',
+ container: 'body',
+ placement: 'right',
+ highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
+ highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
+ dismiss_endpoint: user_callouts_path,
+ auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
+ - if project_nav_tab? :environments
+ = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
+ = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
+ %span
+ = _('Environments')
+
+ - if project_nav_tab? :feature_flags
+ = nav_link(controller: :feature_flags) do
+ = link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
+ %span
+ = _('Feature Flags')
+
+ - if project_nav_tab?(:product_analytics)
+ = nav_link(controller: :product_analytics) do
+ = link_to project_product_analytics_path(@project), title: _('Product Analytics') do
+ %span
+ = _('Product Analytics')
+
+= render_if_exists 'layouts/nav/sidebar/project_packages_link'
+
+- if project_nav_tab? :analytics
+ = render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
+
+- if project_nav_tab?(:confluence)
+ - confluence_url = project_wikis_confluence_path(@project)
+ = nav_link do
+ = link_to confluence_url, class: 'shortcuts-confluence' do
+ .nav-icon-container
+ = image_tag 'confluence.svg', alt: _('Confluence')
+ %span.nav-item-name
+ = _('Confluence')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: 'fly-out-top-item' } ) do
+ = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
+ %strong.fly-out-top-item-name
+ = _('Confluence')
+
+- if project_nav_tab? :wiki
+ = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
+
+- if project_nav_tab?(:external_wiki)
+ - external_wiki_url = @project.external_wiki.external_wiki_url
+ = nav_link do
+ = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
+ .nav-icon-container
+ = sprite_icon('external-link')
+ %span.nav-item-name
+ = s_('ExternalWikiService|External wiki')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: "fly-out-top-item" } ) do
+ = link_to external_wiki_url do
+ %strong.fly-out-top-item-name
+ = s_('ExternalWikiService|External wiki')
+
+- if project_nav_tab? :snippets
+ = nav_link(controller: :snippets) do
+ = link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
+ .nav-icon-container
+ = sprite_icon('snippet')
+ %span.nav-item-name
+ = _('Snippets')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_snippets_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Snippets')
+
+= nav_link(controller: :project_members) do
+ = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name
+ = _('Members')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_project_members_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Members')
+
+- if project_nav_tab? :settings
+ = nav_link(path: sidebar_settings_paths) do
+ = link_to edit_project_path(@project) do
+ .nav-icon-container
+ = sprite_icon('settings')
+ %span.nav-item-name.qa-settings-item#js-onboarding-settings-link
+ = _('Settings')
+
+ %ul.sidebar-sub-level-items
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if can_edit
+ = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
+ = link_to edit_project_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Settings')
+ %li.divider.fly-out-top-item
+ = nav_link(path: %w[projects#edit]) do
+ = link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
+ %span
+ = _('General')
+ - if can_edit
+ = nav_link(controller: [:integrations, :services]) do
+ = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
+ %span
+ = _('Integrations')
+ = nav_link(controller: [:hooks, :hook_logs]) do
+ = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
+ %span
+ = _('Webhooks')
+ - if can?(current_user, :read_resource_access_tokens, @project)
+ = nav_link(controller: [:access_tokens]) do
+ = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
+ %span
+ = _('Access Tokens')
+ = nav_link(controller: :repository) do
+ = link_to project_settings_repository_path(@project), title: _('Repository') do
+ %span
+ = _('Repository')
+ - if !@project.archived? && @project.feature_available?(:builds, current_user)
+ = nav_link(controller: :ci_cd) do
+ = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
+ %span
+ = _('CI/CD')
+ - if settings_operations_available?
+ = nav_link(controller: [:operations]) do
+ = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
+ = _('Operations')
+ - if @project.pages_available?
+ = nav_link(controller: :pages) do
+ = link_to project_pages_path(@project), title: _('Pages') do
+ %span
+ = _('Pages')
+
+-# Shortcut to Project > Activity
+%li.hidden
+ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
+ %span
+ = _('Activity')
+
+-# Shortcut to Repository > Graph (formerly, Network)
+- if project_nav_tab? :network
+ %li.hidden
+ = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
+ = _('Graph')
+
+-# Shortcut to Issues > New Issue
+- if project_nav_tab?(:issues)
+ %li.hidden
+ = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
+ = _('Create a new issue')
+
+-# Shortcut to Pipelines > Jobs
+- if project_nav_tab? :builds
+ %li.hidden
+ = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
+ = _('Jobs')
+
+-# Shortcut to commits page
+- if project_nav_tab? :commits
+ %li.hidden
+ = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
+ = _('Commits')
+
+-# Shortcut to issue boards
+- if project_nav_tab?(:issues)
+ %li.hidden
+ = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml b/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml
new file mode 100644
index 00000000000..a666c032248
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml
@@ -0,0 +1,6 @@
+.context-header
+ = link_to project_path(@project), title: @project.name do
+ .avatar-container.rect-avatar.s40.project-avatar
+ = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
+ .sidebar-context-title
+ = @project.name
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 96a05097935..35335f3ef80 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -15,11 +15,12 @@
.col.form-group
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
+ %p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description
.js-add-ssh-key-validation-warning.hide
.bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
%strong= _('Oops, are you sure?')
- %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible.")
+ %p= s_("Profiles|Publicly visible private SSH keys can compromise your system.")
%button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index cc2e2a30052..6a87e052272 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,3 +1,5 @@
+- icon_classes = 'settings-list-icon gl-display-none gl-sm-display-block'
+
%li.key-list-item
.gl-display-flex.gl-align-items-flex-start
.key-list-item-info.gl-w-full.float-none
@@ -5,15 +7,11 @@
= key.title
.gl-display-flex.gl-align-items-center.gl-mt-2
- - if key.valid?
- - if key.expired?
- %span.gl-display-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') }
- = sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
- - else
- = sprite_icon('key', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
+ - if key.valid? && !key.expired?
+ = sprite_icon('key', css_class: icon_classes)
- else
- %span.gl-display-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') }
- = sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
+ %span.gl-display-inline-block.has-tooltip{ title: ssh_key_expiration_tooltip(key) }
+ = sprite_icon('warning-solid', css_class: icon_classes)
%span.gl-text-truncate.gl-sm-ml-3
= key.fingerprint
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 0bea70be837..c8a5908018d 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -49,7 +49,7 @@
= render_if_exists 'projects/commits/mirror_status'
-.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json) } }
+.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json), default_branch: @project.default_branch } }
- if can?(current_user, :admin_project, @project)
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
.row-content-block
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
deleted file mode 100644
index d96eea77366..00000000000
--- a/app/views/shared/_delete_label_modal.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name }
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
-
- .modal-body
- %p
- = html_escape(_('%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}')) % { label_name: tag.strong(label.name), subject_name: label.subject_name, span_open: '<span>'.html_safe, span_close: '</span>'.html_safe }
-
- .modal-footer
- %a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }= _('Cancel')
-
- = link_to _('Delete label'),
- label.destroy_path,
- title: _('Delete'),
- method: :delete,
- class: 'gl-button btn btn-danger'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 096350b8e35..45aca63d83d 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -36,10 +36,10 @@
label_text_color: label.text_color,
group_name: label.project.group.name } }
= _('Promote to group label')
- - if can?(current_user, :admin_label, label)
- %li
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- %button.text-danger.remove-row{ type: 'button' }= _('Delete')
+ %li
+ %span
+ %button.text-danger.remove-row.js-delete-label-modal-button{ type: 'button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }
+ = _('Delete')
- if current_user
%li.inline.label-subscription
- if label.can_subscribe_to_label_in_different_levels?
@@ -61,5 +61,3 @@
- else
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
-
-= render 'shared/delete_label_modal', label: label
diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml
new file mode 100644
index 00000000000..50586598c34
--- /dev/null
+++ b/app/views/shared/nav/_sidebar.html.haml
@@ -0,0 +1,10 @@
+%aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label }
+ .nav-sidebar-inner-scroll
+ - if sidebar.render_raw_scope_menu_partial
+ = render sidebar.render_raw_scope_menu_partial
+
+ %ul.sidebar-top-level-items.qa-project-sidebar
+ - if sidebar.render_raw_menus_partial
+ = render sidebar.render_raw_menus_partial
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/workers/concerns/each_shard_worker.rb b/app/workers/concerns/each_shard_worker.rb
index 00f589f957e..d1d558f55fe 100644
--- a/app/workers/concerns/each_shard_worker.rb
+++ b/app/workers/concerns/each_shard_worker.rb
@@ -24,7 +24,13 @@ module EachShardWorker
end
def healthy_ready_shards
- ready_shards.select(&:success)
+ success_checks, failed_checks = ready_shards.partition(&:success)
+
+ if failed_checks.any?
+ ::Gitlab::AppLogger.error(message: 'Excluding unhealthy shards', failed_checks: failed_checks.map(&:payload), class: self.class.name)
+ end
+
+ success_checks
end
def ready_shards
diff --git a/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml
new file mode 100644
index 00000000000..e4b6e55bb11
--- /dev/null
+++ b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Make blobs directly accessible through the graphql repository
+merge_request: 58677
+author:
+type: added
diff --git a/changelogs/unreleased/324100-update-default-initial-branch-name.yml b/changelogs/unreleased/324100-update-default-initial-branch-name.yml
new file mode 100644
index 00000000000..3850f19f977
--- /dev/null
+++ b/changelogs/unreleased/324100-update-default-initial-branch-name.yml
@@ -0,0 +1,5 @@
+---
+title: Update default branch in divergence graph
+merge_request: 58871
+author:
+type: changed
diff --git a/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml b/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml
new file mode 100644
index 00000000000..a9b143103fb
--- /dev/null
+++ b/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Add search functionality to Jira Connect App namespaces
+merge_request: 57669
+author:
+type: added
diff --git a/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml b/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml
new file mode 100644
index 00000000000..bd933ea23d7
--- /dev/null
+++ b/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml
@@ -0,0 +1,5 @@
+---
+title: Fix N+1 queries to find or initialize services
+merge_request: 58879
+author:
+type: performance
diff --git a/57952- Update GIicon in geo_node_header.vue.yml b/changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml
index 7e6595473a0..c2c8b30d981 100644
--- a/57952- Update GIicon in geo_node_header.vue.yml
+++ b/changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml
@@ -3,5 +3,3 @@ title: Update GIicon size in geo_node_header.vue
merge_request: 57952
author: singhanshuman
type: changed
-
-
diff --git a/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml b/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml
new file mode 100644
index 00000000000..73297b5da34
--- /dev/null
+++ b/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Fix loading pipelines by commit SHA for GraphQL
+merge_request: 59110
+author:
+type: fixed
diff --git a/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml b/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml
new file mode 100644
index 00000000000..ccca0f16adc
--- /dev/null
+++ b/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure a project iid is set before transitioning on pipeline error
+merge_request: 57783
+author:
+type: performance
diff --git a/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml b/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml
new file mode 100644
index 00000000000..22bb210931f
--- /dev/null
+++ b/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml
@@ -0,0 +1,5 @@
+---
+title: Expose timelogs against issues and merge requests in GraphQL
+merge_request: 57321
+author: Lee Tickett @leetickett
+type: added
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml
new file mode 100644
index 00000000000..cefa8109984
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/lib/gitlab/hook_data
+merge_request: 58262
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml
new file mode 100644
index 00000000000..02326d55121
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/groups
+merge_request: 58423
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml
new file mode 100644
index 00000000000..32bb78d6d1c
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/ide
+merge_request: 58424
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml
new file mode 100644
index 00000000000..c7874529642
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/issues
+merge_request: 58425
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml b/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml
new file mode 100644
index 00000000000..db9daca248e
--- /dev/null
+++ b/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml
@@ -0,0 +1,5 @@
+---
+title: Include project and build ID in Pages tmp directory
+merge_request: 59106
+author:
+type: changed
diff --git a/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml b/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml
new file mode 100644
index 00000000000..129f725c6cc
--- /dev/null
+++ b/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml
@@ -0,0 +1,8 @@
+---
+name: ci_pipeline_ensure_iid_on_drop
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57783
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326886
+milestone: '13.11'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 36774757ed3..6003536010e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2905,6 +2905,7 @@ Relationship between an epic and an issue.
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
+| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
| `title` | [`String!`](#string) | Title of the issue. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
@@ -3468,6 +3469,7 @@ An edge in a connection.
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
+| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
| `title` | [`String!`](#string) | Title of the issue. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
@@ -3980,6 +3982,7 @@ An edge in a connection.
| `targetProjectId` | [`Int!`](#int) | ID of the merge request target project. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Completion status of tasks. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the merge request. |
+| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the merge request. |
| `title` | [`String!`](#string) | Title of the merge request. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the merge request. |
@@ -5440,6 +5443,7 @@ Autogenerated return type of RepositionImageDiffNote.
| Field | Type | Description |
| ----- | ---- | ----------- |
+| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. |
| `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. |
| `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. |
| `rootRef` | [`String`](#string) | Default branch of the repository. |
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index d7271df1694..f995f62f87b 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -12,7 +12,17 @@ module Gitlab
end
pipeline.add_error_message(message)
- pipeline.drop!(drop_reason) if drop_reason && persist_pipeline?
+
+ if drop_reason && persist_pipeline?
+ if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml)
+ # Project iid must be called outside a transaction, so we ensure it is set here
+ # otherwise it may be set within the state transition transaction of the drop! call
+ # which it will lock the InternalId row for the whole transaction
+ pipeline.ensure_project_iid!
+ end
+
+ pipeline.drop!(drop_reason)
+ end
# TODO: consider not to rely on AR errors directly as they can be
# polluted with other unrelated errors (e.g. state machine)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 326955837f7..4ec08243c5c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -623,9 +623,6 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
-msgid "%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}"
-msgstr ""
-
msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
msgstr ""
@@ -10229,7 +10226,7 @@ msgstr ""
msgid "Delete label"
msgstr ""
-msgid "Delete label: %{label_name} ?"
+msgid "Delete label: %{labelName}"
msgstr ""
msgid "Delete pipeline"
@@ -23895,6 +23892,9 @@ msgstr ""
msgid "Profiles|Enter your name, so people you know can recognize you"
msgstr ""
+msgid "Profiles|Expired key is not valid."
+msgstr ""
+
msgid "Profiles|Expires at"
msgstr ""
@@ -23925,6 +23925,9 @@ msgstr ""
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
msgstr ""
+msgid "Profiles|Invalid key."
+msgstr ""
+
msgid "Profiles|Invalid password"
msgstr ""
@@ -23934,6 +23937,15 @@ msgstr ""
msgid "Profiles|Key"
msgstr ""
+msgid "Profiles|Key can still be used after expiration."
+msgstr ""
+
+msgid "Profiles|Key usable beyond expiration date."
+msgstr ""
+
+msgid "Profiles|Key will be deleted on this date."
+msgstr ""
+
msgid "Profiles|Last used:"
msgstr ""
@@ -23979,6 +23991,9 @@ msgstr ""
msgid "Profiles|Public email"
msgstr ""
+msgid "Profiles|Publicly visible private SSH keys can compromise your system."
+msgstr ""
+
msgid "Profiles|Remove avatar"
msgstr ""
@@ -24003,9 +24018,6 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
-msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible."
-msgstr ""
-
msgid "Profiles|This email will be displayed on your public profile"
msgstr ""
@@ -24093,9 +24105,6 @@ msgstr ""
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
msgstr ""
-msgid "Profiles|Your key has expired"
-msgstr ""
-
msgid "Profiles|Your location was automatically set based on your %{provider_label} account"
msgstr ""
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index 16c66ea5761..d9fae3db23f 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -13,7 +13,7 @@ module QA
include SubMenus::Settings
include SubMenus::Packages
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :activity_link
element :merge_requests_link
element :snippets_link
diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb
index 9405ea97fff..398712c04d2 100644
--- a/qa/qa/page/project/sub_menus/ci_cd.rb
+++ b/qa/qa/page/project/sub_menus/ci_cd.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :link_pipelines
end
end
diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb
index 124faf0d346..384af3fb53e 100644
--- a/qa/qa/page/project/sub_menus/issues.rb
+++ b/qa/qa/page/project/sub_menus/issues.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :issue_boards_link
element :issues_item
element :labels_link
diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb
index 042994062c7..af716d1af0d 100644
--- a/qa/qa/page/project/sub_menus/operations.rb
+++ b/qa/qa/page/project/sub_menus/operations.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :operations_link
element :operations_environments_link
element :operations_metrics_link
diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb
index 4af640301b9..8a648279919 100644
--- a/qa/qa/page/project/sub_menus/project.rb
+++ b/qa/qa/page/project/sub_menus/project.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :project_link
end
end
diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb
index c78c7521b64..38fa57eacc8 100644
--- a/qa/qa/page/project/sub_menus/repository.rb
+++ b/qa/qa/page/project/sub_menus/repository.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :repository_link
element :branches_link
element :tags_link
diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb
index df16c329246..531c4686345 100644
--- a/qa/qa/page/project/sub_menus/settings.rb
+++ b/qa/qa/page/project/sub_menus/settings.rb
@@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
- view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :settings_item
element :general_settings_link
element :integrations_settings_link
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index 5d34acc635d..204cb808c8e 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -1,11 +1,22 @@
# frozen_string_literal: true
-# Read about factories at https://github.com/thoughtbot/factory_bot
-
FactoryBot.define do
factory :timelog do
time_spent { 3600 }
- issue
- user { issue.project.creator }
+ for_issue
+
+ factory :issue_timelog, traits: [:for_issue]
+ factory :merge_request_timelog, traits: [:for_merge_request]
+
+ trait :for_issue do
+ issue
+ user { issue.author }
+ end
+
+ trait :for_merge_request do
+ merge_request
+ issue { nil }
+ user { merge_request.author }
+ end
end
end
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
index 217f86b92cf..4ca9fe7488a 100644
--- a/spec/features/projects/labels/user_removes_labels_spec.rb
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -18,17 +18,17 @@ RSpec.describe "User removes labels" do
visit(project_labels_path(project))
end
- it "removes label" do
+ it "removes label", :js do
page.within(".other-labels") do
page.first(".label-list-item") do
first('.js-label-options-dropdown').click
first(".remove-row").click
end
+ end
- expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
+ expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
- first(:link, "Delete label").click
- end
+ first(:link, "Delete label").click
expect(page).to have_content("Label was removed").and have_no_content(label.title)
end
diff --git a/spec/frontend/delete_label_modal_spec.js b/spec/frontend/delete_label_modal_spec.js
new file mode 100644
index 00000000000..df70d3a8393
--- /dev/null
+++ b/spec/frontend/delete_label_modal_spec.js
@@ -0,0 +1,83 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import initDeleteLabelModal from '~/delete_label_modal';
+
+describe('DeleteLabelModal', () => {
+ const buttons = [
+ {
+ labelName: 'label 1',
+ subjectName: 'GitLab Org',
+ destroyPath: `${TEST_HOST}/1`,
+ },
+ {
+ labelName: 'label 2',
+ subjectName: 'GitLab Org',
+ destroyPath: `${TEST_HOST}/2`,
+ },
+ ];
+
+ beforeEach(() => {
+ const buttonContainer = document.createElement('div');
+
+ buttons.forEach((x) => {
+ const button = document.createElement('button');
+ button.setAttribute('class', 'js-delete-label-modal-button');
+ button.setAttribute('data-label-name', x.labelName);
+ button.setAttribute('data-subject-name', x.subjectName);
+ button.setAttribute('data-destroy-path', x.destroyPath);
+ button.innerHTML = 'Action';
+ buttonContainer.appendChild(button);
+ });
+
+ document.body.appendChild(buttonContainer);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ const findJsHooks = () => document.querySelectorAll('.js-delete-label-modal-button');
+ const findModal = () => document.querySelector('.gl-modal');
+
+ it('starts with only js-containers', () => {
+ expect(findJsHooks()).toHaveLength(buttons.length);
+ expect(findModal()).not.toExist();
+ });
+
+ describe('when first button clicked', () => {
+ beforeEach(() => {
+ initDeleteLabelModal();
+ findJsHooks().item(0).click();
+ });
+
+ it('does not replace js-containers with GlModal', () => {
+ expect(findJsHooks()).toHaveLength(buttons.length);
+ });
+
+ it('renders GlModal', () => {
+ expect(findModal()).toExist();
+ });
+ });
+
+ describe.each`
+ index
+ ${0}
+ ${1}
+ `(`when multiple buttons exist`, ({ index }) => {
+ beforeEach(() => {
+ initDeleteLabelModal();
+ findJsHooks().item(index).click();
+ });
+
+ it('correct props are passed to gl-modal', () => {
+ expect(findModal().querySelector('.modal-title').innerHTML).toContain(
+ buttons[index].labelName,
+ );
+ expect(findModal().querySelector('.modal-body').innerHTML).toContain(
+ buttons[index].subjectName,
+ );
+ expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain(
+ buttons[index].destroyPath,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js
index 5c645eccc0e..04f595ba23f 100644
--- a/spec/frontend/jira_connect/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_spec.js
@@ -1,7 +1,7 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-
import { fetchGroups } from '~/jira_connect/api';
import GroupsList from '~/jira_connect/components/groups_list.vue';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
@@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => {
fetchGroups: jest.fn(),
};
});
+
+const mockGroupsPath = '/groups';
+
describe('GroupsList', () => {
let wrapper;
const mockEmptyResponse = { data: [] };
const createComponent = (options = {}) => {
- wrapper = shallowMount(GroupsList, {
- ...options,
- });
+ wrapper = extendedWrapper(
+ shallowMount(GroupsList, {
+ provide: {
+ groupsPath: mockGroupsPath,
+ },
+ ...options,
+ }),
+ );
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const findGlAlert = () => wrapper.find(GlAlert);
@@ -33,56 +40,72 @@ describe('GroupsList', () => {
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
const findSecondItem = () => findAllItems().at(1);
+ const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findGroupsList = () => wrapper.findByTestId('groups-list');
- describe('isLoading is true', () => {
+ describe('when groups are loading', () => {
it('renders loading icon', async () => {
- fetchGroups.mockResolvedValue(mockEmptyResponse);
+ fetchGroups.mockReturnValue(new Promise(() => {}));
createComponent();
- wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
});
- describe('error fetching groups', () => {
+ describe('when groups fetch fails', () => {
it('renders error message', async () => {
fetchGroups.mockRejectedValue();
createComponent();
await waitForPromises();
+ expect(findGlLoadingIcon().exists()).toBe(false);
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
});
});
- describe('no groups returned', () => {
+ describe('with no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
createComponent();
await waitForPromises();
+ expect(findGlLoadingIcon().exists()).toBe(false);
expect(wrapper.text()).toContain('No available namespaces');
});
});
describe('with groups returned', () => {
beforeEach(async () => {
- fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
+ fetchGroups.mockResolvedValue({
+ headers: { 'X-PAGE': 1, 'X-TOTAL': 2 },
+ data: [mockGroup1, mockGroup2],
+ });
createComponent();
await waitForPromises();
});
it('renders groups list', () => {
- expect(findAllItems().length).toBe(2);
+ expect(findAllItems()).toHaveLength(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
+ it('sets GroupListItem `disabled` prop to `false`', () => {
+ findAllItems().wrappers.forEach((groupListItem) => {
+ expect(groupListItem.props('disabled')).toBe(false);
+ });
+ });
+
+ it('does not set opacity of the groups list', () => {
+ expect(findGroupsList().classes()).not.toContain('gl-opacity-5');
+ });
+
it('shows error message on $emit from item', async () => {
const errorMessage = 'error message';
@@ -93,5 +116,55 @@ describe('GroupsList', () => {
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toContain(errorMessage);
});
+
+ describe('when searching groups', () => {
+ const mockSearchTeam = 'mock search term';
+
+ describe('while groups are loading', () => {
+ beforeEach(async () => {
+ fetchGroups.mockClear();
+ fetchGroups.mockReturnValue(new Promise(() => {}));
+
+ findSearchBox().vm.$emit('input', mockSearchTeam);
+ await wrapper.vm.$nextTick();
+ });
+
+ it('calls `fetchGroups` with search term', () => {
+ expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
+ page: 1,
+ perPage: 10,
+ search: mockSearchTeam,
+ });
+ });
+
+ it('disables GroupListItems', async () => {
+ findAllItems().wrappers.forEach((groupListItem) => {
+ expect(groupListItem.props('disabled')).toBe(true);
+ });
+ });
+
+ it('sets opacity of the groups list', () => {
+ expect(findGroupsList().classes()).toContain('gl-opacity-5');
+ });
+
+ it('sets loading prop of ths search box', () => {
+ expect(findSearchBox().props('isLoading')).toBe(true);
+ });
+ });
+
+ describe('when group search finishes loading', () => {
+ beforeEach(async () => {
+ fetchGroups.mockResolvedValue({ data: [mockGroup1] });
+ findSearchBox().vm.$emit('input');
+
+ await waitForPromises();
+ });
+
+ it('renders new groups list', () => {
+ expect(findAllItems()).toHaveLength(1);
+ expect(findFirstItem().props('group')).toBe(mockGroup1);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/delete_label_modal_spec.js b/spec/frontend/vue_shared/components/delete_label_modal_spec.js
new file mode 100644
index 00000000000..3905690dab4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/delete_label_modal_spec.js
@@ -0,0 +1,64 @@
+import { GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import { TEST_HOST } from 'helpers/test_constants';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
+
+const MOCK_MODAL_DATA = {
+ labelName: 'label 1',
+ subjectName: 'GitLab Org',
+ destroyPath: `${TEST_HOST}/1`,
+};
+
+describe('vue_shared/components/delete_label_modal', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = extendedWrapper(
+ mount(DeleteLabelModal, {
+ propsData: {
+ selector: '.js-test-btn',
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findModal = () => wrapper.find(GlModal);
+ const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
+
+ describe('template', () => {
+ describe('when modal data is set', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.labelName = MOCK_MODAL_DATA.labelName;
+ wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName;
+ wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath;
+ });
+
+ it('renders GlModal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('displays the label name and subject name', () => {
+ expect(findModal().text()).toContain(
+ `${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`,
+ );
+ });
+
+ it('passes the destroyPath to the button', () => {
+ expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath);
+ });
+ });
+ });
+});
diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb
new file mode 100644
index 00000000000..bc0344796ee
--- /dev/null
+++ b/spec/graphql/resolvers/blobs_resolver_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::BlobsResolver do
+ include GraphqlHelpers
+
+ describe '.resolver_complexity' do
+ it 'adds one per path being resolved' do
+ control = described_class.resolver_complexity({}, child_complexity: 1)
+
+ expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1))
+ .to eq(control + 3)
+ end
+ end
+
+ describe '#resolve' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:repository) { project.repository }
+ let(:args) { { paths: paths, ref: ref } }
+ let(:paths) { [] }
+ let(:ref) { nil }
+
+ subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) }
+
+ context 'when unauthorized' do
+ it 'raises an exception' do
+ expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'using no filter' do
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'using paths filter' do
+ let(:paths) { ['README.md'] }
+
+ it 'returns the specified blobs for HEAD' do
+ is_expected.to contain_exactly(have_attributes(path: 'README.md'))
+ end
+
+ context 'specifying a non-existent blob' do
+ let(:paths) { ['non-existent'] }
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'specifying a different ref' do
+ let(:ref) { 'add-pdf-file' }
+ let(:paths) { ['files/pdf/test.pdf', 'README.md'] }
+
+ it 'returns the specified blobs for that ref' do
+ is_expected.to contain_exactly(
+ have_attributes(path: 'files/pdf/test.pdf'),
+ have_attributes(path: 'README.md')
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
index 69127c4b061..3d33e0b500d 100644
--- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') }
+ let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') }
let_it_be(:other_pipeline) { create(:ci_pipeline) }
let(:current_user) { create(:user) }
@@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed iid' do
+ expect(Ci::PipelinesFinder)
+ .to receive(:new)
+ .with(project, current_user, iids: ['1234'])
+ .and_call_original
+
result = batch_sync do
resolve_pipeline(project, { iid: '1234' })
end
@@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed sha' do
+ expect(Ci::PipelinesFinder)
+ .to receive(:new)
+ .with(project, current_user, sha: ['sha'])
+ .and_call_original
+
result = batch_sync do
resolve_pipeline(project, { sha: 'sha' })
end
@@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for iid' do
- create(:ci_pipeline, project: project, iid: '1235')
-
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
end
@@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for sha' do
- create(:ci_pipeline, project: project, sha: 'sha2')
-
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { sha: 'sha' }) }
end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
index bebae4c68cf..585cd657e35 100644
--- a/spec/graphql/resolvers/timelog_resolver_spec.rb
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -9,51 +9,57 @@ RSpec.describe Resolvers::TimelogResolver do
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
end
- context "within a group" do
+ context "with a group" do
let_it_be(:current_user) { create(:user) }
- let(:group) { create(:group) }
- let(:project) { create(:project, :public, group: group) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
- before do
+ before_all do
group.add_developer(current_user)
project.add_developer(current_user)
end
+ before do
+ group.clear_memoization(:timelogs)
+ end
+
describe '#resolve' do
- let(:issue) { create(:issue, project: project) }
- let(:issue2) { create(:issue, project: project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project) }
+ let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
+ let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
+ let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
+
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
- let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
- let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
- let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) }
it 'finds all timelogs within given dates' do
- timelogs = resolve_timelogs(args)
+ timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1)
end
it 'return nothing when user has insufficient permissions' do
+ user = create(:user)
group.add_guest(current_user)
- expect(resolve_timelogs(args)).to be_empty
+ expect(resolve_timelogs(user: user, **args)).to be_empty
end
context 'when start_time and end_date are present' do
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
it 'finds timelogs until the end of day of end_date' do
- timelogs = resolve_timelogs(args)
+ timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
- context 'finds timelogs until the time specified on end_time' do
+ context 'when start_date and end_time are present' do
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
it 'finds all timelogs within start_date and end_time' do
- timelogs = resolve_timelogs(args)
+ timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1)
end
@@ -66,7 +72,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { {} }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start and End arguments must be present/)
end
end
@@ -75,7 +81,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -84,7 +90,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -93,7 +99,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: 6.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -102,7 +108,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_date: 2.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -111,7 +117,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -120,7 +126,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@@ -129,7 +135,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Only Time or Date arguments must be present/)
end
end
@@ -138,7 +144,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
@@ -147,7 +153,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
it 'returns correct error' do
- expect {resolve_timelogs(args)}
+ expect { resolve_timelogs(**args) }
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
end
end
@@ -155,7 +161,8 @@ RSpec.describe Resolvers::TimelogResolver do
end
end
- def resolve_timelogs(args = {}, context = { current_user: current_user })
+ def resolve_timelogs(user: current_user, **args)
+ context = { current_user: user }
resolve(described_class, obj: group, args: args, ctx: context)
end
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 21fc530149c..6908a610aae 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
- create_note_email]
+ create_note_email timelogs]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index e9199bd286e..a3bb7e502f2 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -12,4 +12,6 @@ RSpec.describe GitlabSchema.types['Repository'] do
specify { expect(described_class).to have_graphql_field(:tree) }
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
+
+ specify { expect(described_class).to have_graphql_field(:blobs) }
end
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index 9687d038162..2ea832f95dc 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -112,6 +112,46 @@ RSpec.describe ProfilesHelper do
end
end
+ describe "#ssh_key_expiration_tooltip" do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
+ end
+
+ error_message = 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
+
+ where(:error, :expired, :result) do
+ false | false | nil
+ true | false | error_message
+ false | true | 'Key usable beyond expiration date.'
+ true | true | error_message
+ end
+
+ with_them do
+ let_it_be(:key) do
+ build(:personal_key)
+ end
+
+ it do
+ key.expires_at = expired ? 2.days.ago : 2.days.from_now
+ key.errors.add(:base, error_message) if error
+
+ expect(helper.ssh_key_expiration_tooltip(key)).to eq(result)
+ end
+ end
+ end
+
+ describe "#ssh_key_expires_field_description" do
+ before do
+ allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
+ end
+
+ it 'returns the description' do
+ expect(helper.ssh_key_expires_field_description).to eq('Key can still be used after expiration.')
+ end
+ end
+
def stub_cas_omniauth_provider
provider = OpenStruct.new(
'name' => 'cas3',
diff --git a/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb
new file mode 100644
index 00000000000..72a5bf8d011
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do
+ let(:helper_class) do
+ Class.new do
+ include Gitlab::Ci::Pipeline::Chain::Helpers
+
+ attr_accessor :pipeline, :command
+
+ def initialize(pipeline, command)
+ self.pipeline = pipeline
+ self.command = command
+ end
+ end
+ end
+
+ subject(:helper) { helper_class.new(pipeline, command) }
+
+ let(:pipeline) { build(:ci_empty_pipeline) }
+ let(:command) { double(save_incompleted: true) }
+ let(:message) { 'message' }
+
+ describe '.error' do
+ shared_examples 'error function' do
+ specify do
+ expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original
+ expect(pipeline).to receive(:add_error_message).with(message).and_call_original
+ expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original
+
+ subject.error(message, config_error: config_error, drop_reason: drop_reason)
+
+ expect(pipeline.yaml_errors).to eq(yaml_error)
+ expect(pipeline.errors[:base]).to include(message)
+ end
+ end
+
+ context 'when given a drop reason' do
+ context 'when config error is true' do
+ context 'sets the yaml error and overrides the drop reason' do
+ let(:drop_reason) { :config_error }
+ let(:config_error) { true }
+ let(:yaml_error) { message }
+
+ it_behaves_like "error function"
+ end
+ end
+
+ context 'when config error is false' do
+ context 'does not set the yaml error or override the drop reason' do
+ let(:drop_reason) { :size_limit_exceeded }
+ let(:config_error) { false }
+ let(:yaml_error) { nil }
+
+ it_behaves_like "error function"
+ end
+ end
+ end
+
+ context 'when the ci_pipeline_ensure_iid_on_drop feature flag is false' do
+ it 'does not ensure the project iid' do
+ stub_feature_flags(ci_pipeline_ensure_iid_on_drop: false)
+ expect(pipeline).to receive(:ensure_project_iid!).once
+
+ subject.error(message, config_error: true)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 1553a989dba..b735ac7940b 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -407,13 +407,13 @@ RSpec.describe Gitlab::Database do
expect(described_class.db_read_only?).to be_truthy
end
- it 'detects a read write database' do
+ it 'detects a read-write database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
expect(described_class.db_read_only?).to be_falsey
end
- it 'detects a read write database' do
+ it 'detects a read-write database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }])
expect(described_class.db_read_only?).to be_falsey
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 8a2395d70b2..8f898d898de 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::IssueBuilder do
let_it_be(:label) { create(:label) }
let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) }
+
let(:builder) { described_class.new(issue) }
describe '#build' do
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index fede7f273f1..0339faa9fcf 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::MergeRequestBuilder do
let_it_be(:merge_request) { create(:merge_request) }
+
let(:builder) { described_class.new(merge_request) }
describe '#build' do
diff --git a/spec/lib/gitlab/hook_data/release_builder_spec.rb b/spec/lib/gitlab/hook_data/release_builder_spec.rb
index b630780b162..449965f5df1 100644
--- a/spec/lib/gitlab/hook_data/release_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/release_builder_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::ReleaseBuilder do
let_it_be(:project) { create(:project, :public, :repository) }
+
let(:release) { create(:release, project: project) }
let(:builder) { described_class.new(release) }
diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb
index 34d2000f96a..f694fc350ee 100644
--- a/spec/models/concerns/has_timelogs_report_spec.rb
+++ b/spec/models/concerns/has_timelogs_report_spec.rb
@@ -32,18 +32,16 @@ RSpec.describe HasTimelogsReport do
end
describe '#user_can_access_group_timelogs?' do
- before do
+ it 'returns true if user can access group timelogs' do
group.add_developer(user)
- end
- it 'returns true if user can access group timelogs' do
- expect(group.user_can_access_group_timelogs?(user)).to be_truthy
+ expect(group).to be_user_can_access_group_timelogs(user)
end
it 'returns false if user has insufficient permissions' do
group.add_guest(user)
- expect(group.user_can_access_group_timelogs?(user)).to be_falsey
+ expect(group).not_to be_user_can_access_group_timelogs(user)
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b964a18e148..991691a408a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5795,16 +5795,34 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#find_or_initialize_services' do
- before do
- allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
- allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
+ let_it_be(:subject) { create(:project) }
+
+ it 'avoids N+1 database queries' do
+ control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }.count
+
+ expect(control_count).to be <= 4
end
- it 'returns only enabled services' do
- services = subject.find_or_initialize_services
+ it 'avoids N+1 database queries with more available services' do
+ allow(Service).to receive(:available_services_names).and_return(%w[pushover])
+ control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }
- expect(services.count).to eq(2)
- expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
+ allow(Service).to receive(:available_services_names).and_call_original
+ expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count)
+ end
+
+ context 'with disabled services' do
+ before do
+ allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
+ allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
+ end
+
+ it 'returns only enabled services sorted' do
+ services = subject.find_or_initialize_services
+
+ expect(services.size).to eq(2)
+ expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
+ end
end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index e9019b55635..6a252b444f9 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -56,9 +56,9 @@ RSpec.describe Timelog do
group = create(:group)
subgroup = create(:group, parent: group)
- create(:timelog, issue: create(:issue, project: create(:project)))
- timelog1 = create(:timelog, issue: create(:issue, project: create(:project, group: group)))
- timelog2 = create(:timelog, issue: create(:issue, project: create(:project, group: subgroup)))
+ create(:issue_timelog)
+ timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group)))
+ timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup)))
expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
end
@@ -66,9 +66,9 @@ RSpec.describe Timelog do
describe 'between_times' do
it 'returns collection of timelogs within given times' do
- create(:timelog, spent_at: 65.days.ago)
- timelog1 = create(:timelog, spent_at: 15.days.ago)
- timelog2 = create(:timelog, spent_at: 5.days.ago)
+ create(:issue_timelog, spent_at: 65.days.ago)
+ timelog1 = create(:issue_timelog, spent_at: 15.days.ago)
+ timelog2 = create(:issue_timelog, spent_at: 5.days.ago)
timelogs = described_class.between_times(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2)
diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb
index ca015a82148..6e21a73afa9 100644
--- a/spec/requests/api/graphql/group/timelogs_spec.rb
+++ b/spec/requests/api/graphql/group/timelogs_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
+
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
before do
@@ -34,11 +35,11 @@ RSpec.describe 'Timelogs through GroupQuery' do
end
it 'contains correct data', :aggregate_failures do
- username = timelog_array.map {|data| data['user']['username'] }
+ username = timelog_array.map { |data| data['user']['username'] }
spent_at = timelog_array.map { |data| data['spentAt'].to_time }
time_spent = timelog_array.map { |data| data['timeSpent'] }
- issue_title = timelog_array.map {|data| data['issue']['title'] }
- milestone_title = timelog_array.map {|data| data['issue']['milestone']['title'] }
+ issue_title = timelog_array.map { |data| data['issue']['title'] }
+ milestone_title = timelog_array.map { |data| data['issue']['milestone']['title'] }
expect(username).to eq([user.username])
expect(spent_at.first).to be_like_time(timelog1.spent_at)
@@ -50,7 +51,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
context 'when arguments with no time are present' do
let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
- let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }}
+ let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' } }
it 'sets times as start of day and end of day' do
expect(response).to have_gitlab_http_status(:ok)
@@ -111,12 +112,10 @@ RSpec.describe 'Timelogs through GroupQuery' do
}
NODE
- graphql_query_for("group", { "fullPath" => group.full_path },
- [query_graphql_field(
- "timelogs",
- timelog_params,
- timelog_nodes
- )]
+ graphql_query_for(
+ :group,
+ { full_path: group.full_path },
+ query_graphql_field(:timelogs, timelog_params, timelog_nodes)
)
end
end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 9c915075c42..f93822825e0 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -5,14 +5,14 @@ require 'spec_helper'
RSpec.describe 'getting an issue list for a project' do
include GraphqlHelpers
- let(:issues_data) { graphql_data['project']['issues']['edges'] }
-
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
+ let(:issues_data) { graphql_data['project']['issues']['edges'] }
+
let(:fields) do
<<~QUERY
edges {
@@ -76,7 +76,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'no limit is provided' do
+ context 'when no limit is provided' do
let(:issue_limit) { nil }
it 'returns all issues' do
@@ -143,13 +143,15 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:data_path) { [:project, :issues] }
def pagination_query(params)
- graphql_query_for(:project, { full_path: sort_project.full_path },
+ graphql_query_for(
+ :project,
+ { full_path: sort_project.full_path },
query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
)
end
def pagination_results_data(data)
- data.map { |issue| issue.dig('iid').to_i }
+ data.map { |issue| issue['iid'].to_i }
end
context 'when sorting by due date' do
@@ -189,27 +191,38 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
- let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
+ let(:expected_results) do
+ [
+ relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
+ relative_issue4.iid, relative_issue2.iid
+ ]
+ end
end
end
end
context 'when sorting by priority' do
let_it_be(:sort_project) { create(:project, :public) }
- let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
- let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) }
- let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) }
- let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) }
- let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) }
- let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
- let_it_be(:priority_issue4) { create(:issue, project: sort_project) }
+ let_it_be(:on_project) { { project: sort_project } }
+ let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
+ let_it_be(:priority_1) { create(:label, **on_project, priority: 1) }
+ let_it_be(:priority_2) { create(:label, **on_project, priority: 5) }
+ let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) }
+ let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) }
+ let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) }
+ let_it_be(:priority_issue4) { create(:issue, **on_project) }
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_ASC }
let(:first_param) { 2 }
- let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
+ let(:expected_results) do
+ [
+ priority_issue3.iid, priority_issue1.iid,
+ priority_issue2.iid, priority_issue4.iid
+ ]
+ end
end
end
@@ -217,7 +230,9 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_DESC }
let(:first_param) { 2 }
- let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
+ let(:expected_results) do
+ [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid]
+ end
end
end
end
@@ -275,7 +290,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching alert management alert' do
+ context 'when fetching alert management alert' do
let(:fields) do
<<~QUERY
edges {
@@ -297,7 +312,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- create(:alert_management_alert, :with_issue, project: project )
+ create(:alert_management_alert, :with_issue, project: project)
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
end
@@ -312,7 +327,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching labels' do
+ context 'when fetching labels' do
let(:fields) do
<<~QUERY
edges {
@@ -362,7 +377,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching assignees' do
+ context 'when fetching assignees' do
let(:fields) do
<<~QUERY
edges {
@@ -420,9 +435,10 @@ RSpec.describe 'getting an issue list for a project' do
query = graphql_query_for(
:project,
{ full_path: project.full_path },
- query_graphql_field(:issues, search_params, [
+ query_graphql_field(
+ :issues, search_params,
query_graphql_field(:nodes, nil, requested_fields)
- ])
+ )
)
post_graphql(query, current_user: current_user)
end
@@ -448,5 +464,16 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check'
end
+
+ context 'when requesting `timelogs`' do
+ let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
+
+ before do
+ create_list(:issue_timelog, 2, issue: issue_a)
+ create(:issue_timelog, issue: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index e07e2428f33..7fc1ef05fa7 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -299,6 +299,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
reviewers { nodes { username } }
participants { nodes { username } }
headPipeline { status }
+ timelogs { nodes { timeSpent } }
SELECT
end
@@ -307,7 +308,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
query($first: Int) {
project(fullPath: "#{project.full_path}") {
mergeRequests(first: $first) {
- nodes { #{mr_fields} }
+ nodes { iid #{mr_fields} }
}
}
}
@@ -324,6 +325,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
mr.assignees << current_user
mr.reviewers << create(:user)
mr.reviewers << current_user
+ mr.timelogs << create(:merge_request_timelog, merge_request: mr)
end
end
@@ -345,7 +347,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
def user_collection
- { 'nodes' => all(match(a_hash_including('username' => be_present))) }
+ { 'nodes' => be_present.and(all(match(a_hash_including('username' => be_present)))) }
end
it 'returns appropriate results' do
@@ -358,7 +360,8 @@ RSpec.describe 'getting merge request listings nested in a project' do
'assignees' => user_collection,
'reviewers' => user_collection,
'participants' => user_collection,
- 'headPipeline' => { 'status' => be_present }
+ 'headPipeline' => { 'status' => be_present },
+ 'timelogs' => { 'nodes' => be_one }
)))
end
diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb
index 3d89ee96823..486a99dd8df 100644
--- a/spec/services/groups/auto_devops_service_spec.rb
+++ b/spec/services/groups/auto_devops_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Groups::AutoDevopsService, '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
+
let(:group_params) { { auto_devops_enabled: '0' } }
let(:service) { described_class.new(group, user, group_params) }
diff --git a/spec/services/groups/group_links/update_service_spec.rb b/spec/services/groups/group_links/update_service_spec.rb
index 436cdf89a0f..82c4a10f15a 100644
--- a/spec/services/groups/group_links/update_service_spec.rb
+++ b/spec/services/groups/group_links/update_service_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do
let_it_be(:group) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: shared_group) }
+
let(:group_member_user) { create(:user) }
let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 19b746ade34..3a1197970f4 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -5,12 +5,14 @@ require 'spec_helper'
RSpec.describe Groups::TransferService do
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
+
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
context 'handling packages' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:new_group) { create(:group, :public) }
+
let(:project) { create(:project, :public, namespace: group) }
before do
@@ -272,6 +274,7 @@ RSpec.describe Groups::TransferService do
context 'with a group integration' do
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
+
let(:new_created_integration) { Service.find_by(group: group) }
context 'with an inherited integration' do
diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb
index e2838c4ce0b..e941958eb8c 100644
--- a/spec/services/groups/update_shared_runners_service_spec.rb
+++ b/spec/services/groups/update_shared_runners_service_spec.rb
@@ -59,6 +59,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do
context 'disable shared Runners' do
let_it_be(:group) { create(:group) }
+
let(:params) { { shared_runners_setting: 'disabled_and_unoverridable' } }
it 'receives correct method and succeeds' do
diff --git a/spec/services/ide/base_config_service_spec.rb b/spec/services/ide/base_config_service_spec.rb
index debdc6e5809..ee57f2c18ec 100644
--- a/spec/services/ide/base_config_service_spec.rb
+++ b/spec/services/ide/base_config_service_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::BaseConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
+
let(:sha) { 'sha' }
describe '#execute' do
diff --git a/spec/services/ide/schemas_config_service_spec.rb b/spec/services/ide/schemas_config_service_spec.rb
index 19e5ca9e87d..69ad9b5cbea 100644
--- a/spec/services/ide/schemas_config_service_spec.rb
+++ b/spec/services/ide/schemas_config_service_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::SchemasConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
+
let(:filename) { 'sample.yml' }
let(:schema_content) { double(body: '{"title":"Sample schema"}') }
diff --git a/spec/services/ide/terminal_config_service_spec.rb b/spec/services/ide/terminal_config_service_spec.rb
index 2bfc8a7ff3c..483b6413be3 100644
--- a/spec/services/ide/terminal_config_service_spec.rb
+++ b/spec/services/ide/terminal_config_service_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::TerminalConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
+
let(:sha) { 'sha' }
describe '#execute' do
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 16433d49ca1..80fe2474ecd 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Issues::BuildService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
+
let(:user) { developer }
before_all do
diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb
index 9ceb4ffeec5..44180a322ca 100644
--- a/spec/services/issues/clone_service_spec.rb
+++ b/spec/services/issues/clone_service_spec.rb
@@ -242,6 +242,7 @@ RSpec.describe Issues::CloneService do
context 'issue with a design', :clean_gitlab_redis_shared_state do
let_it_be(:new_project) { create(:project) }
+
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
let(:subject) { clone_service.execute(old_issue, new_project) }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index d362f4efb7c..2ae1edcc804 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Issues::CreateService do
describe '#execute' do
let_it_be(:assignee) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
+
let(:issue) { described_class.new(project, user, opts).execute }
context 'when params are valid' do
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index d199f825276..d04480bec18 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Issues::ExportCsvService do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:issue) { create(:issue, project: project, author: user) }
let_it_be(:bad_issue) { create(:issue, project: project, author: user) }
+
subject { described_class.new(Issue.all, project) }
it 'renders csv to string' do
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index eb124f07900..2f29a2e2022 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -206,6 +206,7 @@ RSpec.describe Issues::MoveService do
context 'issue with a design', :clean_gitlab_redis_shared_state do
let_it_be(:new_project) { create(:project) }
+
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
let(:subject) { move_service.execute(old_issue, new_project) }
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index a8a1f95e800..c9c029bca4f 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Issues::RelatedBranchesService do
let_it_be(:developer) { create(:user) }
let_it_be(:issue) { create(:issue) }
+
let(:user) { developer }
subject { described_class.new(issue.project, user) }
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 24a2fa74b77..e0d6b9afcff 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -724,9 +724,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'cleans invalid record and logs warning', :aggregate_failures do
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
- allow_next_instance_of(Project) do |instance|
- allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
- end
+ allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) }))
project = create_project(user, opts)
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index afdb4c3115a..71cb2ebdc33 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -55,6 +55,12 @@ RSpec.describe Projects::UpdatePagesService do
end
end
+ it 'creates a temporary directory with the project and build ID' do
+ expect(Dir).to receive(:mktmpdir).with("project-#{project.id}-build-#{build.id}-", anything).and_call_original
+
+ subject.execute
+ end
+
it "doesn't deploy to legacy storage if it's disabled" do
allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
new file mode 100644
index 00000000000..62bb271bd9c
--- /dev/null
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'profiles/keys/_form.html.haml' do
+ let_it_be(:key) { Key.new }
+
+ let(:page) { Capybara::Node::Simple.new(rendered) }
+
+ before do
+ assign(:key, key)
+ end
+
+ context 'when the form partial is used' do
+ before do
+ allow(view).to receive(:ssh_key_expires_field_description).and_return('Key can still be used after expiration.')
+
+ render
+ end
+
+ it 'renders the form with the correct action' do
+ expect(page.find('form')['action']).to eq('/-/profile/keys')
+ end
+
+ it 'has the key field', :aggregate_failures do
+ expect(rendered).to have_field('Key', type: 'textarea', placeholder: 'Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
+ expect(rendered).to have_text("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.")
+ end
+
+ it 'has the title field', :aggregate_failures do
+ expect(rendered).to have_field('Title', type: 'text', placeholder: 'e.g. My MacBook key')
+ expect(rendered).to have_text('Give your individual key a title.')
+ end
+
+ it 'has the expires at field', :aggregate_failures do
+ expect(rendered).to have_field('Expires at', type: 'date')
+ expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
+ expect(rendered).to have_text('Key can still be used after expiration.')
+ end
+
+ it 'has the validation warning', :aggregate_failures do
+ expect(rendered).to have_text("Oops, are you sure? Publicly visible private SSH keys can compromise your system.")
+ expect(rendered).to have_button('Yes, add it')
+ end
+
+ it 'has the submit button' do
+ expect(rendered).to have_button('Add key')
+ end
+ end
+end
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
new file mode 100644
index 00000000000..a29b8ecc3d5
--- /dev/null
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'profiles/keys/_key.html.haml' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:key).and_return(key)
+ allow(view).to receive(:is_admin).and_return(false)
+ end
+
+ context 'when the key partial is used' do
+ let_it_be(:key) do
+ create(:personal_key,
+ user: user,
+ last_used_at: 7.days.ago,
+ expires_at: 2.days.from_now)
+ end
+
+ it 'displays the correct values', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text(key.title)
+ expect(rendered).to have_css('[data-testid="key-icon"]')
+ expect(rendered).to have_text(key.fingerprint)
+ expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
+ expect(rendered).to have_text(l(key.created_at, format: "%b %d, %Y"))
+ expect(rendered).to have_text(key.expires_at.to_date)
+ expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete')
+ end
+
+ context 'when the key has not been used' do
+ let_it_be(:key) do
+ create(:personal_key,
+ user: user,
+ last_used_at: nil)
+ end
+
+ it 'renders "Never" for last used' do
+ render
+
+ expect(rendered).to have_text('Last used: Never')
+ end
+ end
+
+ context 'when the key does not have an expiration date' do
+ let_it_be(:key) do
+ create(:personal_key,
+ user: user,
+ expires_at: nil)
+ end
+
+ it 'renders "Never" for expires' do
+ render
+
+ expect(rendered).to have_text('Expires: Never')
+ end
+ end
+
+ context 'when the key is not deletable' do
+ # Turns out key.can_delete? is only false for LDAP keys
+ # but LDAP keys don't exist outside EE
+ before do
+ allow(key).to receive(:can_delete?).and_return(false)
+ end
+
+ it 'does not render the partial' do
+ render
+
+ expect(response).not_to render_template(partial: 'shared/ssh_keys/_key_delete')
+ end
+ end
+
+ context 'icon tooltip' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:valid, :expiry, :result) do
+ false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
+ false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
+ true | 2.days.ago | 'Key usable beyond expiration date.'
+ true | 2.days.from_now | ''
+ end
+
+ with_them do
+ let_it_be(:key) do
+ create(:personal_key, user: user)
+ end
+
+ it 'renders the correct icon', :aggregate_failures do
+ unless valid
+ stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
+ end
+
+ key.expires_at = expiry
+
+ render
+
+ if result.empty?
+ expect(rendered).to have_css('[data-testid="key-icon"]')
+ else
+ expect(rendered).to have_css('[data-testid="warning-solid-icon"]')
+ expect(rendered).to have_selector("span.has-tooltip[title='#{result}']")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb
index 06e824f1530..b15b7b76b56 100644
--- a/spec/workers/projects/post_creation_worker_spec.rb
+++ b/spec/workers/projects/post_creation_worker_spec.rb
@@ -64,10 +64,7 @@ RSpec.describe Projects::PostCreationWorker do
it 'cleans invalid record and logs warning', :aggregate_failures do
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
-
- allow_next_found_instance_of(Project) do |instance|
- allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
- end
+ allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
subject
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 5e1bc76ec8e..829abc7d895 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -42,5 +42,12 @@ RSpec.describe RepositoryCheck::DispatchWorker do
subject.perform
end
+
+ it 'logs unhealthy shards' do
+ log_data = { message: "Excluding unhealthy shards", failed_checks: [{ labels: { shard: unhealthy_shard_name }, message: '14:Connect Failed', status: 'failed' }], class: described_class.name }
+ expect(Gitlab::AppLogger).to receive(:error).with(a_hash_including(log_data))
+
+ subject.perform
+ end
end
end