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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-01 00:09:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-01 00:09:09 +0300
commit404895390afe87ce8ab939448bf7dff7dc4b7169 (patch)
tree93c323d7df6b70c84dce7b3e4e4f3d57180394a0
parente9885f7a36065b9b45a35feb6c427c7742a906a4 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitpod.yml9
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue149
-rw-r--r--app/assets/javascripts/pipelines/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql (renamed from app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql)0
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql (renamed from app/assets/javascripts/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql)0
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue45
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue66
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js18
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue42
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue56
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js2
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss4
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb1
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/helpers/sidebars_helper.rb32
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/views/devise/sessions/email_verification.haml4
-rw-r--r--app/views/groups/settings/_advanced.html.haml4
-rw-r--r--app/views/groups/settings/_lfs.html.haml4
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/shared/_broadcast_message.html.haml25
-rw-r--r--config/feature_flags/development/ci_graphql_pipeline_mini_graph.yml8
-rw-r--r--config/feature_flags/development/command_palette.yml8
-rw-r--r--db/migrate/20230328150343_add_retried_at_to_status_check_responses.rb7
-rw-r--r--db/migrate/20230522132239_add_model_experiments_access_level_to_project_feature.rb15
-rw-r--r--db/schema_migrations/202303281503431
-rw-r--r--db/schema_migrations/202305221322391
-rw-r--r--db/structure.sql6
-rw-r--r--doc/integration/arkose.md4
-rw-r--r--doc/security/identity_verification.md1
-rw-r--r--doc/user/admin_area/index.md81
-rw-r--r--doc/user/project/service_desk.md132
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/import_export/project/import_export.yml2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock14
-rw-r--r--rubocop/cop/gitlab/feature_available_usage.rb1
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js31
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js44
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js123
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js150
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js70
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js40
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js17
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js59
-rw-r--r--spec/helpers/sidebars_helper_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/project_feature_spec.rb1
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/requests/api/project_attributes.yml1
57 files changed, 1213 insertions, 142 deletions
diff --git a/.gitpod.yml b/.gitpod.yml
index 684f9a8e5a2..3ec5e59184b 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -51,14 +51,9 @@ tasks:
[[ -f /workspace/gitpod_start_time.sh ]] && source /workspace/gitpod_start_time.sh
SECONDS=0
cd /workspace/gitlab-development-kit
- make gitlab-update
- make gitlab-shell-update
- make gitlab-workhorse-update
# update GDK
- if [ "$GITLAB_UPDATE_GDK" == true ]; then
- echo "$(date) – Updating GDK" | tee -a /workspace/startup.log
- gdk update
- fi
+ echo "$(date) – Updating GDK" | tee -a /workspace/startup.log
+ gdk update
# ensure gdk.yml has correct instance settings
gdk config set gitlab.rails.hostname $(gp url 3000 | sed -e 's+^http[s]*://++')
gdk config set gitlab.rails.port 443
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index a4dfb401f4c..656b1a6c347 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -2,7 +2,7 @@
import { __ } from '~/locale';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '../../constants';
export default {
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 372f04075ab..bb79a4d74da 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -9,7 +9,9 @@ import {
getQueryHeaders,
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@@ -32,11 +34,13 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
+ GraphqlPipelineMiniGraph,
PipelineEditorMiniGraph,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -106,6 +110,9 @@ export default {
hasPipelineData() {
return Boolean(this.pipeline?.id);
},
+ isUsingPipelineMiniGraphQueries() {
+ return this.glFeatures.ciGraphqlPipelineMiniGraph;
+ },
pipelineId() {
return getIdFromGraphQLId(this.pipeline.id);
},
@@ -171,8 +178,14 @@ export default {
</gl-sprintf>
</span>
</div>
- <div class="gl-display-flex gl-flex-wrap">
- <pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
+ <div class="gl-display-flex gl-flex-wrap-wrap">
+ <graphql-pipeline-mini-graph
+ v-if="isUsingPipelineMiniGraphQueries"
+ :full-path="projectFullPath"
+ :iid="pipeline.iid"
+ :pipeline-etag="pipelineEtag"
+ />
+ <pipeline-editor-mini-graph v-else :pipeline="pipeline" v-on="$listeners" />
<gl-button
class="gl-ml-3"
category="secondary"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue
new file mode 100644
index 00000000000..91630d4cfd4
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue
@@ -0,0 +1,149 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
+import {
+ getQueryHeaders,
+ toggleQueryPollingByVisibility,
+} from '~/pipelines/components/graph/utils';
+import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import PipelineMiniGraph from './pipeline_mini_graph.vue';
+
+export default {
+ i18n: {
+ linkedPipelinesFetchError: __('There was a problem fetching linked pipelines.'),
+ stagesFetchError: __('There was a problem fetching the pipeline stages.'),
+ },
+ components: {
+ GlLoadingIcon,
+ PipelineMiniGraph,
+ },
+ props: {
+ pipelineEtag: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ isMergeTrain: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pollInterval: {
+ type: Number,
+ required: false,
+ default: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
+ },
+ },
+ data() {
+ return {
+ linkedPipelines: null,
+ pipelineStages: [],
+ };
+ },
+ apollo: {
+ linkedPipelines: {
+ context() {
+ return getQueryHeaders(this.pipelineEtag);
+ },
+ query: getLinkedPipelinesQuery,
+ pollInterval() {
+ return this.pollInterval;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update({ project }) {
+ return project?.pipeline || this.linkedpipelines;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.linkedPipelinesFetchError });
+ },
+ },
+ pipelineStages: {
+ context() {
+ return getQueryHeaders(this.pipelineEtag);
+ },
+ query: getPipelineStagesQuery,
+ pollInterval() {
+ return this.pollInterval;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update({ project }) {
+ return project?.pipeline?.stages?.nodes || this.pipelineStages;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.stagesFetchError });
+ },
+ },
+ },
+ computed: {
+ downstreamPipelines() {
+ return keepLatestDownstreamPipelines(this.linkedPipelines?.downstream?.nodes);
+ },
+ formattedStages() {
+ return this.pipelineStages.map((stage) => {
+ const { name, detailedStatus } = stage;
+ return {
+ // TODO: Once we fetch stage by ID with GraphQL,
+ // this method will change.
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/384853
+ id: stage.id,
+ dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`,
+ name,
+ path: `${this.pipelinePath}#${name}`,
+ status: {
+ details_path: `${this.pipelinePath}#${name}`,
+ has_details: detailedStatus?.hasDetails || false,
+ ...detailedStatus,
+ },
+ title: `${name}: ${detailedStatus?.text || ''}`,
+ };
+ });
+ },
+ pipelinePath() {
+ return this.linkedPipelines?.path || '';
+ },
+ upstreamPipeline() {
+ return this.linkedPipelines?.upstream;
+ },
+ },
+ mounted() {
+ toggleQueryPollingByVisibility(this.$apollo.queries.linkedPipelines);
+ toggleQueryPollingByVisibility(this.$apollo.queries.pipelineStages);
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.queries.pipelineStages.loading" />
+ <pipeline-mini-graph
+ v-else
+ data-testid="graphql-pipeline-mini-graph"
+ :downstream-pipelines="downstreamPipelines"
+ :is-merge-train="isMergeTrain"
+ :pipeline-path="pipelinePath"
+ :stages="formattedStages"
+ :upstream-pipeline="upstreamPipeline"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index d092c3ca630..a6dd835bb15 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -110,3 +110,7 @@ export const TRACKING_CATEGORIES = {
tabs: 'pipelines_filter_tabs',
search: 'pipelines_filtered_search',
};
+
+// Pipeline Mini Graph
+
+export const PIPELINE_MINI_GRAPH_POLL_INTERVAL = 5000;
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql
index 9257cc7de7b..9257cc7de7b 100644
--- a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql
index 69a29947b16..69a29947b16 100644
--- a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 54d13ecc9c8..84e7edb48c1 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -7,10 +7,12 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
import { formatStages } from '../utils';
-import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '../graphql/queries/get_pipeline_stages.query.graphql';
import { COMMIT_BOX_POLL_INTERVAL } from '../constants';
export default {
@@ -21,8 +23,10 @@ export default {
},
components: {
GlLoadingIcon,
+ GraphqlPipelineMiniGraph,
PipelineMiniGraph,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
fullPath: {
default: '',
@@ -47,15 +51,15 @@ export default {
},
query: getLinkedPipelinesQuery,
pollInterval: COMMIT_BOX_POLL_INTERVAL,
+ skip() {
+ return !this.fullPath || !this.iid || this.isUsingPipelineMiniGraphQueries;
+ },
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
- skip() {
- return !this.fullPath || !this.iid;
- },
update({ project }) {
return project?.pipeline;
},
@@ -69,6 +73,9 @@ export default {
},
query: getPipelineStagesQuery,
pollInterval: COMMIT_BOX_POLL_INTERVAL,
+ skip() {
+ return this.isUsingPipelineMiniGraphQueries;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -95,6 +102,9 @@ export default {
const downstream = this.pipeline?.downstream?.nodes;
return keepLatestDownstreamPipelines(downstream);
},
+ isUsingPipelineMiniGraphQueries() {
+ return this.glFeatures.ciGraphqlPipelineMiniGraph;
+ },
pipelinePath() {
return this.pipeline?.path ?? '';
},
@@ -128,13 +138,22 @@ export default {
<template>
<div>
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
- <pipeline-mini-graph
- v-else
- data-testid="commit-box-pipeline-mini-graph"
- :downstream-pipelines="downstreamPipelines"
- :pipeline-path="pipelinePath"
- :stages="formattedStages"
- :upstream-pipeline="upstreamPipeline"
- />
+ <template v-else>
+ <graphql-pipeline-mini-graph
+ v-if="isUsingPipelineMiniGraphQueries"
+ data-testid="commit-box-pipeline-mini-graph"
+ :pipeline-etag="graphqlResourceEtag"
+ :full-path="fullPath"
+ :iid="iid"
+ />
+ <pipeline-mini-graph
+ v-else
+ data-testid="commit-box-pipeline-mini-graph"
+ :downstream-pipelines="downstreamPipelines"
+ :pipeline-path="pipelinePath"
+ :stages="formattedStages"
+ :upstream-pipeline="upstreamPipeline"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
new file mode 100644
index 00000000000..cfb4c1e447d
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { COMMON_HANDLES, COMMAND_HANDLE, COMMANDS_GROUP_TITLE } from './constants';
+
+export default {
+ name: 'CommandPaletteItems',
+ components: {
+ GlDisclosureDropdownGroup,
+ },
+ inject: ['commandPaletteData'],
+ props: {
+ searchQuery: {
+ type: String,
+ required: true,
+ },
+ handle: {
+ type: String,
+ required: true,
+ validator: (value) => {
+ return COMMON_HANDLES.includes(value);
+ },
+ },
+ },
+ computed: {
+ isCommandMode() {
+ return this.handle === COMMAND_HANDLE;
+ },
+ filteredCommands() {
+ return this.searchQuery
+ ? fuzzaldrinPlus.filter(this.commands, this.searchQuery, {
+ key: 'keywords',
+ })
+ : this.commands;
+ },
+ commandsGroup() {
+ return {
+ name: COMMANDS_GROUP_TITLE,
+ items: this.filteredCommands,
+ };
+ },
+ commands() {
+ return this.commandPaletteData.map(({ text, href, keywords = [] }) => ({
+ text,
+ href,
+ keywords: keywords.join(''),
+ }));
+ },
+ hasResults() {
+ return this.commandsGroup.items?.length;
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-p-0 gl-m-0 gl-list-style-none">
+ <gl-disclosure-dropdown-group
+ v-if="hasResults"
+ :group="commandsGroup"
+ bordered
+ class="gl-mt-0!"
+ />
+ <div v-else class="gl-text-gray-700 gl-pl-5 gl-py-3">{{ __('No results found') }}</div>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
new file mode 100644
index 00000000000..e2d325258ae
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -0,0 +1,18 @@
+import { s__, sprintf } from '~/locale';
+
+export const COMMAND_HANDLE = '>';
+
+export const COMMON_HANDLES = [COMMAND_HANDLE];
+export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
+ s__('CommandPalette|Type %{commandHandle} for command or search...'),
+ {
+ commandHandle: COMMAND_HANDLE,
+ },
+ false,
+);
+
+export const SEARCH_SCOPE = {
+ [COMMAND_HANDLE]: s__('CommandPalette|command'),
+};
+
+export const COMMANDS_GROUP_TITLE = s__('CommandPalette|Commands');
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
new file mode 100644
index 00000000000..201d21f56fe
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
@@ -0,0 +1,42 @@
+<script>
+import { COMMON_HANDLES, SEARCH_SCOPE } from './constants';
+
+export default {
+ name: 'FakeSearchInput',
+ props: {
+ userInput: {
+ type: String,
+ required: true,
+ },
+ scope: {
+ type: String,
+ required: true,
+ validator: (value) => COMMON_HANDLES.includes(value),
+ },
+ },
+ computed: {
+ placeholder() {
+ return SEARCH_SCOPE[this.scope];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-pointer-events-none fake-input">
+ <span class="gl-opacity-0" data-testid="search-scope">{{ scope }}&nbsp;</span>
+ <span
+ v-if="!userInput"
+ data-testid="search-scope-placeholder"
+ class="gl-text-gray-500 gl-pointer-events-none"
+ >{{ placeholder }}</span
+ >
+ </div>
+</template>
+
+<style scoped>
+.fake-input {
+ top: 12px;
+ left: 33px;
+}
+</style>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index 55c28661440..2534e62a301 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -24,6 +24,7 @@ import {
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
} from '~/vue_shared/global_search/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
@@ -35,6 +36,9 @@ import {
SEARCH_INPUT_SELECTOR,
SEARCH_RESULTS_ITEM_SELECTOR,
} from '../constants';
+import CommandPaletteItems from '../command_palette/command_palette_items.vue';
+import FakeSearchInput from '../command_palette/fake_search_input.vue';
+import { COMMAND_HANDLE, SEARCH_OR_COMMAND_MODE_PLACEHOLDER } from '../command_palette/constants';
import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue';
import GlobalSearchDefaultItems from './global_search_default_items.vue';
import GlobalSearchScopedItems from './global_search_scoped_items.vue';
@@ -60,7 +64,10 @@ export default {
GlIcon,
GlToken,
GlModal,
+ CommandPaletteItems,
+ FakeSearchInput,
},
+ mixins: [glFeatureFlagMixin()],
computed: {
...mapState(['search', 'loading', 'searchContext']),
...mapGetters(['searchQuery', 'searchOptions', 'scopedSearchOptions']),
@@ -72,6 +79,9 @@ export default {
this.setSearch(value);
},
},
+ searchPlaceholder() {
+ return this.glFeatures?.commandPalette ? SEARCH_OR_COMMAND_MODE_PLACEHOLDER : SEARCH_GITLAB;
+ },
showDefaultItems() {
return !this.searchText;
},
@@ -104,7 +114,7 @@ export default {
};
},
showScopeHelp() {
- return this.searchTermOverMin;
+ return this.searchTermOverMin && !this.isCommandMode;
},
searchBarItem() {
return this.searchOptions?.[0];
@@ -120,10 +130,26 @@ export default {
scope: this.infieldHelpContent,
});
},
+
+ searchTextFirstChar() {
+ return this.searchText?.trim().charAt(0);
+ },
+ isCommandMode() {
+ return this.glFeatures?.commandPalette && this.searchTextFirstChar === COMMAND_HANDLE;
+ },
+ commandPaletteQuery() {
+ if (this.isCommandMode) {
+ return this.searchText?.trim().substring(1);
+ }
+ return '';
+ },
},
methods: {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
+ if (this.isCommandMode) {
+ return;
+ }
if (!searchTerm) {
this.clearAutocomplete();
} else {
@@ -222,12 +248,12 @@ export default {
>
<form
role="search"
- :aria-label="$options.i18n.SEARCH_GITLAB"
+ :aria-label="searchPlaceholder"
class="gl-relative gl-rounded-base gl-w-full"
:class="searchBarClasses"
data-testid="global-search-form"
>
- <div class="gl-p-1">
+ <div class="gl-p-1 gl-relative">
<gl-search-box-by-type
id="search"
ref="searchInputBox"
@@ -236,7 +262,7 @@ export default {
data-testid="global-search-input"
data-qa-selector="global_search_input"
autocomplete="off"
- :placeholder="$options.i18n.SEARCH_GITLAB"
+ :placeholder="searchPlaceholder"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
borderless
@input="getAutocompleteOptions"
@@ -266,6 +292,13 @@ export default {
<span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">
{{ $options.i18n.SEARCH_DESCRIBED_BY_WITH_RESULTS }}
</span>
+
+ <fake-search-input
+ v-if="isCommandMode"
+ :user-input="commandPaletteQuery"
+ :scope="searchTextFirstChar"
+ class="gl-absolute"
+ />
</div>
<span
role="region"
@@ -282,13 +315,20 @@ export default {
class="global-search-results gl-overflow-y-auto gl-w-full gl-pb-2"
@keydown="onKeydown"
>
- <global-search-default-items v-if="showDefaultItems" />
+ <command-palette-items
+ v-if="isCommandMode"
+ :search-query="commandPaletteQuery"
+ :handle="searchTextFirstChar"
+ />
+
<template v-else>
- <global-search-scoped-items v-if="showScopedSearchItems" />
- <global-search-autocomplete-items />
+ <global-search-default-items v-if="showDefaultItems" />
+ <template v-else>
+ <global-search-scoped-items v-if="showScopedSearchItems" />
+ <global-search-autocomplete-items />
+ </template>
</template>
</div>
-
<template v-if="searchContext">
<input
v-if="searchContext.group"
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 63424277ffc..c6392cbc452 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -72,6 +72,7 @@ export const initSuperSidebar = () => {
const sidebarData = JSON.parse(sidebar);
const searchData = convertObjectPropsToCamelCase(sidebarData.search);
+ const commandPaletteData = convertObjectPropsToCamelCase(sidebarData.command_palette_commands);
const { searchPath, issuesPath, mrPath, autocompletePath, searchContext } = searchData;
const isImpersonating = parseBoolean(sidebarData.is_impersonating);
@@ -85,6 +86,7 @@ export const initSuperSidebar = () => {
toggleNewNavEndpoint,
isImpersonating,
...getTrialStatusWidgetData(sidebarData),
+ commandPaletteData,
},
store: createStore({
searchPath,
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index a0bfca79dc3..f81371828f2 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -46,3 +46,7 @@
min-height: 34px;
}
}
+
+.gl-broadcast-message-content p:last-child {
+ margin: 0;
+}
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 01c34a74b84..8499bf0ced7 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -5,6 +5,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:ci_job_assistant_drawer, @project)
push_frontend_feature_flag(:ai_ci_config_generator, @user)
+ push_frontend_feature_flag(:ci_graphql_pipeline_mini_graph, @project)
end
feature_category :pipeline_composition
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 8aca6a3fd5b..ca8382fa301 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -19,6 +19,9 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_commit_box_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
+ before_action do
+ push_frontend_feature_flag(:ci_graphql_pipeline_mini_graph, @project)
+ end
BRANCH_SEARCH_LIMIT = 1000
COMMIT_DIFFS_PER_PAGE = 20
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 02a912d0227..02b705a4e0d 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -89,7 +89,9 @@ module SidebarsHelper
update_pins_url: pins_url,
is_impersonating: impersonating?,
stop_impersonation_path: admin_impersonation_path,
- shortcut_links: shortcut_links(user, project: project)
+ shortcut_links: shortcut_links(user, project: project),
+ # command palette
+ command_palette_commands: create_command_palette_menu
}
end
@@ -171,6 +173,34 @@ module SidebarsHelper
end
end
+ def create_command_palette_menu
+ menu_items = []
+
+ if current_user.can_create_project?
+ menu_items.push({
+ text: _('New project/repository'),
+ href: new_project_path,
+ keywords: [_('Create a new project/repository')]
+ })
+ end
+
+ if current_user.can_create_group?
+ menu_items.push({
+ text: _('New group'),
+ href: new_group_path,
+ keywords: ['Create a new group']
+ })
+ end
+
+ return unless current_user.can?(:create_snippet)
+
+ menu_items.push({
+ text: _('New snippet'),
+ href: new_snippet_path,
+ keywords: ['Create a new snippet']
+ })
+ end
+
def create_merge_request_menu(user)
[
{
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index b910c0ab5c2..76c733b1c0b 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -114,6 +114,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:infrastructure_access_level, value)
end
+ def model_experiments_access_level=(value)
+ write_feature_attribute_string(:model_experiments_access_level, value)
+ end
+
# TODO: Remove this method after we drop support for project create/edit APIs to set the
# container_registry_enabled attribute. They can instead set the container_registry_access_level
# attribute.
diff --git a/app/models/project.rb b/app/models/project.rb
index 167b9283452..2e94596de5a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -491,6 +491,7 @@ class Project < ApplicationRecord
:operations_access_level, :security_and_compliance_access_level,
:container_registry_access_level, :environments_access_level, :feature_flags_access_level,
:monitor_access_level, :releases_access_level, :infrastructure_access_level,
+ :model_experiments_access_level,
to: :project_feature, allow_nil: true
delegate :show_default_award_emojis, :show_default_award_emojis=,
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 772a82fa173..3c533b4a60d 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -26,6 +26,7 @@ class ProjectFeature < ApplicationRecord
feature_flags
releases
infrastructure
+ model_experiments
].freeze
EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze
@@ -79,6 +80,7 @@ class ProjectFeature < ApplicationRecord
attribute :infrastructure_access_level, default: ENABLED
attribute :feature_flags_access_level, default: ENABLED
attribute :environments_access_level, default: ENABLED
+ attribute :model_experiments_access_level, default: ENABLED
attribute :package_registry_access_level, default: -> do
if ::Gitlab.config.packages.enabled
diff --git a/app/views/devise/sessions/email_verification.haml b/app/views/devise/sessions/email_verification.haml
index 6cafcb941b4..e0b5a266961 100644
--- a/app/views/devise/sessions/email_verification.haml
+++ b/app/views/devise/sessions/email_verification.haml
@@ -2,7 +2,7 @@
= render 'devise/shared/tab_single', tab_title: s_('IdentityVerification|Help us protect your account')
.login-box.gl-p-5
.login-body
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'gl-show-field-errors' }) do |f|
+ = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'gl-show-field-errors' }) do |f|
%p
= s_("IdentityVerification|For added security, you'll need to verify your identity. We've sent a verification code to %{email}").html_safe % { email: "<strong>#{sanitize(obfuscated_email(resource.email))}</strong>".html_safe }
%div
@@ -11,7 +11,7 @@
%p.gl-field-error.gl-mt-2
= resource.errors.full_messages.to_sentence
.gl-mt-5
- = f.submit s_('IdentityVerification|Verify code'), class: 'gl-button btn btn-confirm'
+ = f.submit s_('IdentityVerification|Verify code'), class: 'gl-w-full', pajamas_button: true
- unless send_rate_limited?(resource)
= link_to s_('IdentityVerification|Resend code'), users_resend_verification_code_path, method: :post, class: 'form-control gl-button btn-link gl-mt-3 gl-mb-0'
%p.gl-p-5.gl-text-secondary
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index 21b1986bd34..d92a6b08b60 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -3,7 +3,7 @@
.sub-section
%h4.warning-title= s_('GroupSettings|Change group URL')
- = form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
+ = gitlab_ui_form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
.form-group
%p
@@ -23,7 +23,7 @@
title: group_url_error_message,
maxlength: ::Namespace::URL_MAX_LENGTH,
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- = f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-danger'
+ = f.submit s_('GroupSettings|Change group URL'), class: 'btn-danger', pajamas_button: true
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
diff --git a/app/views/groups/settings/_lfs.html.haml b/app/views/groups/settings/_lfs.html.haml
index 9f04b579a97..74f9298133b 100644
--- a/app/views/groups/settings/_lfs.html.haml
+++ b/app/views/groups/settings/_lfs.html.haml
@@ -7,6 +7,6 @@
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :lfs_enabled,
- _('Allow projects within this group to use Git LFS'),
- help_text: _('Can be overridden in each project.'),
+ _('Projects in this group can use Git LFS'),
+ help_text: _('Possible to override in each project.'),
checkbox_options: { checked: @group.lfs_enabled?, data: { qa_selector: 'lfs_checkbox' } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 02a69f25985..fd90a941c7b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -91,7 +91,7 @@
.sub-section.rename-repository
%h4.warning-title= _('Change path')
= render 'projects/errors'
- = form_for @project do |f|
+ = gitlab_ui_form_for @project do |f|
.form-group
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'rename-a-repository') }
%p= _("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
@@ -107,7 +107,7 @@
.input-group-text
#{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
= f.text_field :path, class: 'form-control h-auto', data: { qa_selector: 'project_path_field' }
- = f.submit _('Change path'), class: "gl-button btn btn-danger", data: { qa_selector: 'change_path_button' }
+ = f.submit _('Change path'), class: "btn-danger", data: { qa_selector: 'change_path_button' }, pajamas_button: true
= render 'transfer', project: @project
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index a2fed883739..b065d5d2dc9 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -19,21 +19,22 @@
icon: 'close',
size: :small,
button_options: { class: 'gl-close-btn-color-inherit gl-broadcast-message-dismiss js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601 } },
- icon_classes: 'gl-mx-3! gl-text-white')
+ icon_classes: 'gl-text-white')
- else
- notification_class = "js-broadcast-notification-#{message.id}"
- notification_class << ' preview' if preview
- .broadcast-message.broadcast-notification-message.mt-2{ role: "alert", class: notification_class, data: { qa_selector: 'broadcast_notification_container' } }
- = sprite_icon(icon_name, css_class: 'vertical-align-text-top')
- - if message.message.present?
- %h2.gl-sr-only
- = s_("Admin message")
- = render_broadcast_message(message)
- - else
- = yield
+ .gl-broadcast-message.broadcast-notification-message.gl-mt-3{ role: "alert", class: notification_class, data: { qa_selector: 'broadcast_notification_container' } }
+ .gl-broadcast-message-content
+ .gl-broadcast-message-icon
+ = sprite_icon(icon_name, css_class: 'vertical-align-text-top')
+ - if message.message.present?
+ %h2.gl-sr-only
+ = s_("Admin message")
+ = render_broadcast_message(message)
+ - else
+ = yield
- if !preview
- = render Pajamas::ButtonComponent.new(variant: :link,
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
icon: 'close',
size: :small,
- button_options: { class: 'js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601, qa_selector: 'close_button' } },
- icon_classes: 'gl-mx-3! gl-text-gray-700')
+ button_options: { class: 'js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601, qa_selector: 'close_button' } })
diff --git a/config/feature_flags/development/ci_graphql_pipeline_mini_graph.yml b/config/feature_flags/development/ci_graphql_pipeline_mini_graph.yml
new file mode 100644
index 00000000000..2b138885d07
--- /dev/null
+++ b/config/feature_flags/development/ci_graphql_pipeline_mini_graph.yml
@@ -0,0 +1,8 @@
+---
+name: ci_graphql_pipeline_mini_graph
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116046
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407818
+milestone: '16.1'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/command_palette.yml b/config/feature_flags/development/command_palette.yml
new file mode 100644
index 00000000000..cba513c305e
--- /dev/null
+++ b/config/feature_flags/development/command_palette.yml
@@ -0,0 +1,8 @@
+---
+name: command_palette
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121932
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413060
+milestone: '16.1'
+type: development
+group: group::foundations
+default_enabled: false
diff --git a/db/migrate/20230328150343_add_retried_at_to_status_check_responses.rb b/db/migrate/20230328150343_add_retried_at_to_status_check_responses.rb
new file mode 100644
index 00000000000..53cc1f0432b
--- /dev/null
+++ b/db/migrate/20230328150343_add_retried_at_to_status_check_responses.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddRetriedAtToStatusCheckResponses < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :status_check_responses, :retried_at, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20230522132239_add_model_experiments_access_level_to_project_feature.rb b/db/migrate/20230522132239_add_model_experiments_access_level_to_project_feature.rb
new file mode 100644
index 00000000000..a34b8a15521
--- /dev/null
+++ b/db/migrate/20230522132239_add_model_experiments_access_level_to_project_feature.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddModelExperimentsAccessLevelToProjectFeature < Gitlab::Database::Migration[2.1]
+ OPERATIONS_DEFAULT_VALUE = 20
+
+ enable_lock_retries!
+
+ def change
+ add_column :project_features,
+ :model_experiments_access_level,
+ :integer,
+ null: false,
+ default: OPERATIONS_DEFAULT_VALUE
+ end
+end
diff --git a/db/schema_migrations/20230328150343 b/db/schema_migrations/20230328150343
new file mode 100644
index 00000000000..05054b0490b
--- /dev/null
+++ b/db/schema_migrations/20230328150343
@@ -0,0 +1 @@
+d5cb88bd614c000b9b782e8a827bf4efcf04c57688bd4bde3d01f555b52f43fb \ No newline at end of file
diff --git a/db/schema_migrations/20230522132239 b/db/schema_migrations/20230522132239
new file mode 100644
index 00000000000..365eb1606cb
--- /dev/null
+++ b/db/schema_migrations/20230522132239
@@ -0,0 +1 @@
+7cda2cca39c53859e84bb7ecf3a2a9817f598486632a3cdd922dde6057b5c930 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 5530bd4c525..acdbe3e2cb9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20904,7 +20904,8 @@ CREATE TABLE project_features (
infrastructure_access_level integer DEFAULT 20 NOT NULL,
feature_flags_access_level integer DEFAULT 20 NOT NULL,
environments_access_level integer DEFAULT 20 NOT NULL,
- releases_access_level integer DEFAULT 20 NOT NULL
+ releases_access_level integer DEFAULT 20 NOT NULL,
+ model_experiments_access_level integer DEFAULT 20 NOT NULL
);
CREATE SEQUENCE project_features_id_seq
@@ -22871,7 +22872,8 @@ CREATE TABLE status_check_responses (
external_approval_rule_id bigint,
sha bytea NOT NULL,
external_status_check_id bigint NOT NULL,
- status smallint DEFAULT 0 NOT NULL
+ status smallint DEFAULT 0 NOT NULL,
+ retried_at timestamp with time zone
);
CREATE SEQUENCE status_check_responses_id_seq
diff --git a/doc/integration/arkose.md b/doc/integration/arkose.md
index 8f6cec0ac0a..cd0b80e5a66 100644
--- a/doc/integration/arkose.md
+++ b/doc/integration/arkose.md
@@ -48,6 +48,10 @@ sequenceDiagram
end
```
+## How do we treat malicious sign-up attempts?
+
+Depending on the risk score received, a user might be required to perform up to three stages of [identity verification](../security/identity_verification.md) to register an account.
+
## How do we treat malicious sign-in attempts?
Users are not denied access if Arkose Protect considers they are malicious. However,
diff --git a/doc/security/identity_verification.md b/doc/security/identity_verification.md
index cf2beaf229f..761bc114da2 100644
--- a/doc/security/identity_verification.md
+++ b/doc/security/identity_verification.md
@@ -40,3 +40,4 @@ You cannot verify an account with a credit card number associated with a banned
## Related topics
- [Identity verification development documentation](../development/identity_verification.md)
+- [Changing risk assessment support](https://about.gitlab.com/handbook/support/workflows/reinstating-blocked-accounts.html#change-risk-assessment-credit-card-verification)
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 4ab8577cbb2..b121dad99ba 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -7,65 +7,16 @@ type: reference
# GitLab Admin Area **(FREE SELF)**
-The Admin Area provides a web UI to manage and configure some features of GitLab
-self-managed instances. If you are an administrator, you can access the Admin Area
-by visiting `/admin` on your self-managed instance. You can also access it through
-the UI:
+The Admin Area provides a web UI to manage and configure features of GitLab
+self-managed instances. If you are an administrator,to access the Admin Area:
-- GitLab versions 14.0 and later: on the top bar, select **Main menu > Admin**.
-- GitLab versions 13.12 and earlier: on the top bar, select the Admin Area icon (**{admin}**).
+- In GitLab 14.0 and later: on the top bar, select **Main menu > Admin**.
+- In GitLab 13.12 and earlier: on the top bar, select the Admin Area icon (**{admin}**).
NOTE:
Only administrators can access the Admin Area.
-## Admin Area sections
-
-The Admin Area is made up of the following sections:
-
-| Section | Description |
-|:-----------------------------------------------|:------------|
-| **{overview}** [Overview](#overview-section) | View your GitLab [Dashboard](#admin-area-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [topics](#administering-topics), [jobs](#administering-jobs), [runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). |
-| **{monitor}** Monitoring | View GitLab [system information](#system-information), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), and [audit events](#audit-events). |
-| **{messages}** Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
-| **{hook}** System Hooks | Configure [system hooks](../../administration/system_hooks.md) for many events. |
-| **{applications}** Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. |
-| **{slight-frown}** Abuse Reports | Manage [abuse reports](review_abuse_reports.md) submitted by your users. |
-| **{license}** License | Add, display, and remove [licenses](license.md). |
-| **{cloud-gear}** Kubernetes | Create and manage instance-level [Kubernetes clusters](../instance/clusters/index.md). |
-| **{push-rules}** Push rules | Configure pre-defined Git [push rules](../project/repository/push_rules.md) for projects. Also, configure [merge requests approvers rules](merge_requests_approvals.md). |
-| **{location-dot}** Geo | Configure and maintain [Geo sites](geo_sites.md). |
-| **{key}** Deploy keys | Create instance-wide [SSH deploy keys](../project/deploy_keys/index.md). |
-| **{lock}** Credentials | View [credentials](credentials_inventory.md) that can be used to access your instance. |
-| **{template}** Integrations | Manage [instance-level default settings](settings/project_integration_management.md) for a project integration. |
-| **{labels}** Labels | Create and maintain [labels](labels.md) for your GitLab instance. |
-| **{appearance}** Appearance | Customize [GitLab appearance](appearance.md). |
-| **{settings}** Settings | Modify the [settings](settings/index.md) for your GitLab instance. |
-
-## Admin Area dashboard
-
-The Dashboard provides statistics and system information about the GitLab instance.
-
-To access the Dashboard, either:
-
-- On the top bar, select **Main menu > Admin**.
-- Visit `/admin` on your self-managed instance.
-
-The Dashboard is the default view of the Admin Area, and is made up of the following sections:
-
-| Section | Description |
-|:-----------|:------------|
-| Projects | The total number of projects, up to 10 of the latest projects, and the option of creating a new project. |
-| Users | The total number of users, up to 10 of the latest users, the option of creating a new user, and a link to [**Users statistics**](#users-statistics). |
-| Groups | The total number of groups, up to 10 of the latest groups, and the option of creating a new group. |
-| Statistics | Totals of all elements of the GitLab instance. |
-| Features | All features available on the GitLab instance. Enabled features are marked with a green circle icon, and disabled features are marked with a power icon. |
-| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. |
-
-## Overview section
-
-The following topics document the **Overview** section of the Admin Area.
-
-### Administering Projects
+## Administering projects
You can administer all projects in the GitLab instance from the Admin Area's Projects page.
@@ -118,7 +69,7 @@ You can combine the filter options. For example, to list only public projects wi
1. Select the **Public** tab.
1. Enter `score` in the **Filter by name...** input box.
-### Administering Users
+## Administering users
You can administer all users in the GitLab instance from the Admin Area's Users page:
@@ -159,7 +110,7 @@ To search for users, enter your criteria in the search field. The user search is
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
-#### User impersonation
+### User impersonation
An administrator can "impersonate" any other user, including other administrators.
This allows the administrator to "see what the user sees," and take actions on behalf of the user.
@@ -178,7 +129,7 @@ By default, impersonation is enabled. GitLab can be configured to [disable imper
![user impersonation button](img/impersonate_user_button_v13_8.png)
-#### User identities
+### User identities
> The ability to see a user's SCIM identity was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294608) in GitLab 15.3.
@@ -192,7 +143,7 @@ When using authentication providers, administrators can see the identities for a
This list shows the user's identities, including SCIM identities. Administrators can use this information to troubleshoot SCIM-related issues and confirm
the identities being used for an account.
-#### User Permission Export **(PREMIUM SELF)**
+### User Permission Export **(PREMIUM SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292436) in GitLab 13.9.
@@ -213,7 +164,7 @@ Only the first 100,000 user accounts are exported.
![user permission export button](img/export_permissions_v13_11.png)
-#### Users statistics
+### Users statistics
The **Users statistics** page provides an overview of user accounts by role. These statistics are
calculated daily, so user changes made since the last update are not reflected.
@@ -226,7 +177,7 @@ The following totals are also included:
GitLab billing is based on the number of [**Billable users**](../../subscriptions/self_managed/index.md#billable-users).
-#### Add email to user
+### Add email to user
You must be an administrator to manually add emails to users:
@@ -238,11 +189,11 @@ You must be an administrator to manually add emails to users:
user and sets the previous email address to be a secondary.
1. Select **Save changes**.
-### User cohorts
+## User cohorts
The [Cohorts](user_cohorts.md) tab displays the monthly cohorts of new users and their activities over time.
-### Prevent a user from creating groups
+## Prevent a user from creating groups
By default, users can create groups. To prevent a user from creating a top level group:
@@ -255,7 +206,7 @@ By default, users can create groups. To prevent a user from creating a top level
It is also possible to [limit which roles can create a subgroup within a group](../group/subgroups/index.md#change-who-can-create-subgroups).
-### Administering Groups
+## Administering groups
You can administer all groups in the GitLab instance from the Admin Area's Groups page.
@@ -275,7 +226,7 @@ insensitive, and applies partial matching.
To [Create a new group](../group/index.md#create-a-group) select **New group**.
-### Administering Topics
+## Administering topics
- > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340920) in GitLab 14.4.
- > Merging topics [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/366884) in GitLab 15.5.
@@ -307,7 +258,7 @@ The assigned topics are visible only to everyone with access to the project,
but everyone can see which topics exist on the GitLab instance.
Do not include sensitive information in the name of a topic.
-### Administering Gitaly servers
+## Administering Gitaly servers
You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers**
page. For more details, see [Gitaly](../../administration/gitaly/index.md).
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 648b34cee2e..085f3305ddb 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -485,6 +485,138 @@ The [incoming email](../../administration/incoming_email.md) address still works
If you don't configure a custom suffix, the default project identification is used for identifying
the project.
+### Configure email ingestion in multi-node environments
+
+A multi-node environment is a setup where GitLab is run across multiple servers
+for scalability, fault tolerance, and performance reasons.
+
+GitLab uses a separate process called `mail_room` to ingest new unread emails
+from the `incoming_email` and `service_desk_email` mailboxes.
+
+#### Helm chart (Kubernetes)
+
+The [GitLab Helm chart](https://docs.gitlab.com/charts/) is made up of multiple subcharts, and one of them is
+the [Mailroom subchart](https://docs.gitlab.com/charts/charts/gitlab/mailroom/index.html). Configure the
+[common settings for `incoming_email`](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration)
+and the [common settings for `service_desk_email`](https://docs.gitlab.com/charts/installation/command-line-options.html#service-desk-email-configuration).
+
+#### Linux package (Omnibus)
+
+In multi-node Linux package (Omnibus) environments, run `mail_room` only on one node. Run it either on a single
+`rails` node (for example, [Omnibus role](https://docs.gitlab.com/omnibus/roles/index.html) `application_role`)
+or completely separately.
+
+##### Set up all nodes
+
+1. Add basic configuration for `incoming_email` and `service_desk_email` on every node
+ to render email addresses in the web UI and in generated emails.
+
+ Find the `incoming_email` or `service_desk_email` section in `/etc/gitlab/gitlab.rb`:
+
+ ::Tabs
+
+ :::TabTitle `incoming_email`
+
+ ```ruby
+ gitlab_rails['incoming_email_enabled'] = true
+ gitlab_rails['incoming_email_address'] = "incoming+%{key}@example.com"
+ ```
+
+ :::TabTitle `service_desk_email`
+
+ ```ruby
+ gitlab_rails['service_desk_email_enabled'] = true
+ gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@example.com"
+ ```
+
+ ::EndTabs
+
+1. GitLab offers two methods to transport emails from `mail_room` to the GitLab
+application. You can configure the `delivery_method` for each email setting individually:
+ 1. Recommended: `webhook` (default in GitLab 15.3 and later) sends the email payload via an API POST request to your GitLab
+ application. It uses a shared token to authenticate. If you choose this method,
+ make sure the `mail_room` process can access the API endpoint and distribute the shared
+ token across all application nodes.
+
+ ::Tabs
+
+ :::TabTitle `incoming_email`
+
+ ```ruby
+ gitlab_rails['incoming_email_delivery_method'] = "webhook"
+
+ # The URL that mail_room can contact. You can also use an internal URL or IP,
+ # just make sure mail_room can access the GitLab API via that address.
+ # Do not end with "/".
+ gitlab_rails['incoming_email_gitlab_url'] = "https://gitlab.example.com"
+
+ # The shared secret file that should contain a random token. Make sure it's the same on every node.
+ gitlab_rails['incoming_email_secret_file'] = ".gitlab_mailroom_secret"
+ ```
+
+ :::TabTitle `service_desk_email`
+
+ ```ruby
+ gitlab_rails['service_desk_email_delivery_method'] = "webhook"
+
+ # The URL that mail_room can contact. You can also use an internal URL or IP,
+ # just make sure mail_room can access the GitLab API via that address.
+ # Do not end with "/".
+
+ gitlab_rails['service_desk_email_gitlab_url'] = "https://gitlab.example.com"
+
+ # The shared secret file that should contain a random token. Make sure it's the same on every node.
+ gitlab_rails['service_desk_email_secret_file'] = ".gitlab_mailroom_secret"
+ ```
+
+ ::EndTabs
+
+ 1. [Deprecated in GitLab 16.0 and planned for removal in 17.0)](../../update/deprecations.md#sidekiq-delivery-method-for-incoming_email-and-service_desk_email-is-deprecated):
+ If you experience issues with the `webhook` setup, use `sidekiq` to deliver the email payload directly to GitLab Sidekiq using Redis.
+
+ ::Tabs
+
+ :::TabTitle `incoming_email`
+
+ ```ruby
+ # It uses the Redis configuration to directly add Sidekiq jobs
+ gitlab_rails['incoming_email_delivery_method'] = "sidekiq"
+ ```
+
+ :::TabTitle `service_desk_email`
+
+ ```ruby
+ # It uses the Redis configuration to directly add Sidekiq jobs
+ gitlab_rails['service_desk_email_delivery_method'] = "sidekiq"
+ ```
+
+ ::EndTabs
+
+1. Disable `mail_room` on all nodes that should not run email ingestion. For example, in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ mailroom['enabled'] = false
+ ```
+
+1. [Reconfigure GitLab](../../administration/restart_gitlab.md) for the changes to take effect.
+
+##### Set up a single email ingestion node
+
+After setting up all nodes and disabling the `mail_room` process, enable `mail_room` on a single node.
+This node polls the mailboxes for `incoming_email` and `service_desk_email` on a regular basis and
+move new unread emails to GitLab.
+
+1. Choose an existing node that additionally handles email ingestion.
+1. Add [full configuration and credentials](../../administration/incoming_email.md#configuration-examples)
+ for `incoming_email` and `service_desk_email`.
+1. Enable `mail_room` on this node. For example, in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ mailroom['enabled'] = true
+ ```
+
+1. [Reconfigure GitLab](../../administration/restart_gitlab.md) on this node for the changes to take effect.
+
## Use Service Desk
You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue).
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index eef176687e7..2d875742430 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -70,6 +70,7 @@ module Gitlab
push_frontend_feature_flag(:vscode_web_ide, current_user)
push_frontend_feature_flag(:super_sidebar_peek, current_user)
push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
+ push_frontend_feature_flag(:command_palette, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
end
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 56cbc5f1bb4..410e918649b 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -321,6 +321,7 @@ included_attributes:
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
+ - :model_experiments_access_level
prometheus_metrics:
- :created_at
- :updated_at
@@ -741,6 +742,7 @@ included_attributes:
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
+ - :model_experiments_access_level
- :auto_devops_deploy_strategy
- :auto_devops_enabled
- :container_registry_enabled
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8ec3ee9bfe7..2c8b94a7282 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4640,9 +4640,6 @@ msgstr ""
msgid "Allow projects and subgroups to override the group setting"
msgstr ""
-msgid "Allow projects within this group to use Git LFS"
-msgstr ""
-
msgid "Allow public access to pipelines and job details, including output logs and artifacts."
msgstr ""
@@ -8742,9 +8739,6 @@ msgstr ""
msgid "Can be manually deployed to"
msgstr ""
-msgid "Can be overridden in each project."
-msgstr ""
-
msgid "Can create groups:"
msgstr ""
@@ -11102,6 +11096,15 @@ msgstr ""
msgid "Command line instructions"
msgstr ""
+msgid "CommandPalette|Commands"
+msgstr ""
+
+msgid "CommandPalette|Type %{commandHandle} for command or search..."
+msgstr ""
+
+msgid "CommandPalette|command"
+msgstr ""
+
msgid "Commands applied"
msgstr ""
@@ -12808,6 +12811,9 @@ msgstr ""
msgid "Create a new project"
msgstr ""
+msgid "Create a new project/repository"
+msgstr ""
+
msgid "Create a new repository"
msgstr ""
@@ -34069,6 +34075,9 @@ msgstr ""
msgid "Port"
msgstr ""
+msgid "Possible to override in each project."
+msgstr ""
+
msgid "Postman collection"
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 114fc17a277..685e8970f78 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 10', '>= 10.4.1', require: 'gitlab/qa'
-gem 'gitlab_quality-test_tooling', '~> 0.4.3', require: false
+gem 'gitlab_quality-test_tooling', '~> 0.6.1', require: false
gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.20.0'
gem 'capybara', '~> 3.39.1'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 64283b60f00..ec654dbf5c8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -99,21 +99,21 @@ GEM
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.3.0)
- gitlab (4.18.0)
- httparty (~> 0.18)
+ gitlab (4.19.0)
+ httparty (~> 0.20)
terminal-table (>= 1.5.1)
- gitlab-qa (10.4.1)
+ gitlab-qa (10.6.0)
activesupport (~> 6.1)
- gitlab (~> 4.18.0)
+ gitlab (~> 4.19)
http (~> 5.0)
nokogiri (~> 1.10)
parallel (>= 1, < 2)
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
- gitlab_quality-test_tooling (0.4.3)
+ gitlab_quality-test_tooling (0.6.1)
activesupport (~> 6.1)
- gitlab (~> 4.18.0)
+ gitlab (~> 4.19)
http (~> 5.0)
nokogiri (~> 1.10)
parallel (>= 1, < 2)
@@ -328,7 +328,7 @@ DEPENDENCIES
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-qa (~> 10, >= 10.4.1)
- gitlab_quality-test_tooling (~> 0.4.3)
+ gitlab_quality-test_tooling (~> 0.6.1)
influxdb-client (~> 2.9)
knapsack (~> 4.0)
nokogiri (~> 1.15, >= 1.15.2)
diff --git a/rubocop/cop/gitlab/feature_available_usage.rb b/rubocop/cop/gitlab/feature_available_usage.rb
index df4409c27e0..cbfb89d3053 100644
--- a/rubocop/cop/gitlab/feature_available_usage.rb
+++ b/rubocop/cop/gitlab/feature_available_usage.rb
@@ -28,6 +28,7 @@ module RuboCop
feature_flags
releases
infrastructure
+ model_experiments
].freeze
EE_FEATURES = %i[requirements].freeze
ALL_FEATURES = (FEATURES + EE_FEATURES).freeze
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1b485e47127..6e3e119ddab 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -42,6 +42,7 @@ FactoryBot.define do
feature_flags_access_level { ProjectFeature::ENABLED }
releases_access_level { ProjectFeature::ENABLED }
infrastructure_access_level { ProjectFeature::ENABLED }
+ model_experiments_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index b8526e569ec..29759f828e4 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 8ca88472bf1..9d93ba332e9 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
@@ -16,7 +17,7 @@ describe('Pipeline Status', () => {
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = () => {
+ const createComponentWithApollo = ({ ciGraphqlPipelineMiniGraph = false } = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -26,6 +27,9 @@ describe('Pipeline Status', () => {
commitSha: mockCommitSha,
},
provide: {
+ glFeatures: {
+ ciGraphqlPipelineMiniGraph,
+ },
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
@@ -34,6 +38,7 @@ describe('Pipeline Status', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGraphqlPipelineMiniGraph = () => wrapper.findComponent(GraphqlPipelineMiniGraph);
const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
@@ -128,4 +133,28 @@ describe('Pipeline Status', () => {
});
});
});
+
+ describe('feature flag behavior', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: mockProjectPipeline() },
+ });
+ });
+
+ it.each`
+ state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
+ ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
+ ${false} | ${{}} | ${true} | ${false}
+ `(
+ 'renders the correct component when the feature flag is set to $state',
+ async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
+ createComponentWithApollo(provide);
+
+ await waitForPromises();
+
+ expect(findPipelineEditorMiniGraph().exists()).toBe(showPipelineMiniGraph);
+ expect(findGraphqlPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ },
+ );
+ });
});
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 7be68df61de..7983f8fddf5 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -7,10 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import {
mockDownstreamQueryResponse,
@@ -28,6 +29,7 @@ describe('Commit box pipeline mini graph', () => {
let wrapper;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGraphqlPipelineMiniGraph = () => wrapper.findComponent(GraphqlPipelineMiniGraph);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse);
@@ -52,7 +54,7 @@ describe('Commit box pipeline mini graph', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (handler) => {
+ const createComponent = ({ handler, ciGraphqlPipelineMiniGraph = false } = {}) => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
@@ -63,6 +65,9 @@ describe('Commit box pipeline mini graph', () => {
iid,
dataMethod: 'graphql',
graphqlResourceEtag: '/api/graphql:pipelines/id/320',
+ glFeatures: {
+ ciGraphqlPipelineMiniGraph,
+ },
},
apolloProvider: createMockApolloProvider(handler),
}),
@@ -148,7 +153,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should pass the pipeline path prop for the counter badge', async () => {
- createComponent(downstreamHandler);
+ createComponent({ handler: downstreamHandler });
await waitForPromises();
@@ -159,7 +164,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should render an upstream pipeline only', async () => {
- createComponent(upstreamHandler);
+ createComponent({ handler: upstreamHandler });
await waitForPromises();
@@ -171,7 +176,7 @@ describe('Commit box pipeline mini graph', () => {
});
it('should render downstream and upstream pipelines', async () => {
- createComponent(upstreamDownstreamHandler);
+ createComponent({ handler: upstreamDownstreamHandler });
await waitForPromises();
@@ -255,4 +260,31 @@ describe('Commit box pipeline mini graph', () => {
);
});
});
+
+ describe('feature flag behavior', () => {
+ it.each`
+ state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
+ ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
+ ${false} | ${{}} | ${true} | ${false}
+ `(
+ 'renders the correct component when the feature flag is set to $state',
+ async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
+ createComponent(provide);
+
+ await waitForPromises();
+
+ expect(findPipelineMiniGraph().exists()).toBe(showPipelineMiniGraph);
+ expect(findGraphqlPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ },
+ );
+
+ it('skips queries when the feature flag is enabled', async () => {
+ createComponent({ ciGraphqlPipelineMiniGraph: true });
+
+ await waitForPromises();
+
+ expect(stagesHandler).not.toHaveBeenCalled();
+ expect(downstreamHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js
new file mode 100644
index 00000000000..69b223461bd
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph_spec.js
@@ -0,0 +1,123 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
+import * as sharedGraphQlUtils from '~/graphql_shared/utils';
+
+import {
+ linkedPipelinesFetchError,
+ stagesFetchError,
+ mockPipelineStagesQueryResponse,
+ mockUpstreamDownstreamQueryResponse,
+} from './mock_data';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('GraphqlPipelineMiniGraph', () => {
+ let wrapper;
+ let linkedPipelinesResponse;
+ let pipelineStagesResponse;
+
+ const fullPath = 'gitlab-org/gitlab';
+ const iid = '315';
+ const pipelineEtag = '/api/graphql:pipelines/id/315';
+
+ const createComponent = ({
+ pipelineStagesHandler = pipelineStagesResponse,
+ linkedPipelinesHandler = linkedPipelinesResponse,
+ } = {}) => {
+ const handlers = [
+ [getLinkedPipelinesQuery, linkedPipelinesHandler],
+ [getPipelineStagesQuery, pipelineStagesHandler],
+ ];
+ const mockApollo = createMockApollo(handlers);
+
+ wrapper = shallowMountExtended(GraphqlPipelineMiniGraph, {
+ propsData: {
+ fullPath,
+ iid,
+ pipelineEtag,
+ },
+ apolloProvider: mockApollo,
+ });
+
+ return waitForPromises();
+ };
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ beforeEach(() => {
+ linkedPipelinesResponse = jest.fn().mockResolvedValue(mockUpstreamDownstreamQueryResponse);
+ pipelineStagesResponse = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse);
+ });
+
+ describe('when initial queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a loading icon and no mini graph', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ it('does not show a loading icon', async () => {
+ await createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders the Pipeline Mini Graph', async () => {
+ await createComponent();
+
+ expect(findPipelineMiniGraph().exists()).toBe(true);
+ });
+
+ it('fires the queries', async () => {
+ await createComponent();
+
+ expect(linkedPipelinesResponse).toHaveBeenCalledWith({ iid, fullPath });
+ expect(pipelineStagesResponse).toHaveBeenCalledWith({ iid, fullPath });
+ });
+ });
+
+ describe('polling', () => {
+ it('toggles query polling with visibility check', async () => {
+ jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('when pipeline queries are unsuccessful', () => {
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ it.each`
+ query | handlerName | errorMessage
+ ${'pipeline stages'} | ${'pipelineStagesHandler'} | ${stagesFetchError}
+ ${'linked pipelines'} | ${'linkedPipelinesHandler'} | ${linkedPipelinesFetchError}
+ `('throws an error for the $query query', async ({ errorMessage, handlerName }) => {
+ await createComponent({ [handlerName]: failedHandler });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js
new file mode 100644
index 00000000000..1c13e9eb62b
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js
@@ -0,0 +1,150 @@
+export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-611-611',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+});
+
+const upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-610-610',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+};
+
+export const mockPipelineStagesQueryResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ stages: {
+ nodes: [
+ {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/409',
+ name: 'build',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-409-409',
+ icon: 'status_success',
+ group: 'success',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockPipelineStatusResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ detailedStatus: {
+ id: 'pending-320-320',
+ detailsPath: '/root/ci-project/-/pipelines/320',
+ icon: 'status_pending',
+ group: 'pending',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockUpstreamDownstreamQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ id: 'pipeline-1',
+ path: '/root/ci-project/-/pipelines/790',
+ downstream: mockDownstreamPipelinesGraphql(),
+ upstream,
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const linkedPipelinesFetchError = 'There was a problem fetching linked pipelines.';
+export const stagesFetchError = 'There was a problem fetching the pipeline stages.';
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
new file mode 100644
index 00000000000..a079188190a
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -0,0 +1,70 @@
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CommandPaletteItems from '~/super_sidebar/components/global_search/command_palette/command_palette_items.vue';
+import {
+ COMMAND_HANDLE,
+ COMMANDS_GROUP_TITLE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
+import { COMMAND_PALETTE_COMMANDS } from './mock_data';
+
+const commands = COMMAND_PALETTE_COMMANDS.map(({ text, href, keywords }) => ({
+ text,
+ href,
+ keywords: keywords.join(''),
+}));
+
+describe('CommandPaletteItems', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(CommandPaletteItems, {
+ propsData: {
+ handle: COMMAND_HANDLE,
+ searchQuery: '',
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
+ provide: {
+ commandPaletteData: COMMAND_PALETTE_COMMANDS,
+ },
+ });
+ };
+
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
+
+ it('renders all commands initially', () => {
+ createComponent();
+ expect(findItems()).toHaveLength(COMMAND_PALETTE_COMMANDS.length);
+ expect(findGroup().props('group')).toEqual({
+ name: COMMANDS_GROUP_TITLE,
+ items: commands,
+ });
+ });
+
+ describe('with search query', () => {
+ it('should filter by the search query', async () => {
+ jest.spyOn(fuzzaldrinPlus, 'filter');
+ createComponent({ searchQuery: 'mr' });
+ const searchQuery = 'todo';
+ await wrapper.setProps({ searchQuery });
+ expect(fuzzaldrinPlus.filter).toHaveBeenCalledWith(
+ commands,
+ searchQuery,
+ expect.objectContaining({ key: 'keywords' }),
+ );
+ });
+
+ it('should display no results message when no command matched the search qery', async () => {
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue([]);
+ createComponent({ searchQuery: 'mr' });
+ const searchQuery = 'todo';
+ await wrapper.setProps({ searchQuery });
+ expect(wrapper.text()).toBe('No results found');
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js
new file mode 100644
index 00000000000..0aeb4c89d06
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/fake_search_input_spec.js
@@ -0,0 +1,40 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import FakeSearchInput from '~/super_sidebar/components/global_search/command_palette/fake_search_input.vue';
+import {
+ COMMAND_HANDLE,
+ SEARCH_SCOPE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
+
+describe('FakeSearchInput', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(FakeSearchInput, {
+ propsData: {
+ scope: COMMAND_HANDLE,
+ userInput: '',
+ ...props,
+ },
+ });
+ };
+
+ const findSearchScope = () => wrapper.findByTestId('search-scope');
+ const findSearchScopePlaceholder = () => wrapper.findByTestId('search-scope-placeholder');
+
+ it('should render the search scope', () => {
+ createComponent();
+ expect(findSearchScope().text()).toBe(COMMAND_HANDLE);
+ });
+
+ describe('placeholder', () => {
+ it('should render the placeholder for its search scope when there is no user input', () => {
+ createComponent();
+ expect(findSearchScopePlaceholder().text()).toBe(SEARCH_SCOPE[COMMAND_HANDLE]);
+ });
+
+ it('should NOT render the placeholder when there is user input', () => {
+ createComponent({ userInput: 'todo' });
+ expect(findSearchScopePlaceholder().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
new file mode 100644
index 00000000000..7469154e363
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -0,0 +1,17 @@
+export const COMMAND_PALETTE_COMMANDS = [
+ {
+ text: 'New project/repository',
+ href: '/projects/new',
+ keywords: ['new', 'project', 'repository'],
+ },
+ {
+ text: 'New group',
+ href: '/groups/new',
+ keywords: ['new', 'group'],
+ },
+ {
+ text: 'New snippet',
+ href: '/-/snippets/new',
+ keywords: ['new', 'snippet'],
+ },
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index f78e141afad..fd79e274ff6 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -7,6 +7,12 @@ import GlobalSearchModal from '~/super_sidebar/components/global_search/componen
import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import FakeSearchInput from '~/super_sidebar/components/global_search/command_palette/fake_search_input.vue';
+import CommandPaletteItems from '~/super_sidebar/components/global_search/command_palette/command_palette_items.vue';
+import {
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ COMMAND_HANDLE,
+} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
@@ -17,6 +23,7 @@ import {
IS_SEARCHING,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
+import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -53,7 +60,18 @@ describe('GlobalSearchModal', () => {
},
};
- const createComponent = (initialState, mockGetters, stubs) => {
+ const defaultMockGetters = {
+ searchQuery: () => MOCK_SEARCH_QUERY,
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ };
+
+ const createComponent = (
+ initialState = deafaultMockState,
+ mockGetters = defaultMockGetters,
+ stubs,
+ glFeatures = { commandPalette: false },
+ ) => {
const store = new Vuex.Store({
state: {
...deafaultMockState,
@@ -71,6 +89,7 @@ describe('GlobalSearchModal', () => {
wrapper = shallowMountExtended(GlobalSearchModal, {
store,
stubs,
+ provide: { glFeatures },
});
};
@@ -98,6 +117,8 @@ describe('GlobalSearchModal', () => {
wrapper.findComponent(GlobalSearchAutocompleteItems);
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
+ const findCommandPaletteItems = () => wrapper.findComponent(CommandPaletteItems);
+ const findFakeSearchInput = () => wrapper.findComponent(FakeSearchInput);
describe('template', () => {
describe('always renders', () => {
@@ -281,6 +302,42 @@ describe('GlobalSearchModal', () => {
).toBe(iconName);
});
});
+
+ describe('Command palette', () => {
+ describe('when FF `command_palette` is disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should not render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(false);
+ expect(findFakeSearchInput().exists()).toBe(false);
+ });
+
+ it('should provide default placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(SEARCH_GITLAB);
+ });
+ });
+
+ describe('when FF `command_palette` is enabled', () => {
+ beforeEach(() => {
+ createComponent({ search: COMMAND_HANDLE }, undefined, undefined, {
+ commandPalette: true,
+ });
+ });
+
+ it('should render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(true);
+ expect(findFakeSearchInput().exists()).toBe(true);
+ });
+
+ it('should provide an alternative placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ );
+ });
+ });
+ });
});
describe('events', () => {
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6648663b634..f4df7a69b8f 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -319,6 +319,17 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
)
end
+ it 'returns command palette items', :use_clean_rails_memory_store_caching do
+ expect(subject[:command_palette_commands]).to match_array([
+ { href: "/projects/new",
+ text: "New project/repository", keywords: [_('Create a new project/repository')] },
+ { href: "/groups/new", text: "New group",
+ keywords: ['Create a new group'] },
+ { href: "/-/snippets/new", text: "New snippet",
+ keywords: ['Create a new snippet'] }
+ ])
+ end
+
describe 'current context' do
context 'when current context is a project' do
let_it_be(:project) { build(:project) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 9e5b5ccd64a..abdd8741377 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -704,6 +704,7 @@ ProjectFeature:
- releases_access_level
- monitor_access_level
- infrastructure_access_level
+- model_experiments_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 8dd1f7b1831..48c9567ebb3 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe ProjectFeature, feature_category: :groups_and_projects do
specify { expect(subject.releases_access_level).to eq(ProjectFeature::ENABLED) }
specify { expect(subject.package_registry_access_level).to eq(ProjectFeature::ENABLED) }
specify { expect(subject.container_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.model_experiments_access_level).to eq(ProjectFeature::ENABLED) }
end
describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 12a34bcee78..1074a328103 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1051,6 +1051,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:environments_access_level).to(:project_feature) }
+ it { is_expected.to delegate_method(:model_experiments_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:feature_flags_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:releases_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:infrastructure_access_level).to(:project_feature) }
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index bf233ed5929..e0e9c944fe4 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -127,6 +127,7 @@ project_feature:
- project_id
- updated_at
- operations_access_level
+ - model_experiments_access_level
computed_attributes:
- issues_enabled
- jobs_enabled