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--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue68
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue112
-rw-r--r--app/assets/javascripts/ci/catalog/global_catalog.vue10
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql16
-rw-r--r--app/assets/javascripts/ci/catalog/index.js37
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js2
-rw-r--r--app/assets/javascripts/emoji/index.js25
-rw-r--r--app/assets/javascripts/pages/explore/catalog/index.js3
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue14
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js11
-rw-r--r--app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue11
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue22
-rw-r--r--app/assets/stylesheets/page_bundles/_system_note_styles.scss59
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss59
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss1
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/repositories/git_http_controller.rb7
-rw-r--r--app/finders/data_transfer/mocked_transfer_finder.rb27
-rw-r--r--app/graphql/mutations/ci/catalog/resources/create.rb2
-rw-r--r--app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb16
-rw-r--r--app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb16
-rw-r--r--app/graphql/types/data_transfer/project_data_transfer_type.rb1
-rw-r--r--app/helpers/auth_helper.rb11
-rw-r--r--app/helpers/projects/pipeline_helper.rb1
-rw-r--r--app/services/ci/catalog/add_resource_service.rb41
-rw-r--r--app/services/ci/catalog/resources/create_service.rb31
-rw-r--r--app/services/ci/enqueue_job_service.rb11
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/explore/catalog/show.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml16
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml6
-rw-r--r--config/feature_flags/development/data_transfer_monitoring_mock_data.yml8
-rw-r--r--doc/ci/jobs/index.md3
-rw-r--r--doc/development/development_processes.md44
-rw-r--r--doc/development/documentation/workflow.md7
-rw-r--r--doc/development/internal_analytics/index.md7
-rw-r--r--doc/development/repository_storage_moves/index.md102
-rw-r--r--doc/user/gitlab_duo_chat.md2
-rw-r--r--doc/user/organization/index.md38
-rw-r--r--doc/user/project/merge_requests/index.md1
-rw-r--r--lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb5
-rw-r--r--lib/generators/gitlab/usage_metric_definition_generator.rb12
-rw-r--r--lib/gitlab/instrumentation/redis_base.rb6
-rw-r--r--lib/gitlab/seeders/ci/catalog/resource_seeder.rb2
-rw-r--r--locale/gitlab.pot28
-rw-r--r--qa/qa/page/component/deploy_token.rb72
-rw-r--r--qa/qa/page/group/settings/group_deploy_tokens.rb55
-rw-r--r--qa/qa/page/project/settings/deploy_tokens.rb84
-rw-r--r--qa/qa/page/project/settings/project_deploy_tokens.rb13
-rw-r--r--qa/qa/page/project/settings/repository.rb2
-rw-r--r--qa/qa/resource/group_deploy_token.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb6
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb20
-rw-r--r--spec/features/explore/catalog_spec.rb80
-rw-r--r--spec/finders/data_transfer/mocked_transfer_finder_spec.rb22
-rw-r--r--spec/frontend/__helpers__/emoji.js7
-rw-r--r--spec/frontend/__helpers__/local_storage_helper.js3
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js58
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js37
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js211
-rw-r--r--spec/frontend/ci/catalog/global_catalog_spec.js17
-rw-r--r--spec/frontend/ci/catalog/index_spec.js48
-rw-r--r--spec/frontend/ci/catalog/mock.js18
-rw-r--r--spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js1
-rw-r--r--spec/frontend/emoji/index_spec.js197
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js9
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js12
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js10
-rw-r--r--spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb20
-rw-r--r--spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb22
-rw-r--r--spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb22
-rw-r--r--spec/helpers/auth_helper_spec.rb103
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb6
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb21
-rw-r--r--spec/lib/gitlab/auth/saml/config_spec.rb4
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/data_transfer_spec.rb42
-rw-r--r--spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/data_transfer_spec.rb42
-rw-r--r--spec/services/ci/catalog/resources/create_service_spec.rb (renamed from spec/services/ci/catalog/add_resource_service_spec.rb)16
-rw-r--r--spec/services/ci/enqueue_job_service_spec.rb29
-rw-r--r--spec/support/finder_collection_allowlist.yml1
-rw-r--r--spec/support/helpers/login_helpers.rb10
-rw-r--r--spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb10
91 files changed, 1621 insertions, 698 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9f9ff6e5808..c36525a1fc3 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-9bf28d2089501b82b40e2b9f6ad21cf80751f15f
+c65b631d971809d9e0294356d7892860d4800cf3
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index 4b9fe01e997..b5cb1862b45 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -1,7 +1,15 @@
<script>
-import { GlDisclosureDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlIcon,
+ GlForm,
+ GlFormCheckbox,
+ GlFormRadioGroup,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
@@ -34,12 +42,14 @@ export default {
GlButton,
GlIcon,
GlForm,
+ GlFormRadioGroup,
GlFormCheckbox,
MarkdownEditor,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
@@ -53,6 +63,7 @@ export default {
note: '',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
@@ -74,6 +85,38 @@ export default {
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
+ radioGroupOptions() {
+ return [
+ {
+ html: [
+ __('Comment'),
+ `<p class="help-text">
+ ${__('Submit general feedback without explicit approval.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'reviewed',
+ },
+ {
+ html: [
+ __('Approve'),
+ `<p class="help-text">
+ ${__('Submit feedback and approve these changes.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'approved',
+ disabled: !this.userPermissions.canApprove,
+ },
+ {
+ html: [
+ __('Request changes'),
+ `<p class="help-text">
+ ${__('Submit feedback that should be addressed before merging.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'requested_changes',
+ },
+ ];
+ },
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
@@ -208,7 +251,14 @@ export default {
@keydown.ctrl.enter="submitReview"
/>
</div>
- <template v-if="userPermissions.canApprove">
+ <gl-form-radio-group
+ v-if="glFeatures.mrRequestChanges"
+ v-model="noteData.reviewer_state"
+ :options="radioGroupOptions"
+ class="gl-mt-4"
+ data-testid="reviewer_states"
+ />
+ <template v-else-if="userPermissions.canApprove">
<gl-form-checkbox
v-model="noteData.approve"
data-testid="approve_merge_request"
@@ -216,14 +266,14 @@ export default {
>
{{ __('Approve merge request') }}
</gl-form-checkbox>
- <approval-password
- v-if="getNoteableData.require_password_to_approve"
- v-show="noteData.approve"
- v-model="noteData.approval_password"
- class="gl-mt-3"
- data-testid="approve_password"
- />
</template>
+ <approval-password
+ v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
+ v-show="noteData.approve || noteData.reviewer_state === 'approved'"
+ v-model="noteData.approval_password"
+ class="gl-mt-3"
+ data-testid="approve_password"
+ />
<div class="gl-display-flex gl-justify-content-start gl-mt-4">
<gl-button
:loading="isSubmitting"
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index 487215875c0..db84eaa82c2 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -4,12 +4,22 @@ import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+const defaultTitle = __('CI/CD Catalog');
+const defaultDescription = s__(
+ 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+);
+
export default {
components: {
GlBanner,
GlLink,
},
- inject: ['pageTitle', 'pageDescription'],
+ inject: {
+ pageTitle: { default: defaultTitle },
+ pageDescription: {
+ default: defaultDescription,
+ },
+ },
data() {
return {
isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
@@ -50,7 +60,7 @@ export default {
</gl-banner>
<h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
<p>
- <span>{{ pageDescription }}</span>
+ <span data-testid="description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
new file mode 100644
index 00000000000..5e8727a3ed0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -0,0 +1,112 @@
+<script>
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+
+export default {
+ components: {
+ CatalogHeader,
+ CatalogListSkeletonLoader,
+ CiResourcesList,
+ EmptyState,
+ },
+ data() {
+ return {
+ catalogResources: [],
+ currentPage: 1,
+ totalCount: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ catalogResources: {
+ query: getCatalogResources,
+ variables() {
+ return {
+ first: ciCatalogResourcesItemsCount,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResources?.nodes || [];
+ },
+ result({ data }) {
+ const { pageInfo } = data?.ciCatalogResources || {};
+ this.pageInfo = pageInfo;
+ this.totalCount = data?.ciCatalogResources?.count || 0;
+ },
+ error(e) {
+ createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
+ },
+ },
+ },
+ computed: {
+ hasResources() {
+ return this.catalogResources.length > 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.catalogResources.loading;
+ },
+ },
+ methods: {
+ async handlePrevPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ before: this.pageInfo.startCursor,
+ last: ciCatalogResourcesItemsCount,
+ first: null,
+ },
+ });
+
+ this.currentPage -= 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ async handleNextPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+
+ this.currentPage += 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ },
+ i18n: {
+ fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
+ },
+};
+</script>
+<template>
+ <div>
+ <catalog-header />
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" />
+ <ci-resources-list
+ v-else
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/global_catalog.vue b/app/assets/javascripts/ci/catalog/global_catalog.vue
new file mode 100644
index 00000000000..76eac11a122
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/global_catalog.vue
@@ -0,0 +1,10 @@
+<script>
+import CiCatalogHome from './components/ci_catalog_home.vue';
+
+export default {
+ components: { CiCatalogHome },
+};
+</script>
+<template>
+ <ci-catalog-home />
+</template>
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
new file mode 100644
index 00000000000..aae29edef5e
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -0,0 +1,16 @@
+#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
+
+query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
+ ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ count
+ nodes {
+ ...CatalogResourceFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
new file mode 100644
index 00000000000..5815245506c
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+
+import GlobalCatalog from './global_catalog.vue';
+import CiResourcesPage from './components/pages/ci_resources_page.vue';
+import { createRouter } from './router';
+
+export const initCatalog = (selector = '#js-ci-cd-catalog') => {
+ const el = document.querySelector(selector);
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+ const { ciCatalogPath } = dataset;
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
+ });
+
+ return new Vue({
+ el,
+ name: 'GlobalCatalog',
+ router: createRouter(ciCatalogPath, CiResourcesPage),
+ apolloProvider,
+ provide: {
+ ciCatalogPath,
+ },
+ render(h) {
+ return h(GlobalCatalog);
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index dc4a2d91c84..ed5ce02c32e 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -64,7 +64,7 @@ export default {
latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
mergeTrainBadgeText: s__('Pipelines|merge train'),
mergeTrainBadgeTooltip: s__(
- 'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
),
invalidBadgeText: s__('Pipelines|yaml invalid'),
failedBadgeText: s__('Pipelines|error'),
@@ -74,7 +74,11 @@ export default {
),
detachedBadgeText: s__('Pipelines|merge request'),
detachedBadgeTooltip: s__(
- "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
+ "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.",
+ ),
+ mergedResultsBadgeText: s__('Pipelines|merged results'),
+ mergedResultsBadgeTooltip: s__(
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.',
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
@@ -527,6 +531,15 @@ export default {
{{ $options.i18n.detachedBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.mergedResultsPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergedResultsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergedResultsBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.stuck"
v-gl-tooltip
:title="$options.i18n.stuckBadgeTooltip"
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 067ec3f305e..0ab5d9bcda0 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -26,6 +26,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
child,
latest,
mergeTrainPipeline,
+ mergedResultsPipeline,
invalid,
failed,
autoDevops,
@@ -62,6 +63,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
child: parseBoolean(child),
latest: parseBoolean(latest),
mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
+ mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
invalid: parseBoolean(invalid),
failed: parseBoolean(failed),
autoDevops: parseBoolean(autoDevops),
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index a78b901af48..f98369c2fde 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -25,12 +25,22 @@ export const EMOJI_VERSION = '3';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
- if (
- isLocalStorageAvailable &&
- window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
- window.localStorage.getItem(CACHE_KEY)
- ) {
- return JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ try {
+ window.localStorage.removeItem(CACHE_VERSION_KEY);
+ } catch {
+ // Cleanup after us and remove the old EMOJI_VERSION_KEY
+ }
+
+ try {
+ if (isLocalStorageAvailable) {
+ const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) {
+ return parsed.data;
+ }
+ }
+ } catch {
+ // Maybe the stored data was corrupted or the version didn't match.
+ // Let's not error out.
}
// We load the JSON file direct from the server
@@ -41,8 +51,7 @@ async function loadEmoji() {
);
try {
- window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
} catch {
// Setting data in localstorage may fail when storage quota is exceeded.
// We should continue even when this fails.
diff --git a/app/assets/javascripts/pages/explore/catalog/index.js b/app/assets/javascripts/pages/explore/catalog/index.js
new file mode 100644
index 00000000000..fec738a93a6
--- /dev/null
+++ b/app/assets/javascripts/pages/explore/catalog/index.js
@@ -0,0 +1,3 @@
+import { initCatalog } from '~/ci/catalog/';
+
+initCatalog();
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index b50a82ff676..86a5f5107f8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
@@ -15,6 +16,7 @@ import {
SCOPE_NOTES,
SCOPE_COMMITS,
SCOPE_MILESTONES,
+ SCOPE_WIKI_BLOBS,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -24,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
+import WikiBlobsFilters from './wiki_blobs_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -33,6 +36,7 @@ export default {
BlobsFilters,
ProjectsFilters,
NotesFilters,
+ WikiBlobsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
@@ -41,6 +45,7 @@ export default {
CommitsFilters,
MilestonesFilters,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
// useSidebarNavigation refers to whether the new left sidebar navigation is enabled
...mapState(['useSidebarNavigation', 'searchType']),
@@ -66,6 +71,12 @@ export default {
showMilestonesFilters() {
return this.currentScope === SCOPE_MILESTONES;
},
+ showWikiBlobsFilters() {
+ return (
+ this.currentScope === SCOPE_WIKI_BLOBS &&
+ this.glFeatures?.searchProjectWikisHideArchivedProjects
+ );
+ },
showScopeNavigation() {
// showScopeNavigation refers to whether the scope navigation should be shown
// while the legacy navigation is being used and there are no search results
@@ -93,6 +104,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
@@ -109,6 +121,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -119,6 +132,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</small-screen-drawer-navigation>
</section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
index ed90e2aaded..96a6f119da2 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
+const scopes = [
+ 'projects',
+ 'issues',
+ 'merge_requests',
+ 'notes',
+ 'blobs',
+ 'commits',
+ 'milestones',
+ 'wiki_blobs',
+];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
new file mode 100644
index 00000000000..b1f386d9f4f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'WikiBlobsFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index b5446ecbb42..1559155a941 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
+export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
+
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 74c41700f43..7962c8573df 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -40,15 +40,14 @@ export default {
},
methods: {
getModalInfoCopyStr() {
- const stateNameEncoded = this.stateName
- ? encodeURIComponent(this.stateName)
- : '<YOUR-STATE-NAME>';
+ const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default';
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
index 7903adea9bd..31cfe387b6e 100644
--- a/app/assets/javascripts/work_items/components/notes/system_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -26,6 +26,11 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+const ALLOWED_ICONS = ['issue-close'];
+const ICON_COLORS = {
+ 'issue-close': 'gl-bg-blue-100! gl-text-blue-700',
+};
+
export default {
i18n: {
deleteButtonLabel: __('Remove description history'),
@@ -66,6 +71,12 @@ export default {
noteAnchorId() {
return `note_${this.noteId}`;
},
+ getIconColor() {
+ return ICON_COLORS[this.note.systemNoteIconName] || '';
+ },
+ isAllowedIcon() {
+ return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -102,9 +113,16 @@ export default {
class="note system-note note-wrapper"
>
<div
- class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
+ :class="[
+ getIconColor,
+ {
+ 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
+ },
+ ]"
+ class="gl-float-left gl--flex-center gl-rounded-full gl-relative"
>
- <gl-icon :name="note.systemNoteIconName" />
+ <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">
diff --git a/app/assets/stylesheets/page_bundles/_system_note_styles.scss b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
new file mode 100644
index 00000000000..68e2b747c52
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
@@ -0,0 +1,59 @@
+/**
+Shared styles for system note dot and icon styles used for MR, Issue, Work Item
+*/
+.system-note-tiny-dot {
+ width: 8px;
+ height: 8px;
+ margin-top: 6px;
+ margin-left: 12px;
+ margin-right: 8px;
+ border: 2px solid var(--gray-50, $gray-50);
+ }
+
+ .system-note-icon {
+ width: 20px;
+ height: 20px;
+ margin-left: 6px;
+
+ &.gl-bg-green-100 {
+ --bg-color: var(--green-100, #{$green-100});
+ }
+
+ &.gl-bg-red-100 {
+ --bg-color: var(--red-100, #{$red-100});
+ }
+
+ &.gl-bg-blue-100 {
+ --bg-color: var(--blue-100, #{$blue-100});
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::before {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ bottom: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, transparent, var(--bg-color));
+
+ .system-note:first-child & {
+ display: none;
+ }
+ }
+
+ .system-note-icon:not(.mr-system-note-empty)::after {
+ content: '';
+ display: block;
+ position: absolute;
+ left: calc(50% - 1px);
+ top: 100%;
+ width: 2px;
+ height: 20px;
+ background: linear-gradient(to bottom, var(--bg-color), transparent);
+
+ .system-note:last-child & {
+ display: none;
+ }
+ } \ No newline at end of file
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 43369efe851..05563f8e314 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,4 +1,5 @@
@import 'mixins_and_variables_and_functions';
+@import 'system_note_styles';
.issuable-details {
section {
@@ -104,61 +105,3 @@
@include gl-font-weight-normal;
}
}
-
-.system-note-tiny-dot {
- width: 8px;
- height: 8px;
- margin-top: 6px;
- margin-left: 12px;
- margin-right: 8px;
- border: 2px solid var(--gray-50, $gray-50);
-}
-
-
-.system-note-icon {
- width: 20px;
- height: 20px;
- margin-left: 6px;
-
- &.gl-bg-green-100 {
- --bg-color: var(--green-100, #{$green-100});
- }
-
- &.gl-bg-red-100 {
- --bg-color: var(--red-100, #{$red-100});
- }
-
- &.gl-bg-blue-100 {
- --bg-color: var(--blue-100, #{$blue-100});
- }
-}
-
-.system-note-icon:not(.mr-system-note-empty)::before {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- bottom: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, transparent, var(--bg-color));
-
- .system-note:first-child & {
- display: none;
- }
-}
-
-.system-note-icon:not(.mr-system-note-empty)::after {
- content: '';
- display: block;
- position: absolute;
- left: calc(50% - 1px);
- top: 100%;
- width: 2px;
- height: 20px;
- background: linear-gradient(to bottom, var(--bg-color), transparent);
-
- .system-note:last-child & {
- display: none;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 154803c7d88..ec73f27ed09 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -1,4 +1,5 @@
@import 'mixins_and_variables_and_functions';
+@import 'system_note_styles';
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
$work-item-overview-right-sidebar-width: 23rem;
diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb
index 1ec25d44bfa..fb0073e0ad4 100644
--- a/app/controllers/projects/merge_requests/drafts_controller.rb
+++ b/app/controllers/projects/merge_requests/drafts_controller.rb
@@ -190,7 +190,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def update_reviewer_state
if reviewer_state_params[:reviewer_state] === 'approved'
::MergeRequests::ApprovalService
- .new(project: @project, current_user: current_user)
+ .new(project: @project, current_user: current_user, params: approve_params)
.execute(merge_request)
else
::MergeRequests::UpdateReviewerStateService
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a9e15c0bd90..8a92db36311 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:widget_pipeline_pass_subscription_update, project)
+ push_frontend_feature_flag(:mr_request_changes, current_user)
end
before_action only: [:edit] do
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index f78a28c89dd..48edda13904 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -125,6 +125,13 @@ module Repositories
def log_user_activity
Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute
end
+
+ def append_info_to_payload(payload)
+ super
+
+ payload[:metadata] ||= {}
+ payload[:metadata][:repository_storage] = project&.repository_storage
+ end
end
end
diff --git a/app/finders/data_transfer/mocked_transfer_finder.rb b/app/finders/data_transfer/mocked_transfer_finder.rb
deleted file mode 100644
index 9c5551005ea..00000000000
--- a/app/finders/data_transfer/mocked_transfer_finder.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# Mocked data for data transfer
-# Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330
-module DataTransfer
- class MockedTransferFinder
- def execute
- start_date = Date.new(2023, 0o1, 0o1)
- date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
-
- 0.upto(11).map do |i|
- {
- date: date_for_index.call(i),
- repository_egress: rand(70000..550000),
- artifacts_egress: rand(70000..550000),
- packages_egress: rand(70000..550000),
- registry_egress: rand(70000..550000)
- }.tap do |hash|
- hash[:total_egress] = hash
- .slice(:repository_egress, :artifacts_egress, :packages_egress, :registry_egress)
- .values
- .sum
- end
- end
- end
- end
-end
diff --git a/app/graphql/mutations/ci/catalog/resources/create.rb b/app/graphql/mutations/ci/catalog/resources/create.rb
index 258f83a3e19..7f934e101c8 100644
--- a/app/graphql/mutations/ci/catalog/resources/create.rb
+++ b/app/graphql/mutations/ci/catalog/resources/create.rb
@@ -15,7 +15,7 @@ module Mutations
def resolve(project_path:)
project = authorized_find!(project_path: project_path)
- response = ::Ci::Catalog::AddResourceService.new(project, current_user).execute
+ response = ::Ci::Catalog::Resources::CreateService.new(project, current_user).execute
errors = response.success? ? [] : [response.message]
diff --git a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
index 83bb144017c..133b86623f1 100644
--- a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::GroupDataTransferFinder.new(
- group: group,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute.map(&:attributes)
- end
+ results = ::DataTransfer::GroupDataTransferFinder.new(
+ group: group,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute.map(&:attributes)
{ egress_nodes: results.to_a }
end
diff --git a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
index c3296f7d4c3..d711f837251 100644
--- a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
+++ b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb
@@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group)
- results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group)
- ::DataTransfer::MockedTransferFinder.new.execute
- else
- ::DataTransfer::ProjectDataTransferFinder.new(
- project: project,
- from: args[:from],
- to: args[:to],
- user: current_user
- ).execute
- end
+ results = ::DataTransfer::ProjectDataTransferFinder.new(
+ project: project,
+ from: args[:from],
+ to: args[:to],
+ user: current_user
+ ).execute
{ egress_nodes: results }
end
diff --git a/app/graphql/types/data_transfer/project_data_transfer_type.rb b/app/graphql/types/data_transfer/project_data_transfer_type.rb
index 36afa20194e..363b675209d 100644
--- a/app/graphql/types/data_transfer/project_data_transfer_type.rb
+++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb
@@ -13,7 +13,6 @@ module Types
def total_egress(parent:)
return unless Feature.enabled?(:data_transfer_monitoring, parent.group)
- return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group)
object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress')
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index fc157df3891..e447940e2af 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -93,16 +93,11 @@ module AuthHelper
end
def saml_providers
- auth_providers.select do |provider|
- provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML'
+ providers = Gitlab.config.omniauth.providers.select do |provider|
+ provider.name == 'saml' || provider.dig('args', 'strategy_class') == 'OmniAuth::Strategies::SAML'
end
- end
-
- def auth_strategy_class(provider)
- config = Gitlab::Auth::OAuth::Provider.config_for(provider)
- return if config.nil? || config['args'].blank?
- config.args['strategy_class']
+ providers.map(&:name).map(&:to_sym)
end
def any_form_based_providers_enabled?
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 0c3b7d26fe2..1558f013462 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -40,6 +40,7 @@ module Projects
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
+ merged_results_pipeline: (pipeline.merged_result_pipeline? && !pipeline.merge_train_pipeline?).to_s,
invalid: pipeline.has_yaml_errors?.to_s,
failed: pipeline.failure_reason?.to_s,
auto_devops: pipeline.auto_devops_source?.to_s,
diff --git a/app/services/ci/catalog/add_resource_service.rb b/app/services/ci/catalog/add_resource_service.rb
deleted file mode 100644
index c22e7e84c3c..00000000000
--- a/app/services/ci/catalog/add_resource_service.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module Catalog
- class AddResourceService
- include Gitlab::Allowable
-
- attr_reader :project, :current_user
-
- def initialize(project, user)
- @current_user = user
- @project = project
- end
-
- def execute
- raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
-
- validation_response = Ci::Catalog::Resources::ValidateService.new(project, project.default_branch).execute
-
- if validation_response.success?
- create_catalog_resource
- else
- ServiceResponse.error(message: validation_response.message)
- end
- end
-
- private
-
- def create_catalog_resource
- catalog_resource = Ci::Catalog::Resource.new(project: project)
-
- if catalog_resource.valid?
- catalog_resource.save!
- ServiceResponse.success(payload: catalog_resource)
- else
- ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
- end
- end
- end
- end
-end
diff --git a/app/services/ci/catalog/resources/create_service.rb b/app/services/ci/catalog/resources/create_service.rb
new file mode 100644
index 00000000000..89367c70e82
--- /dev/null
+++ b/app/services/ci/catalog/resources/create_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class CreateService
+ include Gitlab::Allowable
+
+ attr_reader :project, :current_user
+
+ def initialize(project, user)
+ @current_user = user
+ @project = project
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
+
+ catalog_resource = Ci::Catalog::Resource.new(project: project)
+
+ if catalog_resource.valid?
+ catalog_resource.save!
+ ServiceResponse.success(payload: catalog_resource)
+ else
+ ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/enqueue_job_service.rb b/app/services/ci/enqueue_job_service.rb
index 9e3bea3fd28..db616473336 100644
--- a/app/services/ci/enqueue_job_service.rb
+++ b/app/services/ci/enqueue_job_service.rb
@@ -11,11 +11,14 @@ module Ci
end
def execute(&transition)
- job.user = current_user
- job.job_variables_attributes = variables if variables
-
transition ||= ->(job) { job.enqueue! }
- Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition)
+
+ Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
+ job.user = current_user
+ job.job_variables_attributes = variables if variables
+
+ transition.call(job)
+ end
ResetSkippedJobsService.new(job.project, current_user).execute(job)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 684231d3a37..e4d894ede1c 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -83,7 +83,7 @@
%ul.content-list.todos-list
= render @allowed_todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.gl-empty-state.gl-text-center.hidden
+ .col.js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-all-done-md.svg'
.text-content.gl-text-center
diff --git a/app/views/explore/catalog/show.html.haml b/app/views/explore/catalog/show.html.haml
index 6c10ba7dfd7..7c8d788f8e3 100644
--- a/app/views/explore/catalog/show.html.haml
+++ b/app/views/explore/catalog/show.html.haml
@@ -1,3 +1,3 @@
- page_title _('CI/CD Catalog')
-#js-ci-cd-catalog
+#js-ci-cd-catalog{ data: { ci_catalog_path: explore_catalog_index_path } }
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index b172e3bf94f..109bd559762 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -6,12 +6,12 @@
.form-group
= f.label :name, class: 'label-bold'
- = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
+ = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
.form-group
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
- = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
+ = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at
.text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
.form-group
@@ -22,15 +22,15 @@
.form-group
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
- = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } }
- if container_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } }
- if packages_registry_enabled?(group_or_project)
- = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } }
- = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } }
+ = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } }
+ = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } }
.gl-mt-3
- = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true
+ = f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true
diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index 30917ee6fff..2bc2e6c5b81 100644
--- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
@@ -1,11 +1,11 @@
-.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } }
+.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } }
.well-segment
%h5.gl-mt-0
= s_('DeployTokens|Your new Deploy Token username')
.form-group
.input-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
@@ -15,7 +15,7 @@
.form-group
.input-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger
diff --git a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml b/config/feature_flags/development/data_transfer_monitoring_mock_data.yml
deleted file mode 100644
index 77a43426e74..00000000000
--- a/config/feature_flags/development/data_transfer_monitoring_mock_data.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: data_transfer_monitoring_mock_data
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113392
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397693
-milestone: '15.11'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md
index 761e9e6dd66..b5fc32e69dc 100644
--- a/doc/ci/jobs/index.md
+++ b/doc/ci/jobs/index.md
@@ -297,7 +297,8 @@ For example, if you start rolling out new code and:
## Expand and collapse job log sections
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
+> - Support for output of multi-line command bash shell output [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3486) in GitLab 16.5 behind the [GitLab Runner feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html), `FF_SCRIPT_SECTIONS`.
Job logs are divided into sections that can be collapsed or expanded. Each section displays
the duration.
diff --git a/doc/development/development_processes.md b/doc/development/development_processes.md
index 5efcdd90df4..fa2beab52f6 100644
--- a/doc/development/development_processes.md
+++ b/doc/development/development_processes.md
@@ -1,7 +1,7 @@
---
stage: none
group: unassigned
-info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines"
+info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Development processes
@@ -35,32 +35,12 @@ Complementary reads:
### Development guidelines review
-When you submit a change to the GitLab development guidelines, who
-you ask for reviews depends on the level of change.
+For changes to development guidelines, request review and approval from an experienced GitLab Team Member.
-#### Wording, style, or link changes
-
-Not all changes require extensive review. For example, MRs that don't change the
-content's meaning or function can be reviewed, approved, and merged by any
-maintainer or Technical Writer. These can include:
-
-- Typo fixes.
-- Clarifying links, such as to external programming language documentation.
-- Changes to comply with the [Documentation Style Guide](documentation/index.md)
- that don't change the intent of the documentation page.
-
-#### Specific changes
-
-If the MR proposes changes that are limited to a particular stage, group, or team,
-request a review and approval from an experienced GitLab Team Member in that
-group. For example, if you're documenting a new internal API used exclusively by
+For example, if you're documenting a new internal API used exclusively by
a given group, request an engineering review from one of the group's members.
-After the engineering review is complete, assign the MR to the
-[Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
-in the modified documentation page's metadata.
-If the page is not assigned to a specific group, follow the
-[Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
+Small fixes, like typos, can be merged by any user with at least the Maintainer role.
#### Broader changes
@@ -85,7 +65,6 @@ In these cases, use the following workflow:
- [Quality](https://about.gitlab.com/handbook/engineering/quality/)
- [Engineering Productivity](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity/)
- [Infrastructure](https://about.gitlab.com/handbook/engineering/infrastructure/)
- - [Technical Writing](https://about.gitlab.com/handbook/product/ux/technical-writing/)
You can skip this step for MRs authored by EMs or Staff Engineers responsible
for their area.
@@ -97,15 +76,12 @@ In these cases, use the following workflow:
author / approver of the MR.
If this is a significant change across multiple areas, request final review
- and approval from the VP of Development, the DRI for Development Guidelines,
- @clefelhocz1.
+ and approval from the VP of Development, who is the DRI for development guidelines.
-1. After all approvals are complete, assign the MR to the
- [Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
- in the modified documentation page's metadata.
- If the page is not assigned to a specific group, follow the
- [Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
- The Technical Writer may ask for additional approvals as previously suggested before merging the MR.
+Any Maintainer can merge the MR.
+If you would like a review by a technical writer, post a message in the #docs Slack channel.
+Technical writers do not need to review the content, however, and any Maintainer
+other than the MR author can merge.
### Reviewer values
@@ -114,6 +90,8 @@ In these cases, use the following workflow:
As a reviewer or as a reviewee, make sure to familiarize yourself with
the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/reviewer-values/) we strive for at GitLab.
+Also, any doc content should follow the [Documentation Style Guide](documentation/index.md).
+
## Language-specific guides
### Go guides
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index fc0f4013104..5c99f5c48df 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -36,6 +36,13 @@ A member of the Technical Writing team adds these labels:
`docs::` prefix. For example, `~docs::improvement`.
- The [`~Technical Writing` team label](../labels/index.md#team-labels).
+NOTE:
+With the exception of `/doc/development/documentation`,
+technical writers do not review content in the `doc/development` directory.
+Any Maintainer can merge content in the `doc/development` directory.
+If you would like a technical writer review of content in the `doc/development` directory,
+ask in the `#docs` Slack channel.
+
## Post-merge reviews
If not assigned to a Technical Writer for review prior to merging, a review must be scheduled
diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md
index d02e366252a..b0e47233777 100644
--- a/doc/development/internal_analytics/index.md
+++ b/doc/development/internal_analytics/index.md
@@ -14,6 +14,13 @@ when developing new features or instrumenting existing ones.
## Fundamental concepts
+<div class="video-fallback">
+ See the video about <a href="https://www.youtube.com/watch?v=GtFNXbjygWo">the concepts of events and metrics.</a>
+</div>
+<figure class="video_container">
+ <iframe src="https://www.youtube-nocookie.com/embed/GtFNXbjygWo" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
Events and metrics are the foundation of the internal analytics system.
Understanding the difference between the two concepts is vital to using the system.
diff --git a/doc/development/repository_storage_moves/index.md b/doc/development/repository_storage_moves/index.md
new file mode 100644
index 00000000000..578bc1eabee
--- /dev/null
+++ b/doc/development/repository_storage_moves/index.md
@@ -0,0 +1,102 @@
+---
+stage: Create
+group: Source Code
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Project Repository Storage Moves
+
+This document was created to help contributors understand the code design of
+[project repository storage moves](../../api/project_repository_storage_moves.md).
+Read this document before making changes to the code for this feature.
+
+This document is intentionally limited to an overview of how the code is
+designed, as code can change often. To understand how a specific part of the
+feature works, view the code and the specs. The details here explain how the
+major components of the Code Owners feature work.
+
+NOTE:
+This document should be updated when parts of the codebase referenced in this
+document are updated, removed, or new parts are added.
+
+## Business logic
+
+- `Projects::RepositoryStorageMove`: Tracks the move, includes state machine.
+ - Defined in `app/models/projects/repository_storage_move.rb`.
+- `RepositoryStorageMovable`: Contains the state machine logic, validators, and some helper methods.
+ - Defined in `app/models/concerns/repository_storage_movable.rb`.
+- `Project`: The project model.
+ - Defined in `app/models/project.rb`.
+- `CanMoveRepositoryStorage`: Contains helper methods that are into `Project`.
+ - Defined in `app/models/concerns/can_move_repository_storage.rb`.
+- `API::ProjectRepositoryStorageMoves`: API class for project repository storage moves.
+ - Defined in `lib/api/project_repository_storage_moves.rb`.
+- `Entities::Projects::RepositoryStorageMove`: API entity for serializing the `Projects::RepositoryStorageMove` model.
+ - Defined in `lib/api/entities/projects/repository_storage_moves.rb`.
+- `Projects::ScheduleBulkRepositoryShardMovesService`: Service to schedule bulk moves.
+ - Defined in `app/services/projects/schedule_bulk_repository_shard_moves_service.rb`.
+- `ScheduleBulkRepositoryShardMovesMethods`: Generic methods for bulk moves.
+ - Defined in `app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb`.
+- `Projects::ScheduleBulkRepositoryShardMovesWorker`: Worker to handle bulk moves.
+ - Defined in `app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb`.
+- `Projects::UpdateRepositoryStorageWorker`: Finds repository storage move and then calls the update storage service.
+ - Defined in `app/workers/projects/update_repository_storage_worker.rb`.
+- `UpdateRepositoryStorageWorker`: Module containing generic logic for `Projects::UpdateRepositoryStorageWorker`.
+ - Defined in `app/workers/concerns/update_repository_storage_worker.rb`.
+- `Projects::UpdateRepositoryStorageService`: Performs the move.
+ - Defined in `app/services/projects/update_repository_storage_service.rb`.
+- `UpdateRepositoryStorageMethods`: Module with generic methods included in `Projects::UpdateRepositoryStorageService`.
+ - Defined in `app/services/concerns/update_repository_storage_methods.rb`.
+- `Projects::UpdateService`: Schedules move if the passed parameters request a move.
+ - Defined in `app/services/projects/update_service.rb`.
+- `PoolRepository`: Ruby object representing Gitaly `ObjectPool`.
+ - Defined in `app/models/pool_repository.rb`.
+- `ObjectPool::CreateWorker`: Worker to create an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/create_worker.rb`.
+- `ObjectPool::JoinWorker`: Worker to join an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/join_worker.rb`.
+- `ObjectPool::ScheduleJoinWorker`: Worker to schedule an `ObjectPool::JoinWorker`.
+ - Defined in `app/workers/object_pool/schedule_join_worker.rb`.
+- `ObjectPool::DestroyWorker`: Worker to destroy an `ObjectPool` via `Gitaly`.
+ - Defined in `app/workers/object_pool/destroy_worker.rb`.
+- `ObjectPoolQueue`: Module to configure `ObjectPool` workers.
+ - Defined in `app/workers/concerns/object_pool_queue.rb`.
+- `Repositories::ReplicateService`: Handles replication of data from one repository to another.
+ - Defined in `app/services/repositories/replicate_service.rb`.
+
+## Flow
+
+These flowcharts should help explain the flow from the endpoints down to the
+models for different features.
+
+### Schedule a repository storage move via the API
+
+```mermaid
+graph TD
+ A[<code>POST /api/:version/project_repository_storage_moves</code>] --> C
+ B[<code>POST /api/:version/projects/:id/repository_storage_moves</code>] --> D
+ C[Schedule move for each project in shard] --> D[Set state to scheduled]
+ D --> E[<code>after_transition callback</code>]
+ E --> F{<code>set_repository_read_only!</code>}
+ F -->|success| H[Schedule repository update worker]
+ F -->|error| G[Set state to failed]
+```
+
+### Moving the storage after being scheduled
+
+```mermaid
+graph TD
+ A[Repository update worker scheduled] --> B{State is scheduled?}
+ B -->|Yes| C[Set state to started]
+ B -->|No| D[Return success]
+ C --> E{Same filesystem?}
+ E -.-> G[Set project repo to writable]
+ E -->|Yes| F["Mirror repositories (project, wiki, design, & pool)"]
+ G --> H[Update repo storage value]
+ H --> I[Set state to finished]
+ I --> J[Associate project with new pool repository]
+ J --> K[Unlink old pool repository]
+ K --> L[Update project repository storage values]
+ L --> N[Remove old paths if same filesystem]
+ N --> M[Set state to finished]
+```
diff --git a/doc/user/gitlab_duo_chat.md b/doc/user/gitlab_duo_chat.md
index c103d9c29ba..ba6cd9b8f21 100644
--- a/doc/user/gitlab_duo_chat.md
+++ b/doc/user/gitlab_duo_chat.md
@@ -13,8 +13,6 @@ You can get AI generated support from GitLab Duo Chat about the following topics
- How to use GitLab.
- Questions about an issue.
-- How to use GitLab.
-- Questions about an issue.
- Question about an epic.
- Questions about a code file.
- Follow-up questions to answers from the chat.
diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md
index 2a33543fea5..5a08307cc11 100644
--- a/doc/user/organization/index.md
+++ b/doc/user/organization/index.md
@@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Organization
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/409913) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `ui_for_organizations`. Disabled by default.
+
+FLAG:
+This feature is not ready for production use.
+On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `ui_for_organizations`.
+On GitLab.com, this feature is not available.
+
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
@@ -37,6 +44,37 @@ see [epic 9265](https://gitlab.com/groups/gitlab-org/-/epics/9265).
For a video introduction to the new hierarchy concept for groups and projects for epics, see
[Consolidating groups and projects update (August 2021)](https://www.youtube.com/watch?v=fE74lsG_8yM).
+## View organizations
+
+To view the organizations you have access to:
+
+- On the left sidebar, select **Organizations** (**{organization}**).
+
+## Create an organization
+
+1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**.
+1. In the **Organization name** field, enter a name for the organization.
+1. In the **Organization URL** field, enter a path for the organization.
+1. Select **Create organization**.
+
+## Edit an organization's name
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit.
+1. Select **Settings > General**.
+1. Update the **Organization name** field.
+1. Select **Save changes**.
+
+## Manage groups and projects
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
+1. Select **Manage > Groups and projects**.
+1. To switch between groups and projects, use the **Display** filter next to the search box.
+
+## Manage users
+
+1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
+1. Select **Manage > Users**.
+
## Related topics
- [Organization developer documentation](../../development/organization/index.md)
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 58cfd90c13f..63e5cc93e7d 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -82,6 +82,7 @@ or:
> - Filtering by `reviewer` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47605) in GitLab 13.7.
> - Filtering by potential approvers was moved to GitLab Premium in 13.9.
> - Filtering by `approved-by` moved to GitLab Premium in 13.9.
+> - Filtering by `source-branch` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134555) in GitLab 16.6.
To filter the list of merge requests:
diff --git a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
index 8cd03978f27..f8a05d3132f 100644
--- a/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition/redis_hll_generator.rb
@@ -1,11 +1,14 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
module UsageMetricDefinition
class RedisHllGenerator < Rails::Generators::Base
- desc 'Generates a metric definition .yml file with defaults for Redis HLL.'
+ desc '[DEPRECATED] Generates a metric definition .yml file with defaults for Redis HLL.'
argument :category, type: :string, desc: "Category name"
argument :events, type: :array, desc: "Unique event names", banner: 'event_one event_two event_three'
diff --git a/lib/generators/gitlab/usage_metric_definition_generator.rb b/lib/generators/gitlab/usage_metric_definition_generator.rb
index d57a6b0b724..c231697e22e 100644
--- a/lib/generators/gitlab/usage_metric_definition_generator.rb
+++ b/lib/generators/gitlab/usage_metric_definition_generator.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# DEPRECATED. Consider using using Internal Events tracking framework
+# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
+
require 'rails/generators'
module Gitlab
@@ -30,7 +33,7 @@ module Gitlab
source_root File.expand_path('../../../generator_templates/usage_metric_definition', __dir__)
- desc 'Generates metric definitions yml files'
+ desc '[DEPRECATED] Generates metric definitions yml files'
class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if metric is for ee'
class_option :dir,
@@ -40,6 +43,13 @@ module Gitlab
argument :key_paths, type: :array, desc: 'Unique JSON key paths for the metrics'
def create_metric_file
+ say("This generator is DEPRECATED. Use Internal Events tracking framework instead.")
+ # rubocop: disable Gitlab/DocUrl -- link for developers, not users
+ say("https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html")
+ # rubocop: enable Gitlab/DocUrl
+ desc = ask("Would you like to continue anyway? y/N") || 'n'
+ return unless desc.casecmp('y') == 0
+
validate!
key_paths.each do |key_path|
diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb
index e39bbb36680..88991495a10 100644
--- a/lib/gitlab/instrumentation/redis_base.rb
+++ b/lib/gitlab/instrumentation/redis_base.rb
@@ -90,7 +90,7 @@ module Gitlab
result = ::Gitlab::Instrumentation::RedisClusterValidator.validate(commands)
return true if result.nil?
- if !result[:valid] && !result[:allowed] && (Rails.env.development? || Rails.env.test?)
+ if !result[:valid] && !result[:allowed] && raise_cross_slot_validation_errors?
raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
@@ -189,6 +189,10 @@ module Gitlab
redirection_type, _, target_node_key = err_msg.split
{ redirection_type: redirection_type, target_node_key: target_node_key }
end
+
+ def raise_cross_slot_validation_errors?
+ Rails.env.development? || Rails.env.test?
+ end
end
end
end
diff --git a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
index f1d7e32613d..2971dabe044 100644
--- a/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
+++ b/lib/gitlab/seeders/ci/catalog/resource_seeder.rb
@@ -79,7 +79,7 @@ module Gitlab
end
def create_ci_catalog(project)
- result = ::Ci::Catalog::AddResourceService.new(project, @current_user).execute
+ result = ::Ci::Catalog::Resources::CreateService.new(project, @current_user).execute
if result.success?
result.payload
else
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 09570609ff4..c57f178a662 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10337,6 +10337,9 @@ msgstr ""
msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier."
msgstr ""
+msgid "CiCatalog|Discover CI configuration resources for a seamless CI/CD experience."
+msgstr ""
+
msgid "CiCatalog|Get started with the CI/CD Catalog"
msgstr ""
@@ -12601,9 +12604,6 @@ msgstr ""
msgid "ComplianceReport|Remove framework from selected projects"
msgstr ""
-msgid "ComplianceReport|Retrieving the compliance framework report failed. Refresh the page and try again."
-msgstr ""
-
msgid "ComplianceReport|Search target branch"
msgstr ""
@@ -35330,10 +35330,13 @@ msgstr ""
msgid "Pipelines|This pipeline is stuck"
msgstr ""
-msgid "Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch."
+msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch."
msgstr ""
-msgid "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch."
+msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch."
+msgstr ""
+
+msgid "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch."
msgstr ""
msgid "Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables."
@@ -35417,6 +35420,9 @@ msgstr ""
msgid "Pipelines|merge train"
msgstr ""
+msgid "Pipelines|merged results"
+msgstr ""
+
msgid "Pipelines|stuck"
msgstr ""
@@ -40429,6 +40435,9 @@ msgstr ""
msgid "Request a new one"
msgstr ""
+msgid "Request changes"
+msgstr ""
+
msgid "Request data is too large"
msgstr ""
@@ -46503,6 +46512,15 @@ msgstr ""
msgid "Submit feedback"
msgstr ""
+msgid "Submit feedback and approve these changes."
+msgstr ""
+
+msgid "Submit feedback that should be addressed before merging."
+msgstr ""
+
+msgid "Submit general feedback without explicit approval."
+msgstr ""
+
msgid "Submit review"
msgstr ""
diff --git a/qa/qa/page/component/deploy_token.rb b/qa/qa/page/component/deploy_token.rb
new file mode 100644
index 00000000000..71501391db1
--- /dev/null
+++ b/qa/qa/page/component/deploy_token.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module DeployToken
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/views/shared/deploy_tokens/_form.html.haml' do
+ element 'deploy-token-name-field'
+ element 'deploy-token-expires-at-field'
+ element 'deploy-token-read-repository-checkbox'
+ element 'deploy-token-read-package-registry-checkbox'
+ element 'deploy-token-write-package-registry-checkbox'
+ element 'deploy-token-read-registry-checkbox'
+ element 'deploy-token-write-registry-checkbox'
+ element 'create-deploy-token-button'
+ end
+
+ base.view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
+ element 'created-deploy-token-container'
+ element 'deploy-token-user-field'
+ element 'deploy-token-field'
+ end
+ end
+
+ def fill_token_name(name)
+ fill_element('deploy-token-name-field', name)
+ end
+
+ def fill_token_expires_at(expires_at)
+ fill_element('deploy-token-expires-at-field', "#{expires_at}\n")
+ end
+
+ def fill_scopes(scopes)
+ check_element('deploy-token-read-repository-checkbox', true) if scopes.include? :read_repository
+ check_element('deploy-token-read-package-registry-checkbox', true) if scopes.include? :read_package_registry
+ check_element('deploy-token-write-package-registry-checkbox', true) if scopes.include? :write_package_registry
+ check_element('deploy-token-read-registry-checkbox', true) if scopes.include? :read_registry
+ check_element('deploy-token-write-registry-checkbox', true) if scopes.include? :write_registry
+ end
+
+ def add_token
+ click_element('create-deploy-token-button')
+ end
+
+ def token_username
+ within_new_project_deploy_token do
+ find_element('deploy-token-user-field').value
+ end
+ end
+
+ def token_password
+ within_new_project_deploy_token do
+ find_element('deploy-token-field').value
+ end
+ end
+
+ private
+
+ def within_new_project_deploy_token(&block)
+ has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+
+ within_element('created-deploy-token-container', &block)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/settings/group_deploy_tokens.rb b/qa/qa/page/group/settings/group_deploy_tokens.rb
index c1c3303113b..4a44787d26d 100644
--- a/qa/qa/page/group/settings/group_deploy_tokens.rb
+++ b/qa/qa/page/group/settings/group_deploy_tokens.rb
@@ -5,60 +5,7 @@ module QA
module Group
module Settings
class GroupDeployTokens < Page::Base
- view 'app/views/shared/deploy_tokens/_form.html.haml' do
- element :deploy_token_name_field
- element :deploy_token_expires_at_field
- element :deploy_token_read_repository_checkbox
- element :deploy_token_read_package_registry_checkbox
- element :deploy_token_read_registry_checkbox
- element :deploy_token_write_package_registry_checkbox
- element :create_deploy_token_button
- end
-
- view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
- element :created_deploy_token_container
- element :deploy_token_user_field
- element :deploy_token_field
- end
-
- def fill_token_name(name)
- fill_element(:deploy_token_name_field, name)
- end
-
- def fill_token_expires_at(expires_at)
- fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
- end
-
- def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false )
- check_element(:deploy_token_read_repository_checkbox, true) if read_repository
- check_element(:deploy_token_read_package_registry_checkbox, true) if read_package_registry
- check_element(:deploy_token_read_registry_checkbox, true) if read_registry
- check_element(:deploy_token_write_package_registry_checkbox, true) if write_package_registry
- end
-
- def add_token
- click_element(:create_deploy_token_button)
- end
-
- def token_username
- within_new_project_deploy_token do
- find_element(:deploy_token_user_field).value
- end
- end
-
- def token_password
- within_new_project_deploy_token do
- find_element(:deploy_token_field).value
- end
- end
-
- private
-
- def within_new_project_deploy_token(&block)
- has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
-
- within_element(:created_deploy_token_container, &block)
- end
+ include Page::Component::DeployToken
end
end
end
diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb
deleted file mode 100644
index cf25f4a0568..00000000000
--- a/qa/qa/page/project/settings/deploy_tokens.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Settings
- class DeployTokens < Page::Base
- view 'app/views/shared/deploy_tokens/_form.html.haml' do
- element :deploy_token_name_field
- element :deploy_token_expires_at_field
- element :deploy_token_read_repository_checkbox
- element :deploy_token_read_package_registry_checkbox
- element :deploy_token_write_package_registry_checkbox
- element :deploy_token_read_registry_checkbox
- element :deploy_token_write_registry_checkbox
- element :create_deploy_token_button
- end
-
- view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
- element :created_deploy_token_container
- element :deploy_token_user_field
- element :deploy_token_field
- end
-
- def fill_token_name(name)
- fill_element(:deploy_token_name_field, name)
- end
-
- def fill_token_expires_at(expires_at)
- fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
- end
-
- def fill_scopes(scopes)
- if scopes.include? :read_repository
- check_element(:deploy_token_read_repository_checkbox, true)
- end
-
- if scopes.include? :read_package_registry
- check_element(:deploy_token_read_package_registry_checkbox, true)
- end
-
- if scopes.include? :write_package_registry
- check_element(:deploy_token_write_package_registry_checkbox, true)
- end
-
- if scopes.include? :read_registry
- check_element(:deploy_token_read_registry_checkbox, true)
- end
-
- if scopes.include? :write_registry
- check_element(:deploy_token_write_registry_checkbox, true)
- end
- end
-
- def add_token
- click_element(:create_deploy_token_button)
- end
-
- def token_username
- within_new_project_deploy_token do
- find_element(:deploy_token_user_field).value
- end
- end
-
- def token_password
- within_new_project_deploy_token do
- find_element(:deploy_token_field).value
- end
- end
-
- private
-
- def within_new_project_deploy_token
- has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
-
- within_element(:created_deploy_token_container) do
- yield
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/settings/project_deploy_tokens.rb b/qa/qa/page/project/settings/project_deploy_tokens.rb
new file mode 100644
index 00000000000..61b44a5e546
--- /dev/null
+++ b/qa/qa/page/project/settings/project_deploy_tokens.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class ProjectDeployTokens < Page::Base
+ include Page::Component::DeployToken
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index a5871f6cd50..d68784c09aa 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -33,7 +33,7 @@ module QA
def expand_deploy_tokens(&block)
expand_content('deploy-tokens-settings-content') do
- Settings::DeployTokens.perform(&block)
+ Settings::ProjectDeployTokens.perform(&block)
end
end
diff --git a/qa/qa/resource/group_deploy_token.rb b/qa/qa/resource/group_deploy_token.rb
index 4c9b296ece1..3a110fcbdc8 100644
--- a/qa/qa/resource/group_deploy_token.rb
+++ b/qa/qa/resource/group_deploy_token.rb
@@ -51,7 +51,7 @@ module QA
setting.expand_deploy_tokens do |page|
page.fill_token_name(name)
page.fill_token_expires_at(expires_at)
- page.fill_scopes(read_repository: true, read_package_registry: true, write_package_registry: true)
+ page.fill_scopes(@scopes)
page.add_token
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
index bde817eccd3..7004f608d9e 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_job_artifacts/unlocking_job_artifacts_across_parent_child_pipelines_spec.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
+ RSpec.describe 'Verify', :runner, product_group: :pipeline_security,
+ quarantine: {
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422863',
+ type: :flaky
+ } do
describe 'Unlocking job artifacts across parent-child pipelines' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'unlock-job-artifacts-parent-child-project') }
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 602c9c0a2ce..0ae44d3654e 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -200,4 +200,24 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
end
end
+
+ describe '#append_info_to_payload' do
+ let(:log_payload) { {} }
+ let(:container) { project.design_management_repository }
+ let(:repository_path) { "#{container.full_path}.git" }
+ let(:params) { { repository_path: repository_path, service: 'git-upload-pack' } }
+ let(:repository_storage) { "default" }
+
+ before do
+ allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *|
+ method.call(log_payload)
+ end
+ end
+
+ it 'appends metadata for logging' do
+ post :git_upload_pack, params: params
+ expect(controller).to have_received(:append_info_to_payload)
+ expect(log_payload.dig(:metadata, :repository_storage)).to eq(repository_storage)
+ end
+ end
end
diff --git a/spec/features/explore/catalog_spec.rb b/spec/features/explore/catalog_spec.rb
new file mode 100644
index 00000000000..52ce52e43fe
--- /dev/null
+++ b/spec/features/explore/catalog_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ namespace.add_developer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET explore/catalog' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:ci_resource_projects) do
+ create_list(
+ :project,
+ 3,
+ :repository,
+ description: 'A simple component',
+ namespace: namespace
+ )
+ end
+
+ before do
+ ci_resource_projects.each do |current_project|
+ create(:ci_catalog_resource, project: current_project)
+ end
+
+ visit explore_catalog_index_path
+ wait_for_requests
+ end
+
+ it 'shows CI Catalog title and description', :aggregate_failures do
+ expect(page).to have_content('CI/CD Catalog')
+ expect(page).to have_content('Discover CI configuration resources for a seamless CI/CD experience.')
+ end
+
+ it 'renders CI Catalog resources list' do
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
+ end
+
+ context 'for a single CI/CD catalog resource' do
+ it 'renders resource details', :aggregate_failures do
+ within_testid('catalog-resource-item', match: :first) do
+ expect(page).to have_content(ci_resource_projects[2].name)
+ expect(page).to have_content(ci_resource_projects[2].description)
+ expect(page).to have_content(namespace.name)
+ end
+ end
+
+ context 'when clicked' do
+ before do
+ find_by_testid('ci-resource-link', match: :first).click
+ end
+
+ it 'navigate to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+ end
+ end
+
+ describe 'GET explore/catalog/:id' do
+ let_it_be(:project) { create(:project, :repository, namespace: namespace) }
+ let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project) }
+
+ before do
+ visit explore_catalog_path(id: new_ci_resource["id"])
+ end
+
+ it 'navigates to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+end
diff --git a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb b/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
deleted file mode 100644
index f60bc98f587..00000000000
--- a/spec/finders/data_transfer/mocked_transfer_finder_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe DataTransfer::MockedTransferFinder, feature_category: :source_code_management do
- describe '#execute' do
- subject(:execute) { described_class.new.execute }
-
- it 'returns mock data' do
- expect(execute.first).to include(
- date: '2023-01-01',
- repository_egress: be_a(Integer),
- artifacts_egress: be_a(Integer),
- packages_egress: be_a(Integer),
- registry_egress: be_a(Integer),
- total_egress: be_a(Integer)
- )
-
- expect(execute.size).to eq(12)
- end
- end
-end
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index ef86eba1d1a..1037bd48df6 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -1,5 +1,5 @@
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
-import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
+import { CACHE_KEY } from '~/emoji/constants';
export const validEmoji = {
atom: {
@@ -105,9 +105,8 @@ export function clearEmojiMock() {
initEmojiMap.promise = null;
}
-export async function initEmojiMock(mockData = mockEmojiData) {
+export async function initEmojiMock(data = mockEmojiData) {
clearEmojiMock();
- localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
await initEmojiMap();
}
diff --git a/spec/frontend/__helpers__/local_storage_helper.js b/spec/frontend/__helpers__/local_storage_helper.js
index cf75b0b53fe..367e7ec24ba 100644
--- a/spec/frontend/__helpers__/local_storage_helper.js
+++ b/spec/frontend/__helpers__/local_storage_helper.js
@@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => {
let storage = {};
return {
+ get length() {
+ return Object.keys(storage).length;
+ },
clear: jest.fn(() => {
storage = {};
}),
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 2f057af8a7d..9e0b13c7e6e 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -19,7 +19,11 @@ let wrapper;
let publishReview;
let trackingSpy;
-function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
+function factory({
+ canApprove = true,
+ shouldAnimateReviewButton = false,
+ mrRequestChanges = false,
+} = {}) {
publishReview = jest.fn();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
const requestHandlers = [
@@ -75,6 +79,9 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
wrapper = mountExtended(SubmitDropdown, {
store,
apolloProvider,
+ provide: {
+ glFeatures: { mrRequestChanges },
+ },
});
}
@@ -101,6 +108,7 @@ describe('Batch comments submit dropdown', () => {
note: 'Hello world',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
});
});
@@ -171,4 +179,52 @@ describe('Batch comments submit dropdown', () => {
);
},
);
+
+ describe('when mrRequestChanges feature flag is enabled', () => {
+ it('renders a radio group with review state options', async () => {
+ factory({ mrRequestChanges: true });
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('.gl-form-radio').length).toBe(3);
+ });
+
+ it('renders disabled approve radio button when user can not approve', async () => {
+ factory({ mrRequestChanges: true, canApprove: false });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe(
+ 'disabled',
+ );
+ });
+
+ it.each`
+ value
+ ${'approved'}
+ ${'reviewed'}
+ ${'requested_changes'}
+ `('sends $value review state to api when submitting', async ({ value }) => {
+ factory({ mrRequestChanges: true });
+
+ wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ await waitForPromises();
+
+ await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change');
+
+ findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(publishReview).toHaveBeenCalledWith(expect.anything(), {
+ noteable_type: 'merge_request',
+ noteable_id: 1,
+ note: 'Hello world',
+ approve: false,
+ approval_password: '',
+ reviewer_state: value,
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
index 912fd9e1a93..2a5c24d0515 100644
--- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -10,36 +10,53 @@ describe('CatalogHeader', () => {
let wrapper;
const defaultProps = {};
- const defaultProvide = {
+ const customProvide = {
pageTitle: 'Catalog page',
pageDescription: 'This is a nice catalog page',
};
const findBanner = () => wrapper.findComponent(GlBanner);
const findFeedbackButton = () => findBanner().findComponent(GlButton);
- const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
- const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
+ const findTitle = () => wrapper.find('h1');
+ const findDescription = () => wrapper.findByTestId('description');
- const createComponent = ({ props = {}, stubs = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMountExtended(CatalogHeader, {
propsData: {
...defaultProps,
...props,
},
- provide: defaultProvide,
+ provide,
stubs: {
...stubs,
},
});
};
- it('renders the Catalog title and description', () => {
- createComponent();
+ describe('title and description', () => {
+ describe('when there are no values provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findTitle().exists()).toBe(true);
- expect(findDescription().exists()).toBe(true);
- });
+ it('renders the default values', () => {
+ expect(findTitle().text()).toBe('CI/CD Catalog');
+ expect(findDescription().text()).toBe(
+ 'Discover CI configuration resources for a seamless CI/CD experience.',
+ );
+ });
+ });
+ describe('when custom values are provided', () => {
+ beforeEach(() => {
+ createComponent({ provide: customProvide });
+ });
+ it('renders the custom values', () => {
+ expect(findTitle().text()).toBe(customProvide.pageTitle);
+ expect(findDescription().text()).toBe(customProvide.pageDescription);
+ });
+ });
+ });
describe('Feedback banner', () => {
describe('when user has never dismissed', () => {
beforeEach(() => {
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
new file mode 100644
index 00000000000..e18b418b155
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js
@@ -0,0 +1,211 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createAlert } from '~/alert';
+
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { cacheConfig } from '~/ci/catalog/graphql/settings';
+import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql';
+
+import { emptyCatalogResponseBody, catalogResponseBody } from '../../mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('CiResourcesPage', () => {
+ let wrapper;
+ let catalogResourcesResponse;
+
+ const createComponent = () => {
+ const handlers = [[getCatalogResources, catalogResourcesResponse]];
+ const mockApollo = createMockApollo(handlers, {}, cacheConfig);
+
+ wrapper = shallowMountExtended(ciResourcesPage, {
+ apolloProvider: mockApollo,
+ });
+
+ return waitForPromises();
+ };
+
+ const findCatalogHeader = () => wrapper.findComponent(CatalogHeader);
+ const findCiResourcesList = () => wrapper.findComponent(CiResourcesList);
+ const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+
+ beforeEach(() => {
+ catalogResourcesResponse = jest.fn();
+ });
+
+ describe('when initial queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a loading icon and no list', () => {
+ expect(findLoadingState().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ it('renders the Catalog Header', async () => {
+ await createComponent();
+
+ expect(findCatalogHeader().exists()).toBe(true);
+ });
+
+ describe('and there are no resources', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('renders the empty state', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findCiResourcesList().exists()).toBe(false);
+ });
+ });
+
+ describe('and there are resources', () => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+ it('renders the resources list', () => {
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findCiResourcesList().exists()).toBe(true);
+ });
+
+ it('passes down props to the resources list', () => {
+ expect(findCiResourcesList().props()).toMatchObject({
+ currentPage: 1,
+ resources: nodes,
+ pageInfo,
+ totalCount: count,
+ });
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ it.each`
+ eventName
+ ${'onPrevPage'}
+ ${'onNextPage'}
+ `('refetch query with new params when receiving $eventName', async ({ eventName }) => {
+ const { pageInfo } = catalogResponseBody.data.ciCatalogResources;
+
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+ await createComponent();
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(1);
+
+ await findCiResourcesList().vm.$emit(eventName);
+
+ expect(catalogResourcesResponse).toHaveBeenCalledTimes(2);
+
+ if (eventName === 'onNextPage') {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ after: pageInfo.endCursor,
+ first: 20,
+ });
+ } else {
+ expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
+ before: pageInfo.startCursor,
+ last: 20,
+ first: null,
+ });
+ }
+ });
+ });
+
+ describe('pages count', () => {
+ describe('when the fetchMore call suceeds', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
+
+ await createComponent();
+ });
+
+ it('increments and drecrements the page count correctly', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ await findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ });
+ });
+
+ describe('when the fetchMore call fails', () => {
+ const errorMessage = 'there was an error';
+
+ describe('for next page', () => {
+ beforeEach(async () => {
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not increment the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+
+ describe('for previous page', () => {
+ beforeEach(async () => {
+ // Initial query
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // When clicking on next
+ catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
+ // when clicking on previous
+ catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
+
+ await createComponent();
+ });
+
+ it('does not decrement the page and calls createAlert', async () => {
+ expect(findCiResourcesList().props().currentPage).toBe(1);
+
+ findCiResourcesList().vm.$emit('onNextPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+
+ findCiResourcesList().vm.$emit('onPrevPage');
+ await waitForPromises();
+
+ expect(findCiResourcesList().props().currentPage).toBe(2);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/global_catalog_spec.js b/spec/frontend/ci/catalog/global_catalog_spec.js
new file mode 100644
index 00000000000..fddabf46c0b
--- /dev/null
+++ b/spec/frontend/ci/catalog/global_catalog_spec.js
@@ -0,0 +1,17 @@
+import { shallowMount } from '@vue/test-utils';
+import GlobalCatalog from '~/ci/catalog/global_catalog.vue';
+import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
+
+describe('GlobalCatalog', () => {
+ let wrapper;
+
+ const findHomeComponent = () => wrapper.findComponent(CiCatalogHome);
+
+ beforeEach(() => {
+ wrapper = shallowMount(GlobalCatalog);
+ });
+
+ it('renders the catalog home component', () => {
+ expect(findHomeComponent().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ci/catalog/index_spec.js b/spec/frontend/ci/catalog/index_spec.js
new file mode 100644
index 00000000000..01332cfbb3d
--- /dev/null
+++ b/spec/frontend/ci/catalog/index_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { initCatalog } from '~/ci/catalog/';
+import * as Router from '~/ci/catalog/router';
+import CiResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
+
+describe('~/ci/catalog/index', () => {
+ describe('initCatalog', () => {
+ const SELECTOR = 'SELECTOR';
+
+ let el;
+ let component;
+ const baseRoute = '/explore/catalog';
+
+ const createElement = () => {
+ el = document.createElement('div');
+ el.id = SELECTOR;
+ el.dataset.ciCatalogPath = baseRoute;
+ document.body.appendChild(el);
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ describe('when the element exists', () => {
+ beforeEach(() => {
+ createElement();
+ jest.spyOn(Router, 'createRouter');
+ component = initCatalog(`#${SELECTOR}`);
+ });
+
+ it('returns a Vue Instance', () => {
+ expect(component).toBeInstanceOf(Vue);
+ });
+
+ it('creates a router with the received base path and component', () => {
+ expect(Router.createRouter).toHaveBeenCalledTimes(1);
+ expect(Router.createRouter).toHaveBeenCalledWith(baseRoute, CiResourcesPage);
+ });
+ });
+
+ describe('When the element does not exist', () => {
+ it('returns `null`', () => {
+ expect(initCatalog('foo')).toBe(null);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index 1f24218e494..125f003224c 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -1,5 +1,23 @@
import { componentsMockData } from '~/ci/catalog/constants';
+export const emptyCatalogResponseBody = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 0,
+ nodes: [],
+ },
+ },
+};
+
export const catalogResponseBody = {
data: {
ciCatalogResources: {
diff --git a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
index 6b8a525a15a..dacee556030 100644
--- a/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
+++ b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
@@ -97,6 +97,7 @@ describe('Pipeline details header', () => {
child: false,
latest: true,
mergeTrainPipeline: false,
+ mergedResultsPipeline: false,
invalid: false,
failed: false,
autoDevops: false,
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index a4affdfb7ce..7d6a45fbf30 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -1,9 +1,11 @@
+import MockAdapter from 'axios-mock-adapter';
import {
emojiFixtureMap,
initEmojiMock,
validEmoji,
invalidEmoji,
clearEmojiMock,
+ mockEmojiData,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
import { createMockClient } from 'helpers/mock_apollo_helper';
@@ -16,6 +18,7 @@ import {
getEmojiMap,
emojiFallbackImageSrc,
loadCustomEmojiWithNames,
+ EMOJI_VERSION,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -26,8 +29,11 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
-import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper';
let mockClient;
jest.mock('~/lib/graphql', () => {
@@ -74,6 +80,195 @@ function createMockEmojiClient() {
document.body.dataset.groupFullPath = 'test-group';
}
+describe('retrieval of emojis.json', () => {
+ useLocalStorageSpy();
+
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData);
+ initEmojiMap.promise = null;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ const assertCorrectLocalStorage = () => {
+ expect(localStorage.length).toBe(1);
+ expect(localStorage.getItem(CACHE_KEY)).toBe(
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ };
+
+ const assertEmojiBeingLoadedCorrectly = () => {
+ expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji));
+ };
+
+ it('should remove the old `CACHE_VERSION_KEY`', async () => {
+ localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+
+ await initEmojiMap();
+
+ expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null);
+ });
+
+ describe('when the localStorage is empty', () => {
+ it('should call the API and store results in localStorage', async () => {
+ await initEmojiMap();
+
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores the correct version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should not call the API and not mutate the localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(0);
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores an incorrect version', () => {
+ beforeEach(async () => {
+ localStorage.setItem(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }),
+ );
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores corrupted data', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON");
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage stores data in a different format', () => {
+ beforeEach(async () => {
+ localStorage.setItem(CACHE_KEY, JSON.stringify([]));
+ localStorage.setItem.mockClear();
+ await initEmojiMap();
+ });
+
+ it('should call the API and store results in localStorage', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ assertCorrectLocalStorage();
+ });
+ });
+
+ describe('when the localStorage is full', () => {
+ beforeEach(async () => {
+ const oldSetItem = localStorage.setItem;
+ localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => {
+ if (key === CACHE_KEY) {
+ throw new Error('Storage Full');
+ }
+ oldSetItem(key, value);
+ });
+ await initEmojiMap();
+ });
+
+ it('should call API but not store the results', () => {
+ assertEmojiBeingLoadedCorrectly();
+ expect(mock.history.get.length).toBe(1);
+ expect(localStorage.length).toBe(0);
+ expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ CACHE_KEY,
+ JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
+ );
+ });
+ });
+
+ describe('backwards compatibility', () => {
+ // As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52
+ async function prevImplementation() {
+ if (
+ window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
+ window.localStorage.getItem(CACHE_KEY)
+ ) {
+ return JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ }
+
+ // We load the JSON file direct from the server
+ // because it can't be loaded from a CDN due to
+ // cross domain problems with JSON
+ const { data } = await axios.get(
+ `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
+ );
+
+ try {
+ window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
+ return data;
+ }
+
+ it('Old -> New -> Old should not break', async () => {
+ // The follow steps simulate a multi-version deployment. e.g.
+ // Hitting a page on "regular" .com, then canary, and then "regular" again
+
+ // Load emoji the old way to pre-populate the cache
+ let res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(1);
+ localStorage.setItem.mockClear();
+
+ // Load emoji the new way
+ await initEmojiMap();
+ expect(mock.history.get.length).toBe(2);
+ assertEmojiBeingLoadedCorrectly();
+ assertCorrectLocalStorage();
+ localStorage.setItem.mockClear();
+
+ // Load emoji the old way to pre-populate the cache
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ expect(localStorage.setItem.mock.calls).toEqual([
+ [CACHE_VERSION_KEY, EMOJI_VERSION],
+ [CACHE_KEY, JSON.stringify(mockEmojiData)],
+ ]);
+
+ // Load emoji the old way should work again (and be taken from the cache)
+ res = await prevImplementation();
+ expect(res).toEqual(mockEmojiData);
+ expect(mock.history.get.length).toBe(3);
+ });
+ });
+});
+
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index af6621d5193..c2d88493d71 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -17,6 +17,7 @@ import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
+import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
@@ -44,6 +45,11 @@ describe('GlobalSearchSidebar', () => {
wrapper = shallowMount(GlobalSearchSidebar, {
store,
+ provide: {
+ glFeatures: {
+ searchProjectWikisHideArchivedProjects: true,
+ },
+ },
});
};
@@ -55,6 +61,7 @@ describe('GlobalSearchSidebar', () => {
const findNotesFilters = () => wrapper.findComponent(NotesFilters);
const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters);
+ const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
@@ -85,6 +92,8 @@ describe('GlobalSearchSidebar', () => {
${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
`('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index 4015482b81b..cdd25e90318 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -8,13 +8,13 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'aws/eu-central-1';
-const stateNamePlaceholder = '<YOUR-STATE-NAME>';
const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@@ -67,7 +67,7 @@ describe('InitCommandModal', () => {
describe('init command', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
it('includes correct username', () => {
@@ -94,7 +94,7 @@ describe('InitCommandModal', () => {
describe('on rendering', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`,
+ `-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
});
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 03f1aa356ad..69bc0961240 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -40,8 +40,14 @@ describe('Work Items system note component', () => {
);
});
- it('should render svg icon', () => {
- expect(findTimelineIcon().exists()).toBe(true);
+ it('should render svg icon only for allowed icons', () => {
+ expect(findTimelineIcon().exists()).toBe(false);
+
+ const ALLOWED_ICONS = ['issue-close'];
+ ALLOWED_ICONS.forEach((icon) => {
+ createComponent({ note: { ...workItemSystemNoteWithMetadata, systemNoteIconName: icon } });
+ expect(findTimelineIcon().exists()).toBe(true);
+ });
});
it('should not show compare previous version for FOSS', () => {
diff --git a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
index 4ea3d287454..b5bffbc8803 100644
--- a/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/group_data_transfer_resolver_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+ let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
let(:finder_results) do
[
build(:project_data_transfer, date: to, repository_egress: 250000)
@@ -41,21 +42,12 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
+ it 'calls GroupDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
+ group: group, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls GroupDataTransferFinder with expected arguments' do
- expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
- group: group, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
-
- expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
end
end
diff --git a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
index 7307c1a54a9..25ff02218cf 100644
--- a/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
+++ b/spec/graphql/resolvers/data_transfer/project_data_transfer_resolver_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
+
+ let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
+
let(:finder_results) do
[
{
@@ -44,21 +47,12 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
include_examples 'Data transfer resolver'
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
-
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls ProjectDataTransferFinder with expected arguments' do
- expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
- project: project, from: from, to: to, user: current_user
- ).once.and_return(finder)
- allow(finder).to receive(:execute).once.and_return(finder_results)
+ it 'calls ProjectDataTransferFinder with expected arguments' do
+ expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
+ project: project, from: from, to: to, user: current_user).once.and_return(finder)
+ allow(finder).to receive(:execute).once.and_return(finder_results)
- expect(resolve_egress).to eq({ egress_nodes: finder_results })
- end
+ expect(resolve_egress).to eq({ egress_nodes: finder_results })
end
end
diff --git a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
index a93da279b7f..80ead81650e 100644
--- a/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
+++ b/spec/graphql/types/data_transfer/project_data_transfer_type_spec.rb
@@ -14,25 +14,15 @@ RSpec.describe GitlabSchema.types['ProjectDataTransfer'], feature_category: :sou
let_it_be(:project) { create(:project) }
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
- let(:finder_result) { 40_000_000 }
+ let(:relation) { instance_double(ActiveRecord::Relation) }
- it 'returns mock data' do
- expect(resolve_field(:total_egress, { from: from, to: to }, extras: { parent: project },
- arg_style: :internal)).to eq(finder_result)
+ before do
+ allow(relation).to receive(:sum).and_return(10)
end
- context 'when data_transfer_monitoring_mock_data is disabled' do
- let(:relation) { instance_double(ActiveRecord::Relation) }
-
- before do
- allow(relation).to receive(:sum).and_return(10)
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- end
-
- it 'calls sum on active record relation' do
- expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
- arg_style: :internal)).to eq(10)
- end
+ it 'calls sum on active record relation' do
+ expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
+ arg_style: :internal)).to eq(10)
end
end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 40798b4c038..264137add8a 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -2,7 +2,9 @@
require "spec_helper"
-RSpec.describe AuthHelper do
+RSpec.describe AuthHelper, feature_category: :system_access do
+ include LoginHelpers
+
describe "button_based_providers" do
it 'returns all enabled providers from devise' do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
@@ -310,88 +312,16 @@ RSpec.describe AuthHelper do
end
end
- describe '#auth_strategy_class' do
- subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
-
- context 'when configuration specifies no provider' do
- let(:name) { 'does_not_exist' }
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args but without strategy_class' do
- let(:name) { 'google_oauth2' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'app_id' => 'YOUR_APP_ID'
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
-
- context 'when configuration specifies a provider with args and strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- name,
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns the class' do
- expect(auth_strategy_class).to eq(strategy)
- end
- end
-
- context 'when configuration specifies another provider with args and another strategy_class' do
- let(:name) { 'provider1' }
- let(:strategy) { 'OmniAuth::Strategies::LDAP' }
- let(:provider) do
- Struct.new(:name, :args).new(
- 'another_name',
- 'strategy_class' => strategy
- )
- end
-
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
- end
-
- it 'returns false' do
- expect(auth_strategy_class).to be_falsey
- end
- end
- end
-
describe '#saml_providers' do
subject(:saml_providers) { helper.saml_providers }
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
- let(:saml_provider_1_name) { 'saml_provider_1' }
+ let(:saml_provider_1_name) { 'saml' }
let(:saml_provider_1) do
Struct.new(:name, :args).new(
saml_provider_1_name,
- 'strategy_class' => saml_strategy
+ {}
)
end
@@ -422,7 +352,7 @@ RSpec.describe AuthHelper do
context 'when SAML is enabled without specifying a strategy class' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the saml provider' do
@@ -432,8 +362,7 @@ RSpec.describe AuthHelper do
context 'when configuration specifies no provider' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ stub_omniauth_config(providers: [])
end
it 'returns an empty list' do
@@ -443,30 +372,27 @@ RSpec.describe AuthHelper do
context 'when configuration specifies a provider with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
+ stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym])
end
end
context 'when configuration specifies two providers with a SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
+ stub_omniauth_config(providers: [saml_provider_1, saml_provider_2])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
context 'when configuration specifies a provider with a non-SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
+ stub_omniauth_config(providers: [ldap_provider])
end
it 'returns an empty list' do
@@ -476,12 +402,11 @@ RSpec.describe AuthHelper do
context 'when configuration specifies four providers but only two with SAML strategy_class' do
before do
- allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
+ stub_omniauth_config(providers: [saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
end
it 'returns the provider' do
- expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
end
diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
index b6e1d59f6c0..5265b608ab4 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:category) { 'test_category' }
@@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(Gitlab::UsageMetricDefinitionGenerator) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
index f7a4bac39d7..e0cb74d8559 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
+RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:key_path) { 'counts_weekly.test_metric' }
@@ -14,6 +14,10 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
+ end
end
after do
@@ -100,4 +104,19 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
expect(files.count).to eq(2)
end
end
+
+ ['n', 'N', 'random word', nil].each do |answer|
+ context "when user agreed with deprecation warning by typing: #{answer}" do
+ it 'does not create definition file' do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:ask).and_return(answer)
+ end
+
+ described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name }).invoke_all
+ files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml'))
+
+ expect(files.count).to eq(0)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/config_spec.rb b/spec/lib/gitlab/auth/saml/config_spec.rb
index d657622c9f2..2ecc26f9b96 100644
--- a/spec/lib/gitlab/auth/saml/config_spec.rb
+++ b/spec/lib/gitlab/auth/saml/config_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::Config do
+ include LoginHelpers
+
describe '.enabled?' do
subject { described_class.enabled? }
@@ -10,7 +12,7 @@ RSpec.describe Gitlab::Auth::Saml::Config do
context 'when SAML is enabled' do
before do
- allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
+ stub_basic_saml_config
end
it { is_expected.to eq(true) }
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 07a06025c0f..f8a4d8023c1 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -42,15 +42,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
context 'when Redis calls are made' do
let_it_be(:redis_store_class) { define_helper_redis_store_class }
- before do # init redis connection with `test` env details
+ before do
redis_store_class.with(&:ping)
Gitlab::Redis::Queues.with(&:ping)
RequestStore.clear!
end
- it 'adds Redis data and omits Gitaly data' do
- stub_rails_env('staging') # to avoid raising CrossSlotError
+ it 'adds Redis data including cross slot calls' do
+ expect(Gitlab::Instrumentation::RedisBase)
+ .to receive(:raise_cross_slot_validation_errors?)
+ .once.and_return(false)
+
redis_store_class.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') }
end
diff --git a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
index e4cd0d9c006..4bd4455d1bd 100644
--- a/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
+++ b/spec/lib/gitlab/seeders/ci/catalog/resource_seeder_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe ::Gitlab::Seeders::Ci::Catalog::ResourceSeeder, feature_category:
context 'when ci resource creation fails' do
before do
- allow_next_instance_of(::Ci::Catalog::AddResourceService) do |service|
+ allow_next_instance_of(::Ci::Catalog::Resources::CreateService) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
end
end
diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb
index b7c038afa54..e17074a0247 100644
--- a/spec/requests/api/graphql/group/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/group/data_transfer_spec.rb
@@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management
context 'when user has enough permissions' do
before do
group.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
index e46b614f02e..f990cab55f4 100644
--- a/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/catalog/resources/create_spec.rb
@@ -36,19 +36,5 @@ RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition
expect(response).to have_gitlab_http_status(:success)
end
end
-
- context 'with an invalid project' do
- let_it_be(:project) { create(:project, :repository) }
-
- before_all do
- project.add_owner(current_user)
- end
-
- it 'returns an error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(graphql_mutation_response(:catalog_resources_create)['errors']).not_to be_empty
- end
- end
end
end
diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb
index aafa8d65eb9..79b2b10419b 100644
--- a/spec/requests/api/graphql/project/data_transfer_spec.rb
+++ b/spec/requests/api/graphql/project/data_transfer_spec.rb
@@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme
context 'when user has enough permissions' do
before do
project.add_owner(current_user)
+ subject
end
- context 'when data_transfer_monitoring_mock_data is NOT enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: false)
- subject
- end
-
- it 'returns real results' do
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
- expect(egress_data.count).to eq(2)
+ expect(egress_data.count).to eq(2)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
- expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
- end
-
- it_behaves_like 'a working graphql query'
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
end
- context 'when data_transfer_monitoring_mock_data is enabled' do
- before do
- stub_feature_flags(data_transfer_monitoring_mock_data: true)
- subject
- end
-
- it 'returns mock results' do
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(egress_data.count).to eq(12)
- expect(egress_data.first.keys).to match_array(
- %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
- )
- end
-
- it_behaves_like 'a working graphql query'
- end
+ it_behaves_like 'a working graphql query'
end
end
diff --git a/spec/services/ci/catalog/add_resource_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb
index 145d04d457c..202c76acaec 100644
--- a/spec/services/ci/catalog/add_resource_service_spec.rb
+++ b/spec/services/ci/catalog/resources/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do
+RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :catalog_resource_with_components) }
let_it_be(:user) { create(:user) }
@@ -32,20 +32,6 @@ RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_comp
end
end
- context 'with an invalid project' do
- let_it_be(:project) { create(:project, :repository) }
-
- before_all do
- project.add_owner(user)
- end
-
- it 'does not create a catalog resource' do
- response = service.execute
-
- expect(response.message).to eq('Project must have a description, Project must contain components')
- end
- end
-
context 'with an invalid catalog resource' do
it 'does not save the catalog resource' do
catalog_resource = instance_double(::Ci::Catalog::Resource,
diff --git a/spec/services/ci/enqueue_job_service_spec.rb b/spec/services/ci/enqueue_job_service_spec.rb
index c2bb0bb2bb5..85983651148 100644
--- a/spec/services/ci/enqueue_job_service_spec.rb
+++ b/spec/services/ci/enqueue_job_service_spec.rb
@@ -78,4 +78,33 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
execute
end
end
+
+ context 'when the job is manually triggered another user' do
+ let(:job_variables) do
+ [{ key: 'third', secret_value: 'third' },
+ { key: 'fourth', secret_value: 'fourth' }]
+ end
+
+ let(:service) do
+ described_class.new(build, current_user: user, variables: job_variables)
+ end
+
+ it 'assigns the user and variables to the job', :aggregate_failures do
+ called = false
+ service.execute do
+ unless called
+ called = true
+ raise ActiveRecord::StaleObjectError
+ end
+
+ build.enqueue!
+ end
+
+ build.reload
+
+ expect(called).to be true # ensure we actually entered the failure path
+ expect(build.user).to eq(user)
+ expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
+ end
+ end
end
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index 0af4de11d51..e60cc4278af 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -70,4 +70,3 @@
- UploaderFinder
- UserGroupNotificationSettingsFinder
- UserGroupsCounter
-- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 1a89cf4cc37..d35fa801638 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -262,19 +262,15 @@ module LoginHelpers
end
def stub_omniauth_config(messages)
- allow(Gitlab.config.omniauth).to receive_messages(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(GitlabSettings::Options.build(messages))
end
def stub_basic_saml_config
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', args: {} }])
end
def stub_saml_group_config(groups)
- allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
- allow(config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
- end
+ stub_omniauth_config(providers: [{ name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} }])
end
end
diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
index 8551bd052ce..c50e0434eb1 100644
--- a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb
@@ -1,16 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'Data transfer resolver' do
- it 'returns mock data' do |_query_object|
- mocked_data = ['mocked_data']
-
- allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance|
- allow(instance).to receive(:execute).and_return(mocked_data)
- end
-
- expect(resolve_egress[:egress_nodes]).to eq(mocked_data)
- end
-
context 'when data_transfer_monitoring is disabled' do
before do
stub_feature_flags(data_transfer_monitoring: false)