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>2022-03-22 21:08:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-22 21:08:29 +0300
commitbd1bebd1b0a0d9a0484849c73ee15449dd27fe92 (patch)
treefbfd47b23d0cea97440129d59436c3845a2187dd
parente01b61d83fd7c5d3aa9d87a65eac85e8c7ea9921 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/deprecated_notes.js87
-rw-r--r--app/assets/javascripts/issues/show/components/locked_warning.vue7
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue42
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue122
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js3
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue68
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue2
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_error_details.vue41
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue199
-rw-r--r--app/assets/javascripts/pages/import/history/index.js21
-rw-r--r--app/assets/javascripts/pages/import/history/utils/error_messages.js3
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue37
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue27
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue89
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue106
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue21
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue16
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue22
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue12
-rw-r--r--app/assets/javascripts/runner/components/runner_delete_button.vue5
-rw-r--r--app/assets/javascripts/runner/components/runner_pause_button.vue2
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue27
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue10
-rw-r--r--app/controllers/ide_controller.rb2
-rw-r--r--app/controllers/import/history_controller.rb5
-rw-r--r--app/controllers/projects/jobs_controller.rb5
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/web_ide_schemas_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb7
-rw-r--r--app/controllers/snippets/blobs_controller.rb1
-rw-r--r--app/experiments/new_project_sast_enabled_experiment.rb15
-rw-r--r--app/views/import/history/index.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml6
-rw-r--r--app/views/projects/_import_project_pane.html.haml3
-rw-r--r--app/views/projects/_new_project_fields.html.haml17
-rw-r--r--app/views/projects/_new_project_initialize_with_sast.html.haml16
-rw-r--r--config/feature_flags/development/jobs_table_vue_search.yml (renamed from config/feature_flags/experiment/new_project_sast_enabled.yml)12
-rw-r--r--config/metrics/counts_28d/20210216175000_i_analytics_dev_ops_score_monthly.yml2
-rw-r--r--config/routes/admin.rb3
-rw-r--r--config/routes/import.rb2
-rw-r--r--data/deprecations/14-9-global-search-deprecate-user-email-lookup-limit.yml11
-rw-r--r--data/removals/14_9/14-9-key-user_email_lookup_limit.yml9
-rw-r--r--data/whats_new/202203210001_14_09.yml2
-rw-r--r--doc/development/fe_guide/registry_architecture.md90
-rw-r--r--doc/development/service_ping/index.md2
-rw-r--r--doc/development/testing_guide/review_apps.md6
-rw-r--r--doc/subscriptions/self_managed/index.md14
-rw-r--r--doc/update/deprecations.md14
-rw-r--r--doc/update/removals.md10
-rw-r--r--doc/user/admin_area/analytics/dev_ops_report.md74
-rw-r--r--doc/user/admin_area/analytics/dev_ops_reports.md73
-rw-r--r--doc/user/admin_area/analytics/index.md2
-rw-r--r--doc/user/clusters/agent/troubleshooting.md44
-rw-r--r--doc/user/crm/index.md30
-rw-r--r--doc/user/packages/maven_repository/index.md7
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/wikis.rb2
-rw-r--r--locale/gitlab.pot69
-rw-r--r--qa/qa/page/project/new.rb5
-rw-r--r--qa/qa/page/project/pipeline_editor/show.rb22
-rw-r--r--spec/controllers/projects_controller_spec.rb22
-rw-r--r--spec/experiments/new_project_sast_enabled_experiment_spec.rb20
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb (renamed from spec/features/admin/admin_dev_ops_report_spec.rb)10
-rw-r--r--spec/features/projects/user_creates_project_spec.rb23
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js49
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js57
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js105
-rw-r--r--spec/frontend/jobs/mock_data.js2
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js66
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js205
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js137
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js53
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js99
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js18
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js4
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js20
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js14
-rw-r--r--spec/routing/admin_routing_spec.rb11
87 files changed, 1814 insertions, 705 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 3220e399c28..e9159c44bf8 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1611,10 +1611,6 @@
rules:
- <<: *if-not-ee
when: never
- - <<: *if-dot-com-gitlab-org-merge-request
- changes: *code-qa-patterns
- when: manual
- allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
allow_failure: true
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ccc4d935619..e131cf3c553 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-719c5a5bd2b5ddb54de519d6873ccb1636f7b450
+e9c85a7455f1934bbebf589db4e9600a45c8f1bb
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 0707ae02872..2301ec95167 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -143,7 +143,7 @@ export default class Notes {
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
- this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
+ this.$wrapperEl.on('ajax:success', '.js-note-delete', this.removeNote);
// delete note attachment
this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// update the file name when an attachment is selected
@@ -188,7 +188,7 @@ export default class Notes {
cleanBinding() {
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
- this.$wrapperEl.off('click', '.js-note-delete');
+ this.$wrapperEl.off('ajax:success', '.js-note-delete');
this.$wrapperEl.off('click', '.js-note-attachment-delete');
this.$wrapperEl.off('click', '.js-discussion-reply-button');
this.$wrapperEl.off('click', '.js-add-diff-note-button');
@@ -827,50 +827,53 @@ export default class Notes {
*/
removeNote(e) {
const $note = $(e.currentTarget).closest('.note');
- const noteElId = $note.attr('id');
- $(`.note[id="${noteElId}"]`).each((i, el) => {
- // A same note appears in the "Discussion" and in the "Changes" tab, we have
- // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
- // where $('#noteId') would return only one.
- const $note = $(el);
- const $notes = $note.closest('.discussion-notes');
- const discussionId = $('.notes', $notes).data('discussionId');
-
- $note.remove();
-
- // check if this is the last note for this line
- if ($notes.find('.note').length === 0) {
- const notesTr = $notes.closest('tr');
-
- // "Discussions" tab
- $notes.closest('.timeline-entry').remove();
-
- $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
-
- // The notes tr can contain multiple lists of notes, like on the parallel diff
- // notesTr does not exist for image diffs
- if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
- const $diffFile = $notes.closest('.diff-file');
- if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
- });
- $diffFile[0].dispatchEvent(removeBadgeEvent);
- }
+ $note.one('ajax:complete', () => {
+ const noteElId = $note.attr('id');
+ $(`.note[id="${noteElId}"]`).each((i, el) => {
+ // A same note appears in the "Discussion" and in the "Changes" tab, we have
+ // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
+ // where $('#noteId') would return only one.
+ const $note = $(el);
+ const $notes = $note.closest('.discussion-notes');
+ const discussionId = $('.notes', $notes).data('discussionId');
+
+ $note.remove();
+
+ // check if this is the last note for this line
+ if ($notes.find('.note').length === 0) {
+ const notesTr = $notes.closest('tr');
+
+ // "Discussions" tab
+ $notes.closest('.timeline-entry').remove();
+
+ $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
+
+ // The notes tr can contain multiple lists of notes, like on the parallel diff
+ // notesTr does not exist for image diffs
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
+ const $diffFile = $notes.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
+ },
+ });
+
+ $diffFile[0].dispatchEvent(removeBadgeEvent);
+ }
- $notes.remove();
- } else if (notesTr.length > 0) {
- notesTr.remove();
+ $notes.remove();
+ } else if (notesTr.length > 0) {
+ notesTr.remove();
+ }
}
- }
- });
+ });
- Notes.checkMergeRequestStatus();
- return this.updateNotesCount(-1);
+ Notes.checkMergeRequestStatus();
+ return this.updateNotesCount(-1);
+ });
}
/**
diff --git a/app/assets/javascripts/issues/show/components/locked_warning.vue b/app/assets/javascripts/issues/show/components/locked_warning.vue
index 4b99888ae73..12feacb027b 100644
--- a/app/assets/javascripts/issues/show/components/locked_warning.vue
+++ b/app/assets/javascripts/issues/show/components/locked_warning.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
+import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
const alertMessage = __(
@@ -11,6 +11,7 @@ export default {
components: {
GlSprintf,
GlLink,
+ GlAlert,
},
computed: {
currentPath() {
@@ -21,7 +22,7 @@ export default {
</script>
<template>
- <div class="alert alert-danger">
+ <gl-alert variant="danger" class="gl-mb-5" :dismissible="false">
<gl-sprintf :message="$options.alertMessage">
<template #link="{ content }">
<gl-link :href="currentPath" target="_blank" rel="nofollow">
@@ -29,5 +30,5 @@ export default {
</gl-link>
</template>
</gl-sprintf>
- </div>
+ </gl-alert>
</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
new file mode 100644
index 00000000000..fe7b7428c6e
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlFilteredSearch } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import JobStatusToken from './tokens/job_status_token.vue';
+
+export default {
+ tokenTypes: {
+ status: 'status',
+ },
+ components: {
+ GlFilteredSearch,
+ },
+ computed: {
+ tokens() {
+ return [
+ {
+ type: this.$options.tokenTypes.status,
+ icon: 'status',
+ title: s__('Jobs|Status'),
+ unique: true,
+ token: JobStatusToken,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ];
+ },
+ },
+ methods: {
+ onSubmit(filters) {
+ this.$emit('filterJobsBySearch', filters);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search
+ :placeholder="s__('Jobs|Filter jobs')"
+ :available-tokens="tokens"
+ @submit="onSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue b/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
new file mode 100644
index 00000000000..aad86ded80a
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
@@ -0,0 +1,122 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ statuses() {
+ return [
+ {
+ class: 'ci-status-icon-canceled',
+ icon: 'status_canceled',
+ text: s__('Job|Canceled'),
+ value: 'CANCELED',
+ },
+ {
+ class: 'ci-status-icon-created',
+ icon: 'status_created',
+ text: s__('Job|Created'),
+ value: 'CREATED',
+ },
+ {
+ class: 'ci-status-icon-failed',
+ icon: 'status_failed',
+ text: s__('Job|Failed'),
+ value: 'FAILED',
+ },
+ {
+ class: 'ci-status-icon-manual',
+ icon: 'status_manual',
+ text: s__('Job|Manual'),
+ value: 'MANUAL',
+ },
+ {
+ class: 'ci-status-icon-success',
+ icon: 'status_success',
+ text: s__('Job|Passed'),
+ value: 'SUCCESS',
+ },
+ {
+ class: 'ci-status-icon-pending',
+ icon: 'status_pending',
+ text: s__('Job|Pending'),
+ value: 'PENDING',
+ },
+ {
+ class: 'ci-status-icon-preparing',
+ icon: 'status_preparing',
+ text: s__('Job|Preparing'),
+ value: 'PREPARING',
+ },
+ {
+ class: 'ci-status-icon-running',
+ icon: 'status_running',
+ text: s__('Job|Running'),
+ value: 'RUNNING',
+ },
+ {
+ class: 'ci-status-icon-scheduled',
+ icon: 'status_scheduled',
+ text: s__('Job|Scheduled'),
+ value: 'SCHEDULED',
+ },
+ {
+ class: 'ci-status-icon-skipped',
+ icon: 'status_skipped',
+ text: s__('Job|Skipped'),
+ value: 'SKIPPED',
+ },
+ {
+ class: 'ci-status-icon-waiting-for-resource',
+ icon: 'status-waiting',
+ text: s__('Job|Waiting for resource'),
+ value: 'WAITING_FOR_RESOURCE',
+ },
+ ];
+ },
+ findActiveStatus() {
+ return this.statuses.find((status) => status.value === this.value.data);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
+ <template #view>
+ <div class="gl-display-flex gl-align-items-center">
+ <div :class="findActiveStatus.class">
+ <gl-icon :name="findActiveStatus.icon" class="gl-mr-2 gl-display-block" />
+ </div>
+ <span>{{ findActiveStatus.text }}</span>
+ </div>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(status, index) in statuses"
+ :key="index"
+ :value="status.value"
+ >
+ <div class="gl-display-flex" :class="status.class">
+ <gl-icon :name="status.icon" class="gl-mr-3" />
+ <span>{{ status.text }}</span>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js
index 951d9324813..853834ed51d 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/jobs/components/table/constants.js
@@ -4,6 +4,9 @@ import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
/* Error constants */
export const POST_FAILURE = 'post_failure';
export const DEFAULT = 'default';
+export const RAW_TEXT_WARNING = s__(
+ 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.',
+);
/* Job Status Constants */
export const JOB_SCHEDULED = 'SCHEDULED';
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 864e322eecd..b141dcf81dd 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -1,26 +1,34 @@
<script>
import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
+import createFlash from '~/flash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
import eventHub from './event_hub';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
import JobsTableTabs from './jobs_table_tabs.vue';
+import { RAW_TEXT_WARNING } from './constants';
export default {
i18n: {
errorMsg: __('There was an error fetching the jobs for your project.'),
loadingAriaLabel: __('Loading'),
},
+ filterSearchBoxStyles:
+ 'gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-gray-100 gl-border-b',
components: {
GlAlert,
GlSkeletonLoader,
+ JobsFilteredSearch,
JobsTable,
JobsTableEmptyState,
JobsTableTabs,
GlIntersectionObserver,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
@@ -54,19 +62,37 @@ export default {
hasError: false,
isAlertDismissed: false,
scope: null,
- firstLoad: true,
+ infiniteScrollingTriggered: false,
+ filterSearchTriggered: false,
};
},
computed: {
+ loading() {
+ return this.$apollo.queries.jobs.loading;
+ },
shouldShowAlert() {
return this.hasError && !this.isAlertDismissed;
},
+ // Show when on All tab with no jobs
+ // Show only when not loading and filtered search has not been triggered
+ // So we don't show empty state when results are empty on a filtered search
showEmptyState() {
- return this.jobs.list.length === 0 && !this.scope;
+ return (
+ this.jobs.list.length === 0 && !this.scope && !this.loading && !this.filterSearchTriggered
+ );
},
hasNextPage() {
return this.jobs?.pageInfo?.hasNextPage;
},
+ showLoadingSpinner() {
+ return this.loading && this.infiniteScrollingTriggered;
+ },
+ showSkeletonLoader() {
+ return this.loading && !this.showLoadingSpinner;
+ },
+ showFilteredSearch() {
+ return this.glFeatures?.jobsTableVueSearch && !this.scope;
+ },
},
mounted() {
eventHub.$on('jobActionPerformed', this.handleJobAction);
@@ -79,16 +105,38 @@ export default {
this.$apollo.queries.jobs.refetch({ statuses: this.scope });
},
fetchJobsByStatus(scope) {
- this.firstLoad = true;
+ this.infiniteScrollingTriggered = false;
this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope });
},
+ filterJobsBySearch(filters) {
+ this.infiniteScrollingTriggered = false;
+ this.filterSearchTriggered = true;
+
+ // Eventually there will be more tokens available
+ // this code is written to scale for those tokens
+ filters.forEach((filter) => {
+ // Raw text input in filtered search does not have a type
+ // when a user enters raw text we alert them that it is
+ // not supported and we do not make an additional API call
+ if (!filter.type) {
+ createFlash({
+ message: RAW_TEXT_WARNING,
+ type: 'warning',
+ });
+ }
+
+ if (filter.type === 'status') {
+ this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
+ }
+ });
+ },
fetchMoreJobs() {
- this.firstLoad = false;
+ if (!this.loading) {
+ this.infiniteScrollingTriggered = true;
- if (!this.$apollo.queries.jobs.loading) {
this.$apollo.queries.jobs.fetchMore({
variables: {
fullPath: this.fullPath,
@@ -115,7 +163,13 @@ export default {
<jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" />
- <div v-if="$apollo.loading && firstLoad" class="gl-mt-5">
+ <jobs-filtered-search
+ v-if="showFilteredSearch"
+ :class="$options.filterSearchBoxStyles"
+ @filterJobsBySearch="filterJobsBySearch"
+ />
+
+ <div v-if="showSkeletonLoader" class="gl-mt-5">
<gl-skeleton-loader :width="1248" :height="73">
<circle cx="748.031" cy="37.7193" r="15.0307" />
<circle cx="787.241" cy="37.7193" r="15.0307" />
@@ -138,7 +192,7 @@ export default {
<gl-intersection-observer v-if="hasNextPage" @appear="fetchMoreJobs">
<gl-loading-icon
- v-if="$apollo.loading"
+ v-if="showLoadingSpinner"
size="md"
:aria-label="$options.i18n.loadingAriaLabel"
/>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
index 26791e4284d..ad4651a7c9f 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
@@ -50,7 +50,7 @@ export default {
</script>
<template>
- <gl-tabs content-class="gl-pb-0">
+ <gl-tabs content-class="gl-py-0">
<gl-tab
v-for="tab in tabs"
:key="tab.text"
diff --git a/app/assets/javascripts/pages/import/history/components/import_error_details.vue b/app/assets/javascripts/pages/import/history/components/import_error_details.vue
new file mode 100644
index 00000000000..837b0c0cabb
--- /dev/null
+++ b/app/assets/javascripts/pages/import/history/components/import_error_details.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import API from '~/api';
+import { createAlert } from '~/flash';
+import { DEFAULT_ERROR } from '../utils/error_messages';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ props: {
+ id: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ error: null,
+ };
+ },
+ async mounted() {
+ try {
+ const {
+ data: { import_error: importError },
+ } = await API.project(this.id);
+ this.error = importError;
+ } catch (e) {
+ createAlert({ message: DEFAULT_ERROR });
+ this.error = null;
+ } finally {
+ this.loading = false;
+ }
+ },
+};
+</script>
+<template>
+ <gl-loading-icon v-if="loading" size="md" />
+ <pre v-else>{{ error || s__('BulkImport|No additional information provided.') }}</pre>
+</template>
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
new file mode 100644
index 00000000000..557e25f66e2
--- /dev/null
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -0,0 +1,199 @@
+<script>
+import { GlButton, GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import createFlash from '~/flash';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { getProjects } from '~/rest_api';
+import ImportStatus from '~/import_entities/components/import_status.vue';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { DEFAULT_ERROR } from '../utils/error_messages';
+import ImportErrorDetails from './import_error_details.vue';
+
+const DEFAULT_PER_PAGE = 20;
+const DEFAULT_TH_CLASSES =
+ 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
+
+const tableCell = (config) => ({
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: (value, key, item) => {
+ return {
+ // eslint-disable-next-line no-underscore-dangle
+ 'gl-border-b-0!': item._showDetails,
+ };
+ },
+ ...config,
+});
+
+export default {
+ components: {
+ GlButton,
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlTable,
+ PaginationBar,
+ ImportStatus,
+ ImportErrorDetails,
+ TimeAgo,
+ },
+
+ inject: ['assets'],
+
+ data() {
+ return {
+ loading: true,
+ historyItems: [],
+ paginationConfig: {
+ page: 1,
+ perPage: DEFAULT_PER_PAGE,
+ },
+ pageInfo: {},
+ };
+ },
+
+ fields: [
+ tableCell({
+ key: 'source',
+ label: s__('BulkImport|Source'),
+ thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
+ }),
+ tableCell({
+ key: 'destination',
+ label: s__('BulkImport|Destination'),
+ thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
+ }),
+ tableCell({
+ key: 'created_at',
+ label: __('Date'),
+ }),
+ tableCell({
+ key: 'status',
+ label: __('Status'),
+ tdAttr: { 'data-qa-selector': 'import_status_indicator' },
+ }),
+ ],
+
+ computed: {
+ hasHistoryItems() {
+ return this.historyItems.length > 0;
+ },
+ },
+
+ watch: {
+ paginationConfig: {
+ handler() {
+ this.loadHistoryItems();
+ },
+ deep: true,
+ immediate: true,
+ },
+ },
+
+ methods: {
+ async loadHistoryItems() {
+ try {
+ this.loading = true;
+ const { data: historyItems, headers } = await getProjects(undefined, {
+ imported: true,
+ simple: false,
+ page: this.paginationConfig.page,
+ per_page: this.paginationConfig.perPage,
+ });
+ this.pageInfo = parseIntPagination(normalizeHeaders(headers));
+ this.historyItems = historyItems;
+ } catch (e) {
+ createFlash({ message: DEFAULT_ERROR, captureError: true, error: e });
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ hasHttpProtocol(url) {
+ try {
+ const parsedUrl = new URL(url);
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch (e) {
+ return false;
+ }
+ },
+
+ setPageSize(size) {
+ this.paginationConfig.perPage = size;
+ this.paginationConfig.page = 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
+ >
+ <h1 class="gl-my-0 gl-py-4 gl-font-size-h1">
+ <img :src="assets.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
+ {{ s__('BulkImport|Project import history') }}
+ </h1>
+ </div>
+ <gl-loading-icon v-if="loading" size="md" class="gl-mt-5" />
+ <gl-empty-state
+ v-else-if="!hasHistoryItems"
+ :title="s__('BulkImport|No history is available')"
+ :description="s__('BulkImport|Your imported projects will appear here.')"
+ />
+ <template v-else>
+ <gl-table
+ :fields="$options.fields"
+ :items="historyItems"
+ data-qa-selector="import_history_table"
+ class="gl-w-full"
+ >
+ <template #cell(source)="{ item }">
+ <template v-if="item.import_url">
+ <gl-link
+ v-if="hasHttpProtocol(item.import_url)"
+ :href="item.import_url"
+ target="_blank"
+ >
+ {{ item.import_url }}
+ <gl-icon name="external-link" class="gl-vertical-align-middle" />
+ </gl-link>
+ <span v-else>{{ item.import_url }}</span>
+ </template>
+ <span v-else>{{
+ s__('BulkImport|Template / File-based import / GitLab Migration')
+ }}</span>
+ </template>
+ <template #cell(destination)="{ item }">
+ <gl-link :href="item.http_url_to_repo">
+ {{ item.path_with_namespace }}
+ </gl-link>
+ </template>
+ <template #cell(created_at)="{ value }">
+ <time-ago :time="value" />
+ </template>
+ <template #cell(status)="{ item, toggleDetails, detailsShowing }">
+ <import-status :status="item.import_status" class="gl-display-inline-block gl-w-13" />
+ <gl-button
+ v-if="item.import_status === 'failed'"
+ class="gl-ml-3"
+ :selected="detailsShowing"
+ @click="toggleDetails"
+ >{{ __('Details') }}</gl-button
+ >
+ </template>
+ <template #row-details="{ item }">
+ <import-error-details :id="item.id" />
+ </template>
+ </gl-table>
+ <pagination-bar
+ :page-info="pageInfo"
+ class="gl-m-0 gl-mt-3"
+ @set-page="paginationConfig.page = $event"
+ @set-page-size="setPageSize"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/import/history/index.js b/app/assets/javascripts/pages/import/history/index.js
new file mode 100644
index 00000000000..d540272c266
--- /dev/null
+++ b/app/assets/javascripts/pages/import/history/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import ImportHistoryApp from './components/import_history_app.vue';
+
+function mountImportHistoryApp(mountElement) {
+ if (!mountElement) return undefined;
+
+ return new Vue({
+ el: mountElement,
+ name: 'ImportHistoryRoot',
+ provide: {
+ assets: {
+ gitlabLogo: mountElement.dataset.logo,
+ },
+ },
+ render(createElement) {
+ return createElement(ImportHistoryApp);
+ },
+ });
+}
+
+mountImportHistoryApp(document.querySelector('#import-history-mount-element'));
diff --git a/app/assets/javascripts/pages/import/history/utils/error_messages.js b/app/assets/javascripts/pages/import/history/utils/error_messages.js
new file mode 100644
index 00000000000..24669e22ade
--- /dev/null
+++ b/app/assets/javascripts/pages/import/history/utils/error_messages.js
@@ -0,0 +1,3 @@
+import { __ } from '~/locale';
+
+export const DEFAULT_ERROR = __('Something went wrong on our end.');
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
index a8ad56ab6a5..897bd2dcccf 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
@@ -22,7 +22,6 @@ export default {
),
},
components: {
- GlCard,
GlLink,
GlSprintf,
},
@@ -30,22 +29,20 @@ export default {
};
</script>
<template>
- <gl-card>
- <template #default>
- <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
- <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
- <ol class="gl-mb-3">
- <li v-for="(item, i) in $options.i18n.listItems" :key="`li-${i}`">{{ item }}</li>
- </ol>
- <p class="gl-mb-0">
- <gl-sprintf :message="$options.i18n.note">
- <template #link="{ content }">
- <gl-link :href="runnerHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </template>
- </gl-card>
+ <div>
+ <h3 class="gl-font-lg gl-mt-0 gl-mb-5">{{ $options.i18n.title }}</h3>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <ol class="gl-mb-3">
+ <li v-for="(item, i) in $options.i18n.listItems" :key="`li-${i}`">{{ item }}</li>
+ </ol>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.note">
+ <template #link="{ content }">
+ <gl-link :href="runnerHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
index 3da535f5f94..d2682cf6326 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCard, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
@@ -13,23 +13,20 @@ export default {
),
},
components: {
- GlCard,
GlSprintf,
},
};
</script>
<template>
- <gl-card>
- <template #default>
- <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
- <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
- <p class="gl-mb-0">
- <gl-sprintf :message="$options.i18n.secondParagraph">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- </template>
- </gl-card>
+ <div>
+ <h3 class="gl-font-lg gl-mt-0 gl-mb-5">{{ $options.i18n.title }}</h3>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.secondParagraph">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
index f714f6411f1..04140434af2 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
@@ -1,5 +1,5 @@
<script>
-import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
@@ -20,7 +20,6 @@ export default {
),
},
components: {
- GlCard,
GlLink,
GlSprintf,
},
@@ -28,48 +27,46 @@ export default {
};
</script>
<template>
- <gl-card>
- <template #default>
- <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
- <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
- <ul>
- <li>
- <gl-sprintf :message="$options.i18n.browseExamples">
- <template #link="{ content }">
- <gl-link :href="ciExamplesHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="$options.i18n.viewSyntaxRef">
- <template #link="{ content }">
- <gl-link :href="ymlHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="$options.i18n.learnMore">
- <template #link="{ content }">
- <gl-link :href="ciHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="$options.i18n.needs">
- <template #link="{ content }">
- <gl-link :href="needsHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </li>
- </ul>
- </template>
- </gl-card>
+ <div>
+ <h3 class="gl-font-lg gl-mt-0 gl-mb-5">{{ $options.i18n.title }}</h3>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <ul>
+ <li>
+ <gl-sprintf :message="$options.i18n.browseExamples">
+ <template #link="{ content }">
+ <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.viewSyntaxRef">
+ <template #link="{ content }">
+ <gl-link :href="ymlHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.learnMore">
+ <template #link="{ content }">
+ <gl-link :href="ciHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.needs">
+ <template #link="{ content }">
+ <gl-link :href="needsHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ </div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
index 512414f0246..aeeb52319d2 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
@@ -1,5 +1,4 @@
<script>
-import { GlCard } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
@@ -9,16 +8,11 @@ export default {
'PipelineEditorTutorial|Use the Visualize and Lint tabs in the Pipeline Editor to visualize your pipeline and check for any errors or warnings before committing your changes.',
),
},
- components: {
- GlCard,
- },
};
</script>
<template>
- <gl-card>
- <template #default>
- <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
- <p class="gl-mb-0">{{ $options.i18n.firstParagraph }}</p>
- </template>
- </gl-card>
+ <div>
+ <h3 class="gl-font-lg gl-mt-0 gl-mb-5">{{ $options.i18n.title }}</h3>
+ <p class="gl-mb-0">{{ $options.i18n.firstParagraph }}</p>
+ </div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index 9cb070a5517..375db7f3054 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -1,101 +1,61 @@
<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlDrawer } from '@gitlab/ui';
import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue';
import PipelineConfigReferenceCard from './cards/pipeline_config_reference_card.vue';
import VisualizeAndLintCard from './cards/visualize_and_lint_card.vue';
+const DRAWER_CARD_STYLES = ['gl-border-bottom-0', 'gl-pt-6!', 'gl-pb-0!', 'gl-line-height-20'];
+
export default {
- width: {
- expanded: '482px',
- collapsed: '58px',
- },
+ DRAWER_CARD_STYLES,
i18n: {
- toggleTxt: __('Collapse'),
+ title: __('Help'),
},
- localDrawerKey: DRAWER_EXPANDED_KEY,
components: {
FirstPipelineCard,
GettingStartedCard,
- GlButton,
- GlIcon,
- LocalStorageSync,
+ GlDrawer,
PipelineConfigReferenceCard,
VisualizeAndLintCard,
},
- data() {
- return {
- isExpanded: false,
- topPosition: 0,
- };
+ props: {
+ isVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- buttonIconName() {
- return this.isExpanded ? 'chevron-double-lg-right' : 'chevron-double-lg-left';
- },
- buttonClass() {
- return this.isExpanded ? 'gl-justify-content-end!' : '';
+ drawerCardStyles() {
+ return '';
},
- rootStyle() {
- const { expanded, collapsed } = this.$options.width;
- const top = this.topPosition;
- const style = { top: `${top}px` };
-
- return this.isExpanded ? { ...style, width: expanded } : { ...style, width: collapsed };
+ drawerHeightOffset() {
+ const wrapperEl = document.querySelector('.content-wrapper');
+ return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
},
},
- mounted() {
- this.setTopPosition();
- },
methods: {
- setTopPosition() {
- const navbarEl = document.querySelector('.js-navbar');
-
- if (navbarEl) {
- this.topPosition = navbarEl.getBoundingClientRect().bottom;
- }
- },
- toggleDrawer() {
- this.isExpanded = !this.isExpanded;
+ closeDrawer() {
+ this.$emit('close-drawer');
},
},
};
</script>
<template>
- <local-storage-sync v-model="isExpanded" :storage-key="$options.localDrawerKey" as-json>
- <aside
- aria-live="polite"
- class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-property-width gl-transition-duration-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-200 gl-overflow-y-auto"
- :style="rootStyle"
- >
- <gl-button
- category="tertiary"
- class="gl-w-full gl-h-9 gl-rounded-0! gl-border-none! gl-border-b-solid! gl-border-1! gl-border-gray-100 gl-text-decoration-none! gl-outline-0! gl-display-flex"
- :class="buttonClass"
- :title="__('Toggle sidebar')"
- data-qa-selector="toggle_sidebar_collapse_button"
- @click="toggleDrawer"
- >
- <span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">
- {{ __('Collapse') }}
- </span>
- <gl-icon data-testid="toggle-icon" :name="buttonIconName" />
- </gl-button>
- <div
- v-if="isExpanded"
- class="gl-h-full gl-p-5"
- data-testid="drawer-content"
- data-qa-selector="drawer_content"
- >
- <getting-started-card class="gl-mb-4" />
- <first-pipeline-card class="gl-mb-4" />
- <visualize-and-lint-card class="gl-mb-4" />
- <pipeline-config-reference-card />
- <div class="gl-h-13"></div>
- </div>
- </aside>
- </local-storage-sync>
+ <gl-drawer
+ :header-height="drawerHeightOffset"
+ :open="isVisible"
+ :z-index="200"
+ @close="closeDrawer"
+ >
+ <template #title>
+ <h2 class="gl-m-0 gl-font-lg">{{ $options.i18n.title }}</h2>
+ </template>
+ <getting-started-card :class="$options.DRAWER_CARD_STYLES" />
+ <first-pipeline-card :class="$options.DRAWER_CARD_STYLES" />
+ <visualize-and-lint-card :class="$options.DRAWER_CARD_STYLES" />
+ <pipeline-config-reference-card :class="$options.DRAWER_CARD_STYLES" />
+ </gl-drawer>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
index b4e9ab81d38..9765d669fc1 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
@@ -7,13 +7,23 @@ import { pipelineEditorTrackingOptions, TEMPLATE_REPOSITORY_URL } from '../../co
export default {
i18n: {
browseTemplates: __('Browse templates'),
+ help: __('Help'),
},
TEMPLATE_REPOSITORY_URL,
components: {
GlButton,
},
mixins: [Tracking.mixin()],
+ props: {
+ showDrawer: {
+ type: Boolean,
+ required: true,
+ },
+ },
methods: {
+ toggleDrawer() {
+ this.$emit(this.showDrawer ? 'close-drawer' : 'open-drawer');
+ },
trackTemplateBrowsing() {
const { label, actions } = pipelineEditorTrackingOptions;
@@ -30,9 +40,20 @@ export default {
size="small"
icon="external-link"
target="_blank"
+ data-testid="template-repo-link"
+ data-qa-selector="template_repo_link"
@click="trackTemplateBrowsing"
>
{{ $options.i18n.browseTemplates }}
</gl-button>
+ <gl-button
+ icon="information-o"
+ size="small"
+ data-testid="drawer-toggle"
+ data-qa-selector="drawer_toggle"
+ @click="toggleDrawer"
+ >
+ {{ $options.i18n.help }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 5cff93c884f..d50e6f9a623 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -86,6 +86,10 @@ export default {
type: Boolean,
required: true,
},
+ showDrawer: {
+ type: Boolean,
+ required: true,
+ },
},
apollo: {
appStatus: {
@@ -157,7 +161,7 @@ export default {
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
<walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
- <ci-editor-header />
+ <ci-editor-header :show-drawer="showDrawer" v-on="$listeners" />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 2ebc4306405..5d841bb9a87 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -45,8 +45,6 @@ export const TAB_QUERY_PARAM = 'tab';
export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE';
-export const DRAWER_EXPANDED_KEY = 'pipeline_editor_drawer_expanded';
-
export const BRANCH_PAGINATION_LIMIT = 20;
export const BRANCH_SEARCH_DEBOUNCE = '500';
export const SOURCE_EDITOR_DEBOUNCE = 500;
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index a5436ca63cb..4e6a4ffa6d2 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -388,7 +388,7 @@ export default {
@createEmptyConfigFile="setNewEmptyCiConfigFile"
@refetchContent="refetchContent"
/>
- <div v-else class="gl-pr-10">
+ <div v-else>
<pipeline-editor-messages
:failure-type="failureType"
:failure-reasons="failureReasons"
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 631dd8a2c00..23e3ce10d5a 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -60,6 +60,7 @@ export default {
currentTab: CREATE_TAB,
scrollToCommitForm: false,
shouldLoadNewBranch: false,
+ showDrawer: false,
showSwitchBranchModal: false,
};
},
@@ -72,9 +73,15 @@ export default {
closeBranchModal() {
this.showSwitchBranchModal = false;
},
+ closeDrawer() {
+ this.showDrawer = false;
+ },
handleConfirmSwitchBranch() {
this.showSwitchBranchModal = true;
},
+ openDrawer() {
+ this.showDrawer = true;
+ },
switchBranch() {
this.showSwitchBranchModal = false;
this.shouldLoadNewBranch = true;
@@ -122,7 +129,10 @@ export default {
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
+ :show-drawer="showDrawer"
v-on="$listeners"
+ @open-drawer="openDrawer"
+ @close-drawer="closeDrawer"
@set-current-tab="setCurrentTab"
@walkthrough-popover-cta-clicked="setScrollToCommitForm"
/>
@@ -137,6 +147,10 @@ export default {
@scrolled-to-commit-form="setScrollToCommitForm(false)"
v-on="$listeners"
/>
- <pipeline-editor-drawer />
+ <pipeline-editor-drawer
+ :is-visible="showDrawer"
+ v-on="$listeners"
+ @close-drawer="closeDrawer"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 8aba91eedf7..9abd45424e7 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -1,9 +1,9 @@
<script>
import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
-import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale';
+import { fetchPolicies } from '~/lib/graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@@ -37,7 +37,7 @@ import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = {
query: runnersAdminCountQuery,
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
return data?.runners?.count;
},
@@ -78,10 +78,7 @@ export default {
apollo: {
runners: {
query: runnersAdminQuery,
- // Runners can be updated by users directly in this list.
- // A "cache and network" policy prevents outdated filtered
- // results.
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
},
@@ -224,9 +221,19 @@ export default {
}
return '';
},
+ refetchFilteredCounts() {
+ this.$apollo.queries.allRunnersCount.refetch();
+ this.$apollo.queries.instanceRunnersCount.refetch();
+ this.$apollo.queries.groupRunnersCount.refetch();
+ this.$apollo.queries.projectRunnersCount.refetch();
+ },
+ onToggledPaused() {
+ // When a runner is Paused, the tab count can
+ // become stale, refetch outdated counts.
+ this.refetchFilteredCounts();
+ },
onDeleted({ message }) {
this.$root.$toast?.show(message);
- this.$apollo.queries.runners.refetch();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -289,6 +296,7 @@ export default {
<runner-actions-cell
:runner="runner"
:edit-url="runner.editAdminUrl"
+ @toggledPaused="onToggledPaused"
@deleted="onDeleted"
/>
</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index d3535f89427..7a4760f81ee 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -23,7 +23,7 @@ export default {
required: false,
},
},
- emits: ['deleted'],
+ emits: ['toggledPaused', 'deleted'],
computed: {
canUpdate() {
return this.runner.userPermissions?.updateRunner;
@@ -33,6 +33,9 @@ export default {
},
},
methods: {
+ onToggledPaused() {
+ this.$emit('toggledPaused');
+ },
onDeleted(value) {
this.$emit('deleted', value);
},
@@ -43,7 +46,12 @@ export default {
<template>
<gl-button-group>
<runner-edit-button v-if="canUpdate && editUrl" :href="editUrl" />
- <runner-pause-button v-if="canUpdate" :runner="runner" :compact="true" />
+ <runner-pause-button
+ v-if="canUpdate"
+ :runner="runner"
+ :compact="true"
+ @toggledPaused="onToggledPaused"
+ />
<runner-delete-button
:disabled="!canDelete"
:runner="runner"
diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/runner/components/runner_delete_button.vue
index a546a2788de..f436ac0ce52 100644
--- a/app/assets/javascripts/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/runner/components/runner_delete_button.vue
@@ -126,6 +126,11 @@ export default {
id: this.runner.id,
},
},
+ update: (cache) => {
+ // Remove deleted runner from the cache
+ const cacheId = cache.identify(this.runner);
+ cache.evict({ id: cacheId });
+ },
});
if (errors && errors.length) {
throw new Error(errors.join(' '));
diff --git a/app/assets/javascripts/runner/components/runner_pause_button.vue b/app/assets/javascripts/runner/components/runner_pause_button.vue
index c88634bfbd9..334e5f6023a 100644
--- a/app/assets/javascripts/runner/components/runner_pause_button.vue
+++ b/app/assets/javascripts/runner/components/runner_pause_button.vue
@@ -24,6 +24,7 @@ export default {
default: false,
},
},
+ emits: ['toggledPaused'],
data() {
return {
updating: false,
@@ -83,6 +84,7 @@ export default {
if (errors && errors.length) {
throw new Error(errors.join(' '));
}
+ this.$emit('toggledPaused');
} catch (e) {
this.onError(e);
} finally {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 35fd7fff6d3..00b3cd36846 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,9 +1,9 @@
<script>
import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
-import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale';
+import { fetchPolicies } from '~/lib/graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@@ -35,7 +35,7 @@ import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = {
query: groupRunnersCountQuery,
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) {
return data?.group?.runners?.count;
},
@@ -85,10 +85,7 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
- // Runners can be updated by users directly in this list.
- // A "cache and network" policy prevents outdated filtered
- // results.
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
},
@@ -241,9 +238,18 @@ export default {
editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit;
},
+ refetchFilteredCounts() {
+ this.$apollo.queries.allRunnersCount.refetch();
+ this.$apollo.queries.groupRunnersCount.refetch();
+ this.$apollo.queries.projectRunnersCount.refetch();
+ },
+ onToggledPaused() {
+ // When a runner is Paused, the tab count can
+ // become stale, refetch outdated counts.
+ this.refetchFilteredCounts();
+ },
onDeleted({ message }) {
this.$root.$toast?.show(message);
- this.$apollo.queries.runners.refetch();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -302,7 +308,12 @@ export default {
</gl-link>
</template>
<template #runner-actions-cell="{ runner }">
- <runner-actions-cell :runner="runner" :edit-url="editUrl(runner)" @deleted="onDeleted" />
+ <runner-actions-cell
+ :runner="runner"
+ :edit-url="editUrl(runner)"
+ @toggledPaused="onToggledPaused"
+ @deleted="onDeleted"
+ />
</template>
</runner-list>
<runner-pagination
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index c48c9067250..3d4074fbb02 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -4,6 +4,7 @@ import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
@@ -30,6 +31,7 @@ export const i18n = {
securityTrainingDescription: s__(
'SecurityConfiguration|Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.',
),
+ securityTrainingDoc: s__('SecurityConfiguration|Learn more about vulnerability training'),
};
export default {
@@ -125,6 +127,9 @@ export default {
},
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
+ securityTraininDocLink: helpPagePath('user/application_security/vulnerabilities/index', {
+ anchor: 'enable-security-training-for-vulnerabilities',
+ }),
};
</script>
@@ -262,6 +267,11 @@ export default {
<p>
{{ $options.i18n.securityTrainingDescription }}
</p>
+ <p>
+ <gl-link :href="$options.securityTraininDocLink">{{
+ $options.i18n.securityTrainingDoc
+ }}</gl-link>
+ </p>
</template>
<template #features>
<training-provider-list />
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 44beceb4f48..9494a686467 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -18,6 +18,8 @@ class IdeController < ApplicationController
feature_category :web_ide
+ urgency :low
+
def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end
diff --git a/app/controllers/import/history_controller.rb b/app/controllers/import/history_controller.rb
new file mode 100644
index 00000000000..69e31392f21
--- /dev/null
+++ b/app/controllers/import/history_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class Import::HistoryController < ApplicationController
+ feature_category :importers
+end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index b0f032a01e5..dadf0747345 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -18,6 +18,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action :push_jobs_table_vue, only: [:index]
+ before_action :push_jobs_table_vue_search, only: [:index]
before_action do
push_frontend_feature_flag(:infinitely_collapsible_sections, @project, default_enabled: :yaml)
@@ -269,4 +270,8 @@ class Projects::JobsController < Projects::ApplicationController
def push_jobs_table_vue
push_frontend_feature_flag(:jobs_table_vue, @project, default_enabled: :yaml)
end
+
+ def push_jobs_table_vue_search
+ push_frontend_feature_flag(:jobs_table_vue_search, @project, default_enabled: :yaml)
+ end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 97f9c5814e2..22b0fb2e228 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -18,6 +18,8 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml)
end
+ urgency :low, [:index]
+
def index
@snippet_counts = ::Snippets::CountService
.new(current_user, project: @project)
diff --git a/app/controllers/projects/web_ide_schemas_controller.rb b/app/controllers/projects/web_ide_schemas_controller.rb
index 84a191815f4..cdc416de6c9 100644
--- a/app/controllers/projects/web_ide_schemas_controller.rb
+++ b/app/controllers/projects/web_ide_schemas_controller.rb
@@ -5,6 +5,8 @@ class Projects::WebIdeSchemasController < Projects::ApplicationController
feature_category :web_ide
+ urgency :low
+
def show
return respond_422 unless branch_sha
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 507a8b66942..e76cae5feee 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -82,13 +82,6 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved?
- experiment(:new_project_sast_enabled, user: current_user).track(:created,
- property: active_new_project_tab,
- checked: Gitlab::Utils.to_boolean(project_params[:initialize_with_sast]),
- project: @project,
- namespace: @project.namespace
- )
-
redirect_to(
project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
diff --git a/app/controllers/snippets/blobs_controller.rb b/app/controllers/snippets/blobs_controller.rb
index d7c4bbcf8f2..c9a78f39c89 100644
--- a/app/controllers/snippets/blobs_controller.rb
+++ b/app/controllers/snippets/blobs_controller.rb
@@ -2,6 +2,7 @@
class Snippets::BlobsController < Snippets::ApplicationController
include Snippets::BlobsActions
+ urgency :low
skip_before_action :authenticate_user!, only: [:raw]
end
diff --git a/app/experiments/new_project_sast_enabled_experiment.rb b/app/experiments/new_project_sast_enabled_experiment.rb
deleted file mode 100644
index 4aca4c875b2..00000000000
--- a/app/experiments/new_project_sast_enabled_experiment.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class NewProjectSastEnabledExperiment < ApplicationExperiment
- control { }
- variant(:candidate) { }
- variant(:free_indicator) { }
- variant(:unchecked_candidate) { }
- variant(:unchecked_free_indicator) { }
-
- def publish(*args)
- super
-
- publish_to_database
- end
-end
diff --git a/app/views/import/history/index.html.haml b/app/views/import/history/index.html.haml
new file mode 100644
index 00000000000..bca2d884848
--- /dev/null
+++ b/app/views/import/history/index.html.haml
@@ -0,0 +1,4 @@
+- add_to_breadcrumbs _('Create a new project'), new_project_path
+- page_title _('Import history')
+
+#import-history-mount-element{ data: { logo: asset_url('gitlab_logo.png') } }
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 52eea73ecd2..9bac0f04d23 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -53,7 +53,7 @@
= _('Gitaly Servers')
= nav_link(controller: admin_analytics_nav_links) do
- = link_to admin_dev_ops_report_path, data: { qa_selector: 'admin_analytics_link' }, class: 'has-sub-items' do
+ = link_to admin_dev_ops_reports_path, data: { qa_selector: 'admin_analytics_link' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('chart')
%span.nav-item-name
@@ -61,12 +61,12 @@
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_analytics_submenu_content' } }
= nav_link(controller: admin_analytics_nav_links, html_options: { class: "fly-out-top-item" }) do
- = link_to admin_dev_ops_report_path do
+ = link_to admin_dev_ops_reports_path do
%strong.fly-out-top-item-name
= _('Analytics')
%li.divider.fly-out-top-item
= nav_link(controller: :dev_ops_report) do
- = link_to admin_dev_ops_report_path, title: _('DevOps Reports') do
+ = link_to admin_dev_ops_reports_path, title: _('DevOps Reports') do
%span
= _('DevOps Reports')
= nav_link(controller: :usage_trends) do
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index a8b809d1871..87e4d30ad80 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -3,8 +3,9 @@
.project-import
.form-group.import-btn-container.clearfix
- %h5
+ %h5.gl-display-flex
= _("Import project from")
+ = link_to _('History'), import_history_index_path, class: 'gl-link gl-ml-auto gl-font-weight-normal'
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body', qa_selector: 'gitlab_import_button' } }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 1fb045544aa..d30a7cc3172 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -64,15 +64,14 @@
.form-text.text-muted
= s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
- - experiment(:new_project_sast_enabled, user: current_user) do |e|
- - e.variant(:candidate) do
- = render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: false
- - e.variant(:unchecked_candidate) do
- = render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: false
- - e.variant(:free_indicator) do
- = render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: true
- - e.variant(:unchecked_free_indicator) do
- = render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: true
+ .form-group
+ .form-check.gl-mb-3
+ = check_box_tag 'project[initialize_with_sast]', '1', false, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
+ = label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
+ = s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
+ .form-text.text-muted
+ = s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
+ = link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed' }
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { qa_selector: 'project_create_button', track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
diff --git a/app/views/projects/_new_project_initialize_with_sast.html.haml b/app/views/projects/_new_project_initialize_with_sast.html.haml
deleted file mode 100644
index ec12abbf789..00000000000
--- a/app/views/projects/_new_project_initialize_with_sast.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- experiment_name = local_assigns.fetch(:experiment_name)
-- track_label = local_assigns.fetch(:track_label)
-
-- with_free_badge = local_assigns.fetch(:with_free_badge, false)
-- checked = local_assigns.fetch(:checked, false)
-
-.form-group
- .form-check.gl-mb-3
- = check_box_tag 'project[initialize_with_sast]', '1', checked, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: experiment_name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
- = label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
- = s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
- - if with_free_badge
- = gl_badge_tag _('Free'), variant: :info, size: :sm
- .form-text.text-muted
- = s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
- = link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: experiment_name }
diff --git a/config/feature_flags/experiment/new_project_sast_enabled.yml b/config/feature_flags/development/jobs_table_vue_search.yml
index f47c01d26aa..ad0c25eccce 100644
--- a/config/feature_flags/experiment/new_project_sast_enabled.yml
+++ b/config/feature_flags/development/jobs_table_vue_search.yml
@@ -1,8 +1,8 @@
---
-name: new_project_sast_enabled
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70548
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340929
-milestone: '14.4'
-type: experiment
-group: group::adoption
+name: jobs_table_vue_search
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82539
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356007
+milestone: '14.10'
+type: development
+group: group::pipeline execution
default_enabled: false
diff --git a/config/metrics/counts_28d/20210216175000_i_analytics_dev_ops_score_monthly.yml b/config/metrics/counts_28d/20210216175000_i_analytics_dev_ops_score_monthly.yml
index 350363d79ce..016cbb23d49 100644
--- a/config/metrics/counts_28d/20210216175000_i_analytics_dev_ops_score_monthly.yml
+++ b/config/metrics/counts_28d/20210216175000_i_analytics_dev_ops_score_monthly.yml
@@ -1,7 +1,7 @@
---
data_category: optional
key_path: redis_hll_counters.analytics.i_analytics_dev_ops_score_monthly
-description: Unique visitors to /admin/dev_ops_report by month
+description: Unique visitors to /admin/dev_ops_reports by month
product_section: dev
product_stage: manage
product_group: group::optimize
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 6b786fc82b3..d066dd2fb35 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -105,7 +105,8 @@ namespace :admin do
resources :projects, only: [:index]
resources :usage_trends, only: :index
- resource :dev_ops_report, controller: 'dev_ops_report', only: :show
+ resource :dev_ops_reports, controller: 'dev_ops_report', only: :show
+ get 'dev_ops_report', to: redirect('admin/dev_ops_reports')
resources :cohorts, only: :index
scope(path: 'projects/*namespace_id',
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 9c76c4435ff..228c5776197 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -10,6 +10,8 @@ Devise.omniauth_providers.map(&:downcase).each do |provider|
end
namespace :import do
+ resources :history, only: [:index], controller: :history
+
resources :available_namespaces, only: [:index], controller: :available_namespaces
namespace :url do
diff --git a/data/deprecations/14-9-global-search-deprecate-user-email-lookup-limit.yml b/data/deprecations/14-9-global-search-deprecate-user-email-lookup-limit.yml
new file mode 100644
index 00000000000..2b986a11d47
--- /dev/null
+++ b/data/deprecations/14-9-global-search-deprecate-user-email-lookup-limit.yml
@@ -0,0 +1,11 @@
+- name: "user_email_lookup_limit API field"
+ announcement_milestone: "14.9"
+ announcement_date: "2022-03-22"
+ removal_milestone: "15.0"
+ removal_date: "2022-05-22"
+ breaking_change: true
+ reporter: fzimmer
+ body: | # Do not modify this line, instead modify the lines below.
+ The `user_email_lookup_limit` [API field](https://docs.gitlab.com/ee/api/settings.html) is deprecated and will be removed in GitLab 15.0. Until GitLab 15.0, `user_email_lookup_limit` is aliased to `search_rate_limit` and existing workflows will continue to work.
+
+ Any API calls attempting to change the rate limits for `user_email_lookup_limit` should use `search_rate_limit` instead.
diff --git a/data/removals/14_9/14-9-key-user_email_lookup_limit.yml b/data/removals/14_9/14-9-key-user_email_lookup_limit.yml
deleted file mode 100644
index 52b64a96b87..00000000000
--- a/data/removals/14_9/14-9-key-user_email_lookup_limit.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-- name: "Renamed 'user_email_lookup_limit' to 'search_rate_limit' API field"
- announcement_milestone: "14.9"
- announcement_date: "2022-03-22"
- removal_milestone: "14.9"
- removal_date: "2022-03-22"
- breaking_change: true
- reporter: fzimmer
- body: |
- We renamed the rate limit key from `user_email_lookup_limit` to `search_rate_limit`. Any API calls attempting to change the rate limits for `user_email_lookup_limit` should use `search_rate_limit` instead.
diff --git a/data/whats_new/202203210001_14_09.yml b/data/whats_new/202203210001_14_09.yml
index afdbfe353bd..05a5899d751 100644
--- a/data/whats_new/202203210001_14_09.yml
+++ b/data/whats_new/202203210001_14_09.yml
@@ -30,7 +30,7 @@
gitlab-com: true
packages: [Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/ci/environments/deployment_approvals.html#approve-or-reject-a-deployment'
- image_url: 'https://about.gitlab.com/images/unreleased/release-deployment-approval.mp4'
+ image_url: 'https://about.gitlab.com/images/growth/release.png'
published_at: 2022-03-22
release: 14.9
- title: "New design for the Environments Page"
diff --git a/doc/development/fe_guide/registry_architecture.md b/doc/development/fe_guide/registry_architecture.md
new file mode 100644
index 00000000000..47a6dc40e19
--- /dev/null
+++ b/doc/development/fe_guide/registry_architecture.md
@@ -0,0 +1,90 @@
+---
+stage: Package
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Registry architecture
+
+GitLab has several registry applications. Given that they all leverage similar UI, UX, and business
+logic, they are all built with the same architecture. In addition, a set of shared components
+already exists to unify the user and developer experiences.
+
+Existing registries:
+
+- Package Registry
+- Container Registry
+- Infrastructure Registry
+- Dependency Proxy
+
+## Frontend architecture
+
+### Component classification
+
+All the registries follow an architecture pattern that includes four component types:
+
+- Pages: represent an entire app, or for the registries using [vue-router](https://v3.router.vuejs.org/) they represent one router
+ route.
+- Containers: represent a single piece of functionality. They contain complex logic and may
+ connect to the API.
+- Presentationals: represent a portion of the UI. They receive all their data with `props` or through
+ `inject`, and do not connect to the API.
+- Shared components: presentational components that accept a various array of configurations and are
+ shared across all of the registries.
+
+### Communicating with the API
+
+The complexity and communication with the API should be concentrated in the pages components, and
+in the container components when needed. This makes it easier to:
+
+- Handle concurrent requests, loading states, and user messages.
+- Maintain the code, especially to estimate work. If it touches a page or functional component,
+ expect it to be more complex.
+- Write fast and consistent unit tests.
+
+### Best practices
+
+- Use [`provide` or `inject`](https://v2.vuejs.org/v2/api/?redirect=true#provide-inject)
+ to pass static, non-reactive values coming from the app initialization.
+- When passing data, prefer `props` over nested queries or Vuex bindings. Only pages and
+ container components should be aware of the state and API communication.
+- Don't repeat yourself. If one registry receives functionality, the likelihood of the rest needing
+ it in the future is high. If something seems reusable and isn't bound to the state, create a
+ shared component.
+- Try to express functionality and logic with dedicated components. It's much easier to deal with
+ events and properties than callbacks and asynchronous code (see
+ [`delete_package.vue`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue)).
+- Leverage [startup for GraphQL calls](graphql.md#making-initial-queries-early-with-graphql-startup-calls).
+
+## Shared compoenents library
+
+Inside `vue_shared/components/registry` and `packages_and_registries/shared`, there's a set of
+shared components that you can use to implement registry functionality. These components build the
+main pieces of the desired UI and UX of a registry page. The most important components are:
+
+- `code-instruction`: represents a copyable box containing code. Supports multiline and single line
+ code boxes. Snowplow tracks the code copy event.
+- `details-row`: represents a row of details. Used to add additional info in the details area of
+ the `list-item` component.
+- `history-item`: represents a history list item used to build a timeline.
+- `list-item`: represents a list element in the registry. It supports: left action, left primary and
+ secondary content, right primary and secondary content, right action, and details slots.
+- `metadata-item`: represents one piece of metadata, with an icon or a link. Used primarily in the
+ title area.
+- `persisted-dropdown-selection`: represents a dropdown menu that stores the user selection in the
+ `localStorage`.
+- `registry-search`: implements `gl-filtered-search` with a sorting section on the right.
+- `title-area`: implements the top title area of the registry. Includes: a main title, an avatar, a
+ subtitle, a metadata row, and a right actions slot.
+
+## Adding a new registry page
+
+When adding a new registry:
+
+- Leverage the shared components that already exist. It's good to look at how the components are
+ structured and used in the more mature registries (for example, the Package Registry).
+- If it's in line with the backend requirements, we suggest using GraphQL for the API. This helps in
+ dealing with the innate performance issue of registries.
+- If possible, we recommend using [Vue Router](https://v3.router.vuejs.org/)
+ and frontend routing. Coupled with Apollo, the caching layer helps with the perceived page
+ performance.
diff --git a/doc/development/service_ping/index.md b/doc/development/service_ping/index.md
index 1968b12f6ee..380dfffbe46 100644
--- a/doc/development/service_ping/index.md
+++ b/doc/development/service_ping/index.md
@@ -48,7 +48,7 @@ make better product decisions.
There are several other benefits to enabling Service Ping:
- As a benefit of having Service Ping active, GitLab lets you analyze the users' activities over time of your GitLab installation.
-- As a benefit of having Service Ping active, GitLab provides you with [DevOps Score](../../user/admin_area/analytics/dev_ops_report.md#devops-score), which gives you an overview of your entire instance's adoption of Concurrent DevOps from planning to monitoring.
+- As a benefit of having Service Ping active, GitLab provides you with [DevOps Score](../../user/admin_area/analytics/dev_ops_reports.md#devops-score), which gives you an overview of your entire instance's adoption of Concurrent DevOps from planning to monitoring.
- You get better, more proactive support (assuming that our TAMs and support organization used the data to deliver more value).
- You get insight and advice into how to get the most value out of your investment in GitLab. Wouldn't you want to know that a number of features or values are not being adopted in your organization?
- You get a report that illustrates how you compare against other similar organizations (anonymized), with specific advice and recommendations on how to improve your DevOps processes.
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 27d5ae70ed7..0a34bf5e403 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -224,14 +224,10 @@ If you need your Review App to stay up for a longer time, you can
`review-deploy` job to update the "latest deployed at" time.
The `review-cleanup` job that automatically runs in scheduled
-pipelines (and is manual in merge request) stops stale Review Apps after 5 days,
+pipelines stops stale Review Apps after 5 days,
deletes their environment after 6 days, and cleans up any dangling Helm releases
and Kubernetes resources after 7 days.
-The `review-gcp-cleanup` job that automatically runs in scheduled pipelines
-(and is manual in merge request) removes any dangling GCP network resources
-that were not removed along with the Kubernetes resources.
-
## Cluster configuration
The cluster is configured via Terraform in the [`engineering-productivity-infrastructure`](https://gitlab.com/gitlab-org/quality/engineering-productivity-infrastructure) project.
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index d38b56bb1f8..a670ba35ab0 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -144,8 +144,8 @@ Service data helps GitLab improve the product experience and provide proactive s
Most data is categorized as optional and can be disabled. Data that is categorized as
operational, like number of issues, pipelines, merge requests, and version, is not configurable.
-Please see our [service usage privacy page](https://about.gitlab.com/handbook/legal/privacy/services-usage-data/)
-for details on what information is collected.
+See our [service usage privacy page](https://about.gitlab.com/handbook/legal/privacy/services-usage-data/)
+for details about what information is collected.
#### Quarterly subscription reconciliation
@@ -268,22 +268,22 @@ instance, ensure you're purchasing enough seats to
If you are an administrator, you can view the status of your subscription:
1. On the top bar, select **Menu > Admin**.
-1. On the left sidebar, select **License**.
+1. On the left sidebar, select **Subscription**.
-The **License** page includes the following details:
+The **Subscription** page includes the following details:
- Licensee
- Plan
- When it was uploaded, started, and when it expires
-It also displays the following important statistics:
+It also displays the following information:
| Field | Description |
|:-------------------|:------------|
| Users in License | The number of users you've paid for in the current license loaded on the system. The number does not change unless you [add seats](#add-seats-to-a-subscription) during your current subscription period. |
| Billable users | The daily count of billable users on your system. The count may change as you block or add users to your instance. |
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
-| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that needs to be paid for at renewal. |
+| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that must be paid before renewal. |
## Export your license usage
@@ -295,7 +295,7 @@ If you are an administrator, you can export your license usage into a CSV:
1. On the left sidebar, select **Subscription**.
1. In the top right, select **Export license usage file**.
-This file contains all the information GitLab needs to manually process quarterly reconciliations or renewals. If your instance is firewalled or air-gapped, you can provide GitLab with this information.
+This file contains the information GitLab uses to manually process quarterly reconciliations or renewals. If your instance is firewalled or air-gapped, you must provide GitLab with this information.
The **License Usage** CSV includes the following details:
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 6f63bd38123..193143ac8c4 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -134,6 +134,20 @@ Since it isn't used in the context of GitLab (the product), `htpasswd` authentic
**Planned removal milestone: 15.0 (2022-05-22)**
+### user_email_lookup_limit API field
+
+WARNING:
+This feature will be changed or removed in 15.0
+as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
+Before updating GitLab, review the details carefully to determine if you need to make any
+changes to your code, settings, or workflow.
+
+The `user_email_lookup_limit` [API field](https://docs.gitlab.com/ee/api/settings.html) is deprecated and will be removed in GitLab 15.0. Until GitLab 15.0, `user_email_lookup_limit` is aliased to `search_rate_limit` and existing workflows will continue to work.
+
+Any API calls attempting to change the rate limits for `user_email_lookup_limit` should use `search_rate_limit` instead.
+
+**Planned removal milestone: 15.0 (2022-05-22)**
+
## 14.8
### Changes to the `CI_JOB_JWT`
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 077390835d4..7e2b4f84fa1 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -42,16 +42,6 @@ In GitLab 14.4, GitLab released an integrated error tracking backend that replac
For additional background on this removal, please reference [Disable Integrated Error Tracking by Default](https://gitlab.com/groups/gitlab-org/-/epics/7580). If you have feedback please add a comment to [Feedback: Removal of Integrated Error Tracking](https://gitlab.com/gitlab-org/gitlab/-/issues/355493).
-### Renamed 'user_email_lookup_limit' to 'search_rate_limit' API field
-
-WARNING:
-This feature was changed or removed in 14.9
-as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
-Before updating GitLab, review the details carefully to determine if you need to make any
-changes to your code, settings, or workflow.
-
-We renamed the rate limit key from `user_email_lookup_limit` to `search_rate_limit`. Any API calls attempting to change the rate limits for `user_email_lookup_limit` should use `search_rate_limit` instead.
-
## 14.6
### Limit the number of triggered pipeline to 25K in free tier
diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md
index 2ad18d5f70e..077718863e7 100644
--- a/doc/user/admin_area/analytics/dev_ops_report.md
+++ b/doc/user/admin_area/analytics/dev_ops_report.md
@@ -1,73 +1,9 @@
---
-stage: Manage
-group: Optimize
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+redirect_to: 'dev_ops_reports.md'
+remove_date: '2022-06-16'
---
-# DevOps Reports **(FREE SELF)**
+This document was moved to [another location](dev_ops_reports.md).
-DevOps Reports give you an overview of your entire instance's adoption of
-[Concurrent DevOps](https://about.gitlab.com/topics/concurrent-devops/)
-from planning to monitoring.
-
-To see DevOps Reports:
-
-1. On the top bar, select **Menu > Admin**.
-1. On the left sidebar, select **Analytics > DevOps Reports**.
-
-## DevOps Score
-
-> [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/20976) from Conversational Development Index in GitLab 12.6.
-
-NOTE:
-To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping). DevOps Score is a comparative tool, so your score data must be centrally processed by GitLab Inc. first.
-
-You can use the DevOps score to compare your DevOps status to other organizations.
-
-The DevOps Score tab displays usage of major GitLab features on your instance over
-the last 30 days, averaged over the number of billable users in that time period.
-You can also see the Leader usage score, calculated from top-performing instances based on
-[Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has collected.
-Your score is compared to the lead score of each feature and then expressed
-as a percentage at the bottom of said feature. Your overall **DevOps Score** is an average of your
-feature scores.
-
-Service Ping data is aggregated on GitLab servers for analysis. Your usage
-information is **not sent** to any other GitLab instances.
-If you have just started using GitLab, it might take a few weeks for data to be collected before this
-feature is available.
-
-## DevOps Adoption **(ULTIMATE SELF)**
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247112) in GitLab 13.7 as a [Beta feature](../../../policy/alpha-beta-support.md#beta-features).
-> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
-> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
-> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
-> - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2.
-> - Multi-select [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2.
-> - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3.
-
-DevOps Adoption shows feature adoption for development, security, and operations.
-
-| Category | Feature |
-| --- | --- |
-| Development | Approvals<br>Code owners<br>Issues<br>Merge requests |
-| Security | DAST<br>Dependency Scanning<br>Fuzz Testing<br>SAST |
-| Operations | Deployments<br>Pipelines<br>Runners |
-
-You can use Group DevOps Adoption to:
-
-- Identify specific subgroups that are lagging in their adoption of GitLab features, so you can guide them on
-their DevOps journey.
-- Find subgroups that have adopted certain features, and provide guidance to other subgroups on
-how to use those features.
-- Verify if you are getting the return on investment that you expected from GitLab.
-
-## Add or remove a group
-
-To add or remove a subgroup from the DevOps Adoption report:
-
-1. Select **Add or remove groups**.
-1. Select the subgroup you want to add or remove and select **Save changes**.
-
-![DevOps Adoption](img/admin_devops_adoption_v14_2.png)
+<!-- This redirect file can be deleted after <2022-06-16>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/user/admin_area/analytics/dev_ops_reports.md b/doc/user/admin_area/analytics/dev_ops_reports.md
new file mode 100644
index 00000000000..2ad18d5f70e
--- /dev/null
+++ b/doc/user/admin_area/analytics/dev_ops_reports.md
@@ -0,0 +1,73 @@
+---
+stage: Manage
+group: Optimize
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# DevOps Reports **(FREE SELF)**
+
+DevOps Reports give you an overview of your entire instance's adoption of
+[Concurrent DevOps](https://about.gitlab.com/topics/concurrent-devops/)
+from planning to monitoring.
+
+To see DevOps Reports:
+
+1. On the top bar, select **Menu > Admin**.
+1. On the left sidebar, select **Analytics > DevOps Reports**.
+
+## DevOps Score
+
+> [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/20976) from Conversational Development Index in GitLab 12.6.
+
+NOTE:
+To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping). DevOps Score is a comparative tool, so your score data must be centrally processed by GitLab Inc. first.
+
+You can use the DevOps score to compare your DevOps status to other organizations.
+
+The DevOps Score tab displays usage of major GitLab features on your instance over
+the last 30 days, averaged over the number of billable users in that time period.
+You can also see the Leader usage score, calculated from top-performing instances based on
+[Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has collected.
+Your score is compared to the lead score of each feature and then expressed
+as a percentage at the bottom of said feature. Your overall **DevOps Score** is an average of your
+feature scores.
+
+Service Ping data is aggregated on GitLab servers for analysis. Your usage
+information is **not sent** to any other GitLab instances.
+If you have just started using GitLab, it might take a few weeks for data to be collected before this
+feature is available.
+
+## DevOps Adoption **(ULTIMATE SELF)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247112) in GitLab 13.7 as a [Beta feature](../../../policy/alpha-beta-support.md#beta-features).
+> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
+> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
+> - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2.
+> - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2.
+> - Multi-select [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2.
+> - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3.
+
+DevOps Adoption shows feature adoption for development, security, and operations.
+
+| Category | Feature |
+| --- | --- |
+| Development | Approvals<br>Code owners<br>Issues<br>Merge requests |
+| Security | DAST<br>Dependency Scanning<br>Fuzz Testing<br>SAST |
+| Operations | Deployments<br>Pipelines<br>Runners |
+
+You can use Group DevOps Adoption to:
+
+- Identify specific subgroups that are lagging in their adoption of GitLab features, so you can guide them on
+their DevOps journey.
+- Find subgroups that have adopted certain features, and provide guidance to other subgroups on
+how to use those features.
+- Verify if you are getting the return on investment that you expected from GitLab.
+
+## Add or remove a group
+
+To add or remove a subgroup from the DevOps Adoption report:
+
+1. Select **Add or remove groups**.
+1. Select the subgroup you want to add or remove and select **Save changes**.
+
+![DevOps Adoption](img/admin_devops_adoption_v14_2.png)
diff --git a/doc/user/admin_area/analytics/index.md b/doc/user/admin_area/analytics/index.md
index cd505e154c6..9315b926acc 100644
--- a/doc/user/admin_area/analytics/index.md
+++ b/doc/user/admin_area/analytics/index.md
@@ -15,5 +15,5 @@ Administrators have access to instance-wide analytics:
There are several kinds of statistics:
-- [DevOps Reports](dev_ops_report.md): Provides an overview of your entire instance's feature usage.
+- [DevOps Reports](dev_ops_reports.md): Provides an overview of your entire instance's feature usage.
- [Usage Trends](usage_trends.md): Shows how much data your instance contains, and how that is changing.
diff --git a/doc/user/clusters/agent/troubleshooting.md b/doc/user/clusters/agent/troubleshooting.md
index a5e568837ad..c5c7e46c078 100644
--- a/doc/user/clusters/agent/troubleshooting.md
+++ b/doc/user/clusters/agent/troubleshooting.md
@@ -27,9 +27,8 @@ If you are a GitLab administrator, you can also view the [GitLab agent server lo
}
```
-This error is shown if there are some connectivity issues between the address
-specified as `kas-address`, and your agent pod. To fix it, make sure that you
-specified the `kas-address` correctly.
+This error occurs when there are connectivity issues between the `kas-address`
+and your agent pod. To fix this issue, make sure the `kas-address` is accurate.
```json
{
@@ -41,8 +40,8 @@ specified the `kas-address` correctly.
}
```
-This error occurs if the `kas-address` doesn't include a trailing slash. To fix it, make sure that the
-`wss` or `ws` URL ends with a trailing slash, such as `wss://GitLab.host.tld:443/-/kubernetes-agent/`
+This error occurs when the `kas-address` doesn't include a trailing slash. To fix this issue, make sure that the
+`wss` or `ws` URL ends with a trailing slash, like `wss://GitLab.host.tld:443/-/kubernetes-agent/`
or `ws://GitLab.host.tld:80/-/kubernetes-agent/`.
## ValidationError(Deployment.metadata)
@@ -58,9 +57,10 @@ or `ws://GitLab.host.tld:80/-/kubernetes-agent/`.
}
```
-This error is shown if a manifest file is malformed, and Kubernetes can't
-create specified objects. Make sure that your manifest files are valid. You
-may try using them to create objects in Kubernetes directly for more troubleshooting.
+This error occurs when a manifest file is malformed and Kubernetes can't
+create the specified objects. Make sure that your manifest files are valid.
+
+For additional troubleshooting, try to use the manifest files to create objects in Kubernetes directly.
## Error while dialing failed to WebSocket dial: failed to send handshake request
@@ -73,16 +73,10 @@ may try using them to create objects in Kubernetes directly for more troubleshoo
}
```
-This error is shown if you configured `wss` as `kas-address` on the agent side,
-but KAS on the server side is not available via `wss`. To fix it, make sure the
+This error occurs when you configured `wss` as `kas-address` on the agent side,
+but the agent server is not available at `wss`. To fix this issue, make sure the
same schemes are configured on both sides.
-It's not possible to set the `grpc` scheme due to the issue
-[It is not possible to configure KAS to work with `grpc` without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
-issue is in progress, directly edit the deployment with the
-`kubectl edit deployment gitlab-kas` command, and change `--listen-websocket=true` to `--listen-websocket=false`. After running that command, you should be able to use
-`grpc://gitlab-kas.<YOUR-NAMESPACE>:8150`.
-
## Decompressor is not installed for grpc-encoding
```json
@@ -94,8 +88,8 @@ issue is in progress, directly edit the deployment with the
}
```
-This error is shown if the version of the agent is newer that the version of KAS.
-To fix it, make sure that both `agentk` and KAS use the same versions.
+This error occurs when the version of the agent is newer that the version of the agent server (KAS).
+To fix it, make sure that both `agentk` and the agent server are the same version.
## Certificate signed by unknown authority
@@ -109,9 +103,11 @@ To fix it, make sure that both `agentk` and KAS use the same versions.
}
```
-This error is shown if your GitLab instance is using a certificate signed by an internal CA that
-is unknown to the agent. One approach to fixing it is to present the CA certificate file to the agent
-via a Kubernetes `configmap` and mount the file in the agent `/etc/ssl/certs` directory from where it
+This error occurs when your GitLab instance is using a certificate signed by an internal
+certificate authority that is unknown to the agent.
+
+To fix this issue, you can present the CA certificate file to the agent
+by using a Kubernetes `configmap` and mount the file in the agent `/etc/ssl/certs` directory from where it
will be picked up automatically.
For example, if your internal CA certificate is `myCA.pem`:
@@ -153,7 +149,7 @@ Then in `resources.yml`:
path: myCA.pem
```
-Alternatively, you can mount the certificate file at a different location and include it using the
+Alternatively, you can mount the certificate file at a different location and specify it for the
`--ca-cert-file` agent parameter:
```yaml
@@ -188,5 +184,5 @@ Alternatively, you can mount the certificate file at a different location and in
}
```
-This error is shown if the manifest project is not public. To fix it, make sure your manifest project is public or your manifest files
-are stored in the agent's configuration repository.
+This error occurs when the project where you keep your manifests is not public. To fix it, make sure your project is public or your manifest files
+are stored in the repository where the agent is configured.
diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md
index 1fb628cf505..d5c0a9e4155 100644
--- a/doc/user/crm/index.md
+++ b/doc/user/crm/index.md
@@ -103,7 +103,15 @@ organizations using the GraphQL API.
### Edit an organization
-You can only [edit](../../api/graphql/reference/index.md#mutationcustomerrelationsorganizationupdate)
+To edit an existing organization:
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Customer relations > Organizations**.
+1. Next to the organization you wish to edit, select **Edit** (**{pencil}**).
+1. Edit the required fields.
+1. Select **Save changes**.
+
+You can also [edit](../../api/graphql/reference/index.md#mutationcustomerrelationsorganizationupdate)
organizations using the GraphQL API.
## Issues
@@ -171,3 +179,23 @@ When you use the `/add_contacts` or `/remove_contacts` quick actions, follow the
/add_contacts [contact:
/remove_contacts [contact:
```
+
+## Moving objects with CRM entries
+
+The root group is the topmost group in the group hierarchy.
+
+When you move an issue, project, or group **within the same group hierarchy**,
+issues retain their contacts.
+
+When you move an issue or project and the **root group changes**,
+issues lose their contacts.
+
+When you move a group and its **root group changes**:
+
+- All unique contacts and organizations are migrated to the new root group.
+- Contacts that already exist (by email address) are deemed duplicates and deleted.
+- Organizations that already exist (by name) are deemed duplicates and deleted.
+- All issues retain their contacts or are updated to point at contacts with the same email address.
+
+If you do not have permission to create contacts and organizations in the new
+root group, the group transfer fails.
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 6e3af6a92d5..6a515b78fc1 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -283,7 +283,8 @@ To authenticate to the Package Registry, you need either a personal access token
### Authenticate with a personal access token in Gradle
-Create a file `~/.gradle/gradle.properties` with the following content:
+In [your `GRADLE_USER_HOME` directory](https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home),
+create a file `gradle.properties` with the following content:
```groovy
gitLabPrivateToken=REPLACE_WITH_YOUR_PERSONAL_ACCESS_TOKEN
@@ -586,7 +587,7 @@ To publish a package by using Gradle:
url "https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/packages/maven"
credentials(HttpHeaderCredentials) {
name = "Private-Token"
- value = gitLabPrivateToken // the variable resides in ~/.gradle/gradle.properties
+ value = gitLabPrivateToken // the variable resides in $GRADLE_USER_HOME/gradle.properties
}
authentication {
header(HttpHeaderAuthentication)
@@ -820,7 +821,7 @@ rm -rf ~/.m2/repository
If you're using Gradle, run this command to clear the cache:
```shell
-rm -rf ~/.gradle/caches
+rm -rf ~/.gradle/caches # Or replace ~/.gradle with your custom GRADLE_USER_HOME
```
### Review network trace logs
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index a80e45637dc..14792730eae 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -38,7 +38,7 @@ module API
params do
use :pagination
end
- get ":id/snippets" do
+ get ":id/snippets", urgency: :low do
authenticate!
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet, current_user: current_user
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 9a3c68bc854..aa122c13e0c 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -184,7 +184,7 @@ module API
params do
use :raw_file_params
end
- get ":id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
+ get ":id/files/:ref/:file_path/raw", urgency: :low, requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
snippet = snippets.find_by_id(params.delete(:id))
not_found!('Snippet') unless snippet&.repo_exists?
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index e90d88940a5..68175116aee 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -48,7 +48,7 @@ module API
optional :version, type: String, desc: 'The version hash of a wiki page'
optional :render_html, type: Boolean, default: false, desc: 'Render content to HTML'
end
- get ':id/wikis/:slug' do
+ get ':id/wikis/:slug', urgency: :low do
authorize! :read_wiki, container
present wiki_page(params[:version]), with: Entities::WikiPage, render_html: params[:render_html]
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 967fc8be4c6..2fa0f565cb2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6321,6 +6321,9 @@ msgstr ""
msgid "BulkImport|%{feature} (require v%{version})"
msgstr ""
+msgid "BulkImport|Destination"
+msgstr ""
+
msgid "BulkImport|Existing groups"
msgstr ""
@@ -6363,12 +6366,18 @@ msgstr ""
msgid "BulkImport|New group"
msgstr ""
+msgid "BulkImport|No additional information provided."
+msgstr ""
+
msgid "BulkImport|No history is available"
msgstr ""
msgid "BulkImport|No parent"
msgstr ""
+msgid "BulkImport|Project import history"
+msgstr ""
+
msgid "BulkImport|Re-import creates a new group. It does not sync with the existing group."
msgstr ""
@@ -6381,9 +6390,15 @@ msgstr ""
msgid "BulkImport|Showing %{start}-%{end} of %{total} matching filter \"%{filter}\" from %{link}"
msgstr ""
+msgid "BulkImport|Source"
+msgstr ""
+
msgid "BulkImport|Source group"
msgstr ""
+msgid "BulkImport|Template / File-based import / GitLab Migration"
+msgstr ""
+
msgid "BulkImport|To new group"
msgstr ""
@@ -6396,6 +6411,9 @@ msgstr ""
msgid "BulkImport|Your imported groups will appear here."
msgstr ""
+msgid "BulkImport|Your imported projects will appear here."
+msgstr ""
+
msgid "BulkImport|expected an associated Group but has an associated Project"
msgstr ""
@@ -16128,9 +16146,6 @@ msgstr ""
msgid "Framework successfully deleted"
msgstr ""
-msgid "Free"
-msgstr ""
-
msgid "Free Trial of GitLab.com Ultimate"
msgstr ""
@@ -21372,6 +21387,9 @@ msgstr ""
msgid "Jobs|Create CI/CD configuration file"
msgstr ""
+msgid "Jobs|Filter jobs"
+msgstr ""
+
msgid "Jobs|Job is stuck. Check runners."
msgstr ""
@@ -21381,6 +21399,12 @@ msgstr ""
msgid "Jobs|No jobs to show"
msgstr ""
+msgid "Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens."
+msgstr ""
+
+msgid "Jobs|Status"
+msgstr ""
+
msgid "Jobs|Use jobs to automate your tasks"
msgstr ""
@@ -21408,15 +21432,24 @@ msgstr ""
msgid "Job|Cancel"
msgstr ""
+msgid "Job|Canceled"
+msgstr ""
+
msgid "Job|Complete Raw"
msgstr ""
+msgid "Job|Created"
+msgstr ""
+
msgid "Job|Download"
msgstr ""
msgid "Job|Erase job log and artifacts"
msgstr ""
+msgid "Job|Failed"
+msgstr ""
+
msgid "Job|Finished at"
msgstr ""
@@ -21432,9 +21465,27 @@ msgstr ""
msgid "Job|Keep"
msgstr ""
+msgid "Job|Manual"
+msgstr ""
+
+msgid "Job|Passed"
+msgstr ""
+
+msgid "Job|Pending"
+msgstr ""
+
+msgid "Job|Preparing"
+msgstr ""
+
msgid "Job|Retry"
msgstr ""
+msgid "Job|Running"
+msgstr ""
+
+msgid "Job|Scheduled"
+msgstr ""
+
msgid "Job|Scroll to bottom"
msgstr ""
@@ -21444,6 +21495,9 @@ msgstr ""
msgid "Job|Show complete raw"
msgstr ""
+msgid "Job|Skipped"
+msgstr ""
+
msgid "Job|Status"
msgstr ""
@@ -21468,6 +21522,9 @@ msgstr ""
msgid "Job|This job is stuck because you don't have any active runners that can run this job."
msgstr ""
+msgid "Job|Waiting for resource"
+msgstr ""
+
msgid "Job|allowed to fail"
msgstr ""
@@ -32910,6 +32967,9 @@ msgstr ""
msgid "SecurityConfiguration|Immediately begin risk analysis and remediation with application security features. Start with SAST and Secret Detection, available to all plans. Upgrade to Ultimate to get all features, including:"
msgstr ""
+msgid "SecurityConfiguration|Learn more about vulnerability training"
+msgstr ""
+
msgid "SecurityConfiguration|Manage corpus"
msgstr ""
@@ -43785,6 +43845,9 @@ msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
+msgid "ciReport|Manually Added"
+msgstr ""
+
msgid "ciReport|New"
msgstr ""
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 340e40127c9..26fff85dd99 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -13,6 +13,7 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
+ element :initialize_with_sast_checkbox
element :project_name
element :project_path
element :project_description
@@ -20,10 +21,6 @@ module QA
element :visibility_radios
end
- view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do
- element :initialize_with_sast_checkbox
- end
-
view 'app/views/projects/project_templates/_template.html.haml' do
element :use_template_button
element :template_option_row
diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb
index caf54a10025..1a8e1e07994 100644
--- a/qa/qa/page/project/pipeline_editor/show.rb
+++ b/qa/qa/page/project/pipeline_editor/show.rb
@@ -15,9 +15,9 @@ module QA
element :target_branch_field, required: true
end
- view 'app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue' do
- element :toggle_sidebar_collapse_button
- element :drawer_content
+ view 'app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue' do
+ element :drawer_toggle, required: true
+ element :template_repo_link, required: true
end
view 'app/assets/javascripts/vue_shared/components/source_editor.vue' do
@@ -46,13 +46,6 @@ module QA
element :file_editor_container
end
- def initialize
- super
-
- wait_for_requests
- close_toggle_sidebar
- end
-
def open_branch_selector_dropdown
click_element(:branch_selector_button)
end
@@ -148,15 +141,6 @@ module QA
find('.nav-item', text: name).click
end
end
-
- # If the page thinks user has never opened pipeline editor before
- # It will expand pipeline editor sidebar by default
- # Collapse the sidebar if it is expanded
- def close_toggle_sidebar
- return unless has_element?(:drawer_content)
-
- click_element(:toggle_sidebar_collapse_button)
- end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c098ea71f7a..d0aff6e282a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -473,28 +473,6 @@ RSpec.describe ProjectsController do
end
end
end
-
- context 'with new_project_sast_enabled', :experiment do
- let(:params) do
- {
- path: 'foo',
- description: 'bar',
- namespace_id: user.namespace.id,
- initialize_with_sast: '1'
- }
- end
-
- it 'tracks an event on project creation' do
- expect(experiment(:new_project_sast_enabled)).to track(:created,
- property: 'blank',
- checked: true,
- project: an_instance_of(Project),
- namespace: user.namespace
- ).on_next_instance.with_context(user: user)
-
- post :create, params: { project: params }
- end
- end
end
describe 'GET edit' do
diff --git a/spec/experiments/new_project_sast_enabled_experiment_spec.rb b/spec/experiments/new_project_sast_enabled_experiment_spec.rb
deleted file mode 100644
index 041e5dfa469..00000000000
--- a/spec/experiments/new_project_sast_enabled_experiment_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe NewProjectSastEnabledExperiment do
- it "defines the expected behaviors and variants" do
- expect(subject.variant_names).to match_array([
- :candidate,
- :free_indicator,
- :unchecked_candidate,
- :unchecked_free_indicator
- ])
- end
-
- it "publishes to the database" do
- expect(subject).to receive(:publish_to_database)
-
- subject.publish
- end
-end
diff --git a/spec/features/admin/admin_dev_ops_report_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index cee79f8f440..bf32819cb52 100644
--- a/spec/features/admin/admin_dev_ops_report_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'DevOps Report page', :js do
end
it 'has dismissable intro callout' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_content 'Introducing Your DevOps Report'
@@ -32,13 +32,13 @@ RSpec.describe 'DevOps Report page', :js do
end
it 'shows empty state' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_text('Service ping is off')
end
it 'hides the intro callout' do
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).not_to have_content 'Introducing Your DevOps Report'
end
@@ -48,7 +48,7 @@ RSpec.describe 'DevOps Report page', :js do
it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true)
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_content('Data is still calculating')
end
@@ -59,7 +59,7 @@ RSpec.describe 'DevOps Report page', :js do
stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_report_metric)
- visit admin_dev_ops_report_path
+ visit admin_dev_ops_reports_path
expect(page).to have_selector('[data-testid="devops-score-app"]')
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 6491a7425f7..84977b6c962 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -33,29 +33,6 @@ RSpec.describe 'User creates a project', :js do
end
it 'creates a new project that is not blank' do
- stub_experiments(new_project_sast_enabled: 'candidate')
-
- visit(new_project_path)
-
- click_link 'Create blank project'
- fill_in(:project_name, with: 'With initial commits')
-
- expect(page).to have_checked_field 'Initialize repository with a README'
- expect(page).to have_checked_field 'Enable Static Application Security Testing (SAST)'
-
- click_button('Create project')
-
- project = Project.last
-
- expect(page).to have_current_path(project_path(project), ignore_query: true)
- expect(page).to have_content('With initial commits')
- expect(page).to have_content('Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist')
- expect(page).to have_content('README.md Initial commit')
- end
-
- it 'allows creating a new project when the new_project_sast_enabled is assigned the unchecked candidate' do
- stub_experiments(new_project_sast_enabled: 'unchecked_candidate')
-
visit(new_project_path)
click_link 'Create blank project'
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
new file mode 100644
index 00000000000..322cfa3ba1f
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -0,0 +1,49 @@
+import { GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import { mockFailedSearchToken } from '../../mock_data';
+
+describe('Jobs filtered search', () => {
+ let wrapper;
+
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const getSearchToken = (type) =>
+ findFilteredSearch()
+ .props('availableTokens')
+ .find((token) => token.type === type);
+
+ const findStatusToken = () => getSearchToken('status');
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobsFilteredSearch);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays filtered search', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('displays status token', () => {
+ expect(findStatusToken()).toMatchObject({
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ });
+ });
+
+ it('emits filter token to parent component', () => {
+ findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
+
+ expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
+ });
+});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
new file mode 100644
index 00000000000..ce8e482cc16
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -0,0 +1,57 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+
+describe('Job Status Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
+
+ const defaultProps = {
+ config: {
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobStatusToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ it('renders all job statuses available', () => {
+ const expectedLength = 11;
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(expectedLength);
+ expect(findAllGlIcons()).toHaveLength(expectedLength);
+ });
+});
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 4d51624dfff..98d8419b26e 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,30 +1,48 @@
-import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
+import {
+ GlSkeletonLoader,
+ GlAlert,
+ GlEmptyState,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import {
+ mockJobsQueryResponse,
+ mockJobsQueryEmptyResponse,
+ mockFailedSearchToken,
+} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
+jest.mock('~/flash');
+
describe('Job table app', () => {
let wrapper;
+ let jobsTableVueSearch = true;
const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
@@ -48,6 +66,7 @@ describe('Job table app', () => {
},
provide: {
fullPath: projectPath,
+ glFeatures: { jobsTableVueSearch },
},
apolloProvider: createMockApolloProvider(handler),
});
@@ -58,11 +77,21 @@ describe('Job table app', () => {
});
describe('loading state', () => {
- it('should display skeleton loader when loading', () => {
+ beforeEach(() => {
createComponent();
+ });
+ it('should display skeleton loader when loading', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+
+ it('when switching tabs only the skeleton loader should show', () => {
+ findTabs().vm.$emit('fetchJobsByStatus', 'PENDING');
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
});
@@ -76,6 +105,7 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
it('should refetch jobs query on fetchJobsByStatus event', async () => {
@@ -98,8 +128,12 @@ describe('Job table app', () => {
});
it('handles infinite scrolling by calling fetch more', async () => {
+ expect(findLoadingSpinner().exists()).toBe(true);
+
await waitForPromises();
+ expect(findLoadingSpinner().exists()).toBe(false);
+
expect(successHandler).toHaveBeenCalledWith({
after: 'eyJpZCI6IjIzMTcifQ',
fullPath: 'gitlab-org/gitlab',
@@ -137,4 +171,69 @@ describe('Job table app', () => {
expect(findTable().exists()).toBe(true);
});
});
+
+ describe('filtered search', () => {
+ it('should display filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ // this test should be updated once BE supports tab and filtered search filtering
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/356210
+ it.each`
+ scope | shouldDisplay
+ ${null} | ${true}
+ ${'PENDING'} | ${false}
+ ${'RUNNING'} | ${false}
+ ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false}
+ `(
+ 'with tab scope $scope the filtered search displays $shouldDisplay',
+ async ({ scope, shouldDisplay }) => {
+ createComponent();
+
+ await findTabs().vm.$emit('fetchJobsByStatus', scope);
+
+ expect(findFilteredSearch().exists()).toBe(shouldDisplay);
+ },
+ );
+
+ it('refetches jobs query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: s__(
+ 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.',
+ ),
+ type: 'warning',
+ };
+
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createFlash).toHaveBeenCalledWith(expectedWarning);
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not display filtered search', () => {
+ jobsTableVueSearch = false;
+
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 73b9df1853d..b4cc58a04cc 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1918,3 +1918,5 @@ export const CIJobConnectionExistingCache = {
],
statuses: 'PENDING',
};
+
+export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
new file mode 100644
index 00000000000..4ff3f0361cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -0,0 +1,66 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+
+describe('ImportErrorDetails', () => {
+ const FAKE_ID = 5;
+ const API_URL = `/api/v4/projects/${FAKE_ID}`;
+
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportErrorDetails, {
+ propsData: {
+ id: FAKE_ID,
+ },
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders import_error if it is available', async () => {
+ const FAKE_IMPORT_ERROR = 'IMPORT ERROR';
+ mock.onGet(API_URL).reply(200, { import_error: FAKE_IMPORT_ERROR });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR);
+ });
+
+ it('renders default text if error is not available', async () => {
+ mock.onGet(API_URL).reply(200, { import_error: null });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe('No additional information provided.');
+ });
+ });
+});
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
new file mode 100644
index 00000000000..0d821b114cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -0,0 +1,205 @@
+import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+import ImportHistoryApp from '~/pages/import/history/components/import_history_app.vue';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+
+describe('ImportHistoryApp', () => {
+ const API_URL = '/api/v4/projects.json';
+
+ const DEFAULT_HEADERS = {
+ 'x-page': 1,
+ 'x-per-page': 20,
+ 'x-next-page': 2,
+ 'x-total': 22,
+ 'x-total-pages': 2,
+ 'x-prev-page': null,
+ };
+ const DUMMY_RESPONSE = [
+ {
+ id: 1,
+ path_with_namespace: 'root/imported',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ {
+ id: 2,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'https://dummy.github/url',
+ import_type: 'github',
+ import_status: 'failed',
+ },
+ {
+ id: 3,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy2',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'git://non-http.url',
+ import_type: 'gi',
+ import_status: 'finished',
+ },
+ ];
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportHistoryApp, {
+ provide: { assets: { gitlabLogo: 'http://dummy.host' } },
+ stubs: shallow ? { GlTable: { ...stubComponent(GlTable), props: ['items'] } } : {},
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders empty state when no data is available', async () => {
+ mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ });
+
+ it('renders table with data when history is available', async () => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ const table = wrapper.find(GlTable);
+ expect(table.exists()).toBe(true);
+ expect(table.props().items).toStrictEqual(DUMMY_RESPONSE);
+ });
+
+ it('changes page when requested by pagination bar', async () => {
+ const NEW_PAGE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ const FAKE_NEXT_PAGE_REPLY = [
+ {
+ id: 4,
+ path_with_namespace: 'root/some_other_project',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ ];
+
+ mock.onGet(API_URL).reply(200, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
+ expect(wrapper.find(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY);
+ });
+ });
+
+ it('changes page size when requested by pagination bar', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE }),
+ );
+ });
+
+ it('resets page to 1 when page size is changed', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE, page: 1 }),
+ );
+ });
+
+ describe('details button', () => {
+ beforeEach(() => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent({ shallow: false });
+ return axios.waitForAll();
+ });
+
+ it('renders details button if relevant item has failed', async () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
+ ).toBe(true);
+ });
+
+ it('does not render details button if relevant item does not failed', () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(0)).findByText('Details').exists(),
+ ).toBe(false);
+ });
+
+ it('expands details when details button is clicked', async () => {
+ const ORIGINAL_ROW_INDEX = 1;
+ await extendedWrapper(wrapper.find('tbody').findAll('tr').at(ORIGINAL_ROW_INDEX))
+ .findByText('Details')
+ .trigger('click');
+
+ const detailsRowContent = wrapper
+ .find('tbody')
+ .findAll('tr')
+ .at(ORIGINAL_ROW_INDEX + 1)
+ .findComponent(ImportErrorDetails);
+
+ expect(detailsRowContent.exists()).toBe(true);
+ expect(detailsRowContent.props().id).toBe(DUMMY_RESPONSE[1].id);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index ba06f113120..33b53bf6a56 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,146 +1,27 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
-import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
-import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue';
+import { GlDrawer } from '@gitlab/ui';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
-import { DRAWER_EXPANDED_KEY } from '~/pipeline_editor/constants';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Pipeline editor drawer', () => {
- useLocalStorageSpy();
-
let wrapper;
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
const createComponent = () => {
- wrapper = shallowMount(PipelineEditorDrawer, {
- stubs: { LocalStorageSync },
- });
+ wrapper = shallowMount(PipelineEditorDrawer);
};
- const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard);
- const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard);
- const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard);
- const findToggleBtn = () => wrapper.findComponent(GlButton);
- const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard);
-
- const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]');
- const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
- const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
-
- const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
-
- const originalObjects = [];
-
- beforeEach(() => {
- originalObjects.push(window.gon, window.gl);
- });
-
afterEach(() => {
wrapper.destroy();
- localStorage.clear();
- [window.gon, window.gl] = originalObjects;
- });
-
- describe('default expanded state', () => {
- it('sets the drawer to be closed by default', async () => {
- createComponent();
- expect(findDrawerContent().exists()).toBe(false);
- });
- });
-
- describe('when the drawer is collapsed', () => {
- beforeEach(async () => {
- createComponent();
- });
-
- it('shows the left facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
- });
-
- it('does not show the collapse text', () => {
- expect(findCollapseText().exists()).toBe(false);
- });
-
- it('does not show the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(false);
- });
-
- it('can open the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(false);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(true);
- });
- });
-
- describe('when the drawer is expanded', () => {
- beforeEach(async () => {
- createComponent();
- await clickToggleBtn();
- });
-
- it('shows the right facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right');
- });
-
- it('shows the collapse text', () => {
- expect(findCollapseText().exists()).toBe(true);
- });
-
- it('shows the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(true);
- });
-
- it('shows all the introduction cards', () => {
- expect(findFirstPipelineCard().exists()).toBe(true);
- expect(findGettingStartedCard().exists()).toBe(true);
- expect(findPipelineConfigReferenceCard().exists()).toBe(true);
- expect(findVisualizeAndLintCard().exists()).toBe(true);
- });
-
- it('can close the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(true);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
});
- describe('local storage', () => {
- it('saves the drawer expanded value to local storage', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, 'false');
-
- createComponent();
- await clickToggleBtn();
-
- expect(localStorage.setItem.mock.calls).toEqual([
- [DRAWER_EXPANDED_KEY, 'false'],
- [DRAWER_EXPANDED_KEY, 'true'],
- ]);
- });
-
- it('loads the drawer collapsed when local storage is set to `false`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, false);
- createComponent();
-
- await nextTick();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
+ it('emits close event when closing the drawer', () => {
+ createComponent();
- it('loads the drawer expanded when local storage is set to `true`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, true);
- createComponent();
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
- await nextTick();
+ findDrawer().vm.$emit('close');
- expect(findDrawerContent().exists()).toBe(true);
- });
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
});
});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
index 3ee53d4a055..8f50325295e 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import {
@@ -11,11 +11,18 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = () => {
- wrapper = shallowMount(CiEditorHeader, {});
+ const createComponent = ({ showDrawer = false } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CiEditorHeader, {
+ propsData: {
+ showDrawer,
+ },
+ }),
+ );
};
- const findLinkBtn = () => wrapper.findComponent(GlButton);
+ const findLinkBtn = () => wrapper.findByTestId('template-repo-link');
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -50,4 +57,42 @@ describe('CI Editor Header', () => {
});
});
});
+
+ describe('help button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('finds the help button', () => {
+ expect(findHelpBtn().exists()).toBe(true);
+ });
+
+ it('has the information-o icon', () => {
+ expect(findHelpBtn().props('icon')).toBe('information-o');
+ });
+
+ describe('when pipeline editor drawer is closed', () => {
+ it('emits open drawer event when clicked', () => {
+ createComponent({ showDrawer: false });
+
+ expect(wrapper.emitted('open-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('open-drawer')).toHaveLength(1);
+ });
+ });
+
+ describe('when pipeline editor drawer is open', () => {
+ it('emits close drawer event when clicked', () => {
+ createComponent({ showDrawer: true });
+
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index fee52db9b64..6dffb7e5470 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -40,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isNewCiConfigFile: true,
+ showDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 6f969546171..98e2c17967c 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlModal } from '@gitlab/ui';
+import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
@@ -18,24 +20,26 @@ describe('Pipeline editor home wrapper', () => {
let wrapper;
const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
- wrapper = shallowMount(PipelineEditorHome, {
- data: () => data,
- propsData: {
- ciConfigData: mockLintResponse,
- ciFileContent: mockCiYml,
- isCiConfigDataLoading: false,
- isNewCiConfigFile: false,
- ...props,
- },
- provide: {
- projectFullPath: '',
- totalBranches: 19,
- glFeatures: {
- ...glFeatures,
+ wrapper = extendedWrapper(
+ shallowMount(PipelineEditorHome, {
+ data: () => data,
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ isNewCiConfigFile: false,
+ ...props,
},
- },
- stubs,
- });
+ provide: {
+ projectFullPath: '',
+ totalBranches: 19,
+ glFeatures: {
+ ...glFeatures,
+ },
+ },
+ stubs,
+ }),
+ );
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
@@ -45,6 +49,7 @@ describe('Pipeline editor home wrapper', () => {
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -70,10 +75,6 @@ describe('Pipeline editor home wrapper', () => {
it('shows the commit section by default', () => {
expect(findCommitSection().exists()).toBe(true);
});
-
- it('show the pipeline drawer', () => {
- expect(findPipelineEditorDrawer().exists()).toBe(true);
- });
});
describe('modal when switching branch', () => {
@@ -175,4 +176,58 @@ describe('Pipeline editor home wrapper', () => {
});
});
});
+
+ describe('help drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ it('hides the drawer by default', () => {
+ createComponent();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the drawer on button click', async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ findPipelineEditorDrawer().find(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index cdaec0a3a8b..313870e8ea1 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -235,9 +235,11 @@ describe('AdminRunnersApp', () => {
const mockRunner = runnersData.data.runners.nodes[0];
const { id: graphqlId, shortSha } = mockRunner;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockRunnersQuery.mockClear();
+ mockRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -252,12 +254,18 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
- expect(mockRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
- expect(mockRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
+
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 9ca99d1109b..7a949cb6505 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -100,6 +100,16 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('runner')).toEqual(mockRunner);
});
+ it('Emits toggledPaused events', () => {
+ createComponent();
+
+ expect(wrapper.emitted('toggledPaused')).toBe(undefined);
+
+ findRunnerPauseBtn().vm.$emit('toggledPaused');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+
it('Emits delete events', () => {
const value = { name: 'Runner' };
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
index 3d9df03977e..9ebb30b6ed7 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => {
it('The button does not have a loading state', () => {
expect(findBtn().props('loading')).toBe(false);
});
+
+ it('The button emits toggledPaused', () => {
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
});
describe('When update fails', () => {
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 70e303e8626..6d7ecc4506a 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -193,9 +193,11 @@ describe('GroupRunnersApp', () => {
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockGroupRunnersQuery.mockClear();
+ mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -219,12 +221,20 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is deleted, data is refetched and a toast is shown', async () => {
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
+
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
+ COUNT_QUERIES + FILTERED_COUNT_QUERIES,
+ );
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 963577fa763..5787747b00b 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlTab, GlTabs } from '@gitlab/ui';
+import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -107,6 +107,7 @@ describe('App component', () => {
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
+ const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
const securityFeaturesMock = [
{
@@ -454,9 +455,16 @@ describe('App component', () => {
});
it('renders security training description', () => {
- const vulnerabilityManagementTab = wrapper.findByTestId('vulnerability-management-tab');
+ expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
+ });
+
+ it('renders link to help docs', () => {
+ const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
- expect(vulnerabilityManagementTab.text()).toContain(i18n.securityTrainingDescription);
+ expect(trainingLink.text()).toBe('Learn more about vulnerability training');
+ expect(trainingLink.attributes('href')).toBe(
+ '/help/user/application_security/vulnerabilities/index#enable-security-training-for-vulnerabilities',
+ );
});
});
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 8c36d7d4668..f48b4de23a2 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -134,10 +134,17 @@ RSpec.describe Admin::HealthCheckController, "routing" do
end
end
-# admin_dev_ops_report GET /admin/dev_ops_report(.:format) admin/dev_ops_report#show
+# admin_dev_ops_reports GET /admin/dev_ops_reports(.:format) admin/dev_ops_report#show
RSpec.describe Admin::DevOpsReportController, "routing" do
it "to #show" do
- expect(get("/admin/dev_ops_report")).to route_to('admin/dev_ops_report#show')
+ expect(get("/admin/dev_ops_reports")).to route_to('admin/dev_ops_report#show')
+ end
+
+ describe 'admin devops reports' do
+ include RSpec::Rails::RequestExampleGroup
+ it 'redirects from /admin/dev_ops_report to /admin/dev_ops_reports' do
+ expect(get("/admin/dev_ops_report")).to redirect_to(admin_dev_ops_reports_path)
+ end
end
end