diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-22 21:08:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-22 21:08:29 +0300 |
commit | bd1bebd1b0a0d9a0484849c73ee15449dd27fe92 (patch) | |
tree | fbfd47b23d0cea97440129d59436c3845a2187dd | |
parent | e01b61d83fd7c5d3aa9d87a65eac85e8c7ea9921 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |