diff options
Diffstat (limited to 'app')
40 files changed, 788 insertions, 290 deletions
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 } |