diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-01 00:09:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-01 00:09:09 +0300 |
commit | 404895390afe87ce8ab939448bf7dff7dc4b7169 (patch) | |
tree | 93c323d7df6b70c84dce7b3e4e4f3d57180394a0 /app | |
parent | e9885f7a36065b9b45a35feb6c427c7742a906a4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
24 files changed, 444 insertions, 45 deletions
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 }} </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' } }) |