diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/pipelines | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/pipelines')
27 files changed, 1170 insertions, 484 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 93156d5d05b..363226a0d85 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -4,7 +4,7 @@ import LinksLayer from '../graph_shared/links_layer.vue'; import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; -import { reportToSentry } from './utils'; +import { reportToSentry, validateConfigPaths } from './utils'; export default { name: 'PipelineGraph', @@ -15,15 +15,20 @@ export default { StageColumnComponent, }, props: { - isLinkedPipeline: { - type: Boolean, - required: false, - default: false, + configPaths: { + type: Object, + required: true, + validator: validateConfigPaths, }, pipeline: { type: Object, required: true, }, + isLinkedPipeline: { + type: Boolean, + required: false, + default: false, + }, type: { type: String, required: false, @@ -66,6 +71,12 @@ export default { hasUpstreamPipelines() { return Boolean(this.pipeline?.upstream?.length > 0); }, + metricsConfig() { + return { + path: this.configPaths.metricsPath, + collectMetrics: true, + }; + }, // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( @@ -95,8 +106,8 @@ export default { height: this.$refs[this.containerId].scrollHeight, }; }, - onError(errorType) { - this.$emit('error', errorType); + onError(payload) { + this.$emit('error', payload); }, setJob(jobName) { this.hoveredJobName = jobName; @@ -131,6 +142,7 @@ export default { <template #upstream> <linked-pipelines-column v-if="showUpstreamPipelines" + :config-paths="configPaths" :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :type="$options.pipelineTypeConstants.UPSTREAM" @@ -145,6 +157,8 @@ export default { :container-id="containerId" :container-measurements="measurements" :highlighted-job="hoveredJobName" + :metrics-config="metricsConfig" + :never-show-links="true" default-link-color="gl-stroke-transparent" @error="onError" @highlightedJobsChange="updateHighlightedJobs" @@ -170,6 +184,7 @@ export default { <linked-pipelines-column v-if="showDownstreamPipelines" class="gl-mr-6" + :config-paths="configPaths" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" :type="$options.pipelineTypeConstants.DOWNSTREAM" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index f596333237d..962f2ca2a4c 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -2,9 +2,15 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { __ } from '~/locale'; -import { DEFAULT, LOAD_FAILURE } from '../../constants'; +import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import PipelineGraph from './graph_component.vue'; -import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; +import { + getQueryHeaders, + reportToSentry, + serializeLoadErrors, + toggleQueryPollingByVisibility, + unwrapPipelineData, +} from './utils'; export default { name: 'PipelineGraphWrapper', @@ -14,6 +20,12 @@ export default { PipelineGraph, }, inject: { + graphqlResourceEtag: { + default: '', + }, + metricsPath: { + default: '', + }, pipelineIid: { default: '', }, @@ -29,11 +41,15 @@ export default { }; }, errorTexts: { + [DRAW_FAILURE]: __('An error occurred while drawing job relationship links.'), [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'), [DEFAULT]: __('An unknown error occurred while loading this graph.'), }, apollo: { pipeline: { + context() { + return getQueryHeaders(this.graphqlResourceEtag); + }, query: getPipelineDetails, pollInterval: 10000, variables() { @@ -43,16 +59,45 @@ export default { }; }, update(data) { + /* + This check prevents the pipeline from being overwritten + when a poll times out and the data returned is empty. + This can be removed once the timeout behavior is updated. + See: https://gitlab.com/gitlab-org/gitlab/-/issues/323213. + */ + + if (!data?.project?.pipeline) { + return this.pipeline; + } + return unwrapPipelineData(this.pipelineProjectPath, data); }, - error() { - this.reportFailure(LOAD_FAILURE); + error(err) { + this.reportFailure({ type: LOAD_FAILURE, skipSentry: true }); + reportToSentry( + this.$options.name, + `type: ${LOAD_FAILURE}, info: ${serializeLoadErrors(err)}`, + ); + }, + result({ error }) { + /* + If there is a successful load after a failure, clear + the failure notification to avoid confusion. + */ + if (!error && this.alertType === LOAD_FAILURE) { + this.hideAlert(); + } }, }, }, computed: { alert() { switch (this.alertType) { + case DRAW_FAILURE: + return { + text: this.$options.errorTexts[DRAW_FAILURE], + variant: 'danger', + }; case LOAD_FAILURE: return { text: this.$options.errorTexts[LOAD_FAILURE], @@ -65,6 +110,12 @@ export default { }; } }, + configPaths() { + return { + graphqlResourceEtag: this.graphqlResourceEtag, + metricsPath: this.metricsPath, + }; + }, showLoadingIcon() { /* Shows the icon only when the graph is empty, not when it is is @@ -82,15 +133,20 @@ export default { methods: { hideAlert() { this.showAlert = false; + this.alertType = null; }, refreshPipelineGraph() { this.$apollo.queries.pipeline.refetch(); }, - reportFailure(type) { + /* eslint-disable @gitlab/require-i18n-strings */ + reportFailure({ type, err = 'No error string passed.', skipSentry = false }) { this.showAlert = true; - this.failureType = type; - reportToSentry(this.$options.name, this.failureType); + this.alertType = type; + if (!skipSentry) { + reportToSentry(this.$options.name, `type: ${type}, info: ${err}`); + } }, + /* eslint-enable @gitlab/require-i18n-strings */ }, }; </script> @@ -102,6 +158,7 @@ export default { <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" /> <pipeline-graph v-if="pipeline" + :config-paths="configPaths" :pipeline="pipeline" @error="reportFailure" @refreshPipelineGraph="refreshPipelineGraph" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 3ce77a1c60a..b55a77a3c4f 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -3,7 +3,14 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu import { LOAD_FAILURE } from '../../constants'; import { ONE_COL_WIDTH, UPSTREAM } from './constants'; import LinkedPipeline from './linked_pipeline.vue'; -import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; +import { + getQueryHeaders, + reportToSentry, + serializeLoadErrors, + toggleQueryPollingByVisibility, + unwrapPipelineData, + validateConfigPaths, +} from './utils'; export default { components: { @@ -15,6 +22,11 @@ export default { type: String, required: true, }, + configPaths: { + type: Object, + required: true, + validator: validateConfigPaths, + }, linkedPipelines: { type: Array, required: true, @@ -72,6 +84,9 @@ export default { this.$apollo.addSmartQuery('currentPipeline', { query: getPipelineDetails, pollInterval: 10000, + context() { + return getQueryHeaders(this.configPaths.graphqlResourceEtag); + }, variables() { return { projectPath, @@ -79,18 +94,29 @@ export default { }; }, update(data) { + /* + This check prevents the pipeline from being overwritten + when a poll times out and the data returned is empty. + This can be removed once the timeout behavior is updated. + See: https://gitlab.com/gitlab-org/gitlab/-/issues/323213. + */ + + if (!data?.project?.pipeline) { + return this.currentPipeline; + } + return unwrapPipelineData(projectPath, data); }, result() { this.loadingPipelineId = null; this.$emit('scrollContainer'); }, - error(err, _vm, _key, type) { - this.$emit('error', LOAD_FAILURE); + error(err) { + this.$emit('error', { type: LOAD_FAILURE, skipSentry: true }); reportToSentry( 'linked_pipelines_column', - `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`, + `error type: ${LOAD_FAILURE}, error: ${serializeLoadErrors(err)}`, ); }, }); @@ -175,6 +201,7 @@ export default { v-if="isExpanded(pipeline.id)" :type="type" class="d-inline-block gl-mt-n2" + :config-paths="configPaths" :pipeline="currentPipeline" :is-linked-pipeline="true" /> diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 1a935599bfa..b9a8e2638bc 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,6 +1,6 @@ +import * as Sentry from '@sentry/browser'; import Visibility from 'visibilityjs'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import * as Sentry from '~/sentry/wrapper'; import { unwrapStagesWithNeeds } from '../unwrapping_utils'; const addMulti = (mainPipelineProjectPath, linkedPipeline) => { @@ -10,6 +10,73 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { }; }; +/* eslint-disable @gitlab/require-i18n-strings */ +const getQueryHeaders = (etagResource) => { + return { + fetchOptions: { + method: 'GET', + }, + headers: { + 'X-GITLAB-GRAPHQL-FEATURE-CORRELATION': 'verify/ci/pipeline-graph', + 'X-GITLAB-GRAPHQL-RESOURCE-ETAG': etagResource, + 'X-Requested-With': 'XMLHttpRequest', + }, + }; +}; + +const reportToSentry = (component, failureType) => { + Sentry.withScope((scope) => { + scope.setTag('component', component); + Sentry.captureException(failureType); + }); +}; + +const serializeGqlErr = (gqlError) => { + const { locations = [], message = '', path = [] } = gqlError; + + return ` + ${message}. + Locations: ${locations + .flatMap((loc) => Object.entries(loc)) + .flat(2) + .join(' ')}. + Path: ${path.join(', ')}. + `; +}; + +const serializeLoadErrors = (errors) => { + const { gqlError, graphQLErrors, networkError, message } = errors; + + if (graphQLErrors) { + return graphQLErrors.map((err) => serializeGqlErr(err)).join('; '); + } + + if (gqlError) { + return serializeGqlErr(gqlError); + } + + if (networkError) { + return `Network error: ${networkError.message}`; + } + + return message; +}; + +/* eslint-enable @gitlab/require-i18n-strings */ + +const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { + const stopStartQuery = (query) => { + if (!Visibility.hidden()) { + query.startPolling(interval); + } else { + query.stopPolling(); + } + }; + + stopStartQuery(queryRef); + Visibility.change(stopStartQuery.bind(null, queryRef)); +}; + const transformId = (linkedPipeline) => { return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) }; }; @@ -42,24 +109,14 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { }; }; -const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { - const stopStartQuery = (query) => { - if (!Visibility.hidden()) { - query.startPolling(interval); - } else { - query.stopPolling(); - } - }; - - stopStartQuery(queryRef); - Visibility.change(stopStartQuery.bind(null, queryRef)); -}; - -export { unwrapPipelineData, toggleQueryPollingByVisibility }; +const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; -export const reportToSentry = (component, failureType) => { - Sentry.withScope((scope) => { - scope.setTag('component', component); - Sentry.captureException(failureType); - }); +export { + getQueryHeaders, + reportToSentry, + serializeGqlErr, + serializeLoadErrors, + toggleQueryPollingByVisibility, + unwrapPipelineData, + validateConfigPaths, }; diff --git a/app/assets/javascripts/pipelines/components/graph_shared/api.js b/app/assets/javascripts/pipelines/components/graph_shared/api.js new file mode 100644 index 00000000000..04ac15ae24c --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/api.js @@ -0,0 +1,8 @@ +import axios from '~/lib/utils/axios_utils'; +import { reportToSentry } from '../graph/utils'; + +export const reportPerformance = (path, stats) => { + axios.post(path, stats).catch((err) => { + reportToSentry('links_inner_perf', `error: ${err}`); + }); +}; diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue index 289e04e02c5..fad57084992 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -1,8 +1,19 @@ <script> import { isEmpty } from 'lodash'; +import { + PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START, + PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END, + PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + PIPELINES_DETAIL_LINK_DURATION, + PIPELINES_DETAIL_LINKS_TOTAL, + PIPELINES_DETAIL_LINKS_JOB_RATIO, +} from '~/performance/constants'; +import { performanceMarkAndMeasure } from '~/performance/utils'; import { DRAW_FAILURE } from '../../constants'; import { createJobsHash, generateJobNeedsDict } from '../../utils'; +import { reportToSentry } from '../graph/utils'; import { parseData } from '../parsing_utils'; +import { reportPerformance } from './api'; import { generateLinksData } from './drawing_utils'; export default { @@ -25,6 +36,15 @@ export default { type: Array, required: true, }, + totalGroups: { + type: Number, + required: true, + }, + metricsConfig: { + type: Object, + required: false, + default: () => ({}), + }, defaultLinkColor: { type: String, required: false, @@ -43,6 +63,9 @@ export default { }; }, computed: { + shouldCollectMetrics() { + return this.metricsConfig.collectMetrics && this.metricsConfig.path; + }, hasHighlightedJob() { return Boolean(this.highlightedJob); }, @@ -87,23 +110,70 @@ export default { this.$emit('highlightedJobsChange', jobs); }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, mounted() { if (!isEmpty(this.pipelineData)) { this.prepareLinkData(); } }, methods: { + beginPerfMeasure() { + if (this.shouldCollectMetrics) { + performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START }); + } + }, + finishPerfMeasureAndSend() { + if (this.shouldCollectMetrics) { + performanceMarkAndMeasure({ + mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END, + measures: [ + { + name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START, + }, + ], + }); + } + + window.requestAnimationFrame(() => { + const duration = window.performance.getEntriesByName( + PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + )[0]?.duration; + + if (!duration) { + return; + } + + const data = { + histograms: [ + { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 }, + { name: PIPELINES_DETAIL_LINKS_TOTAL, value: this.links.length }, + { + name: PIPELINES_DETAIL_LINKS_JOB_RATIO, + value: this.links.length / this.totalGroups, + }, + ], + }; + + reportPerformance(this.metricsConfig.path, data); + }); + }, isLinkHighlighted(linkRef) { return this.highlightedLinks.includes(linkRef); }, prepareLinkData() { + this.beginPerfMeasure(); try { const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups); const parsedData = parseData(arrayOfJobs); this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`); - } catch { - this.$emit('error', DRAW_FAILURE); + } catch (err) { + this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false }); + reportToSentry(this.$options.name, err); } + this.finishPerfMeasureAndSend(); }, getLinkClasses(link) { return [ diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue index 1c1bc7ecb2a..42eab13b0bd 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -1,6 +1,19 @@ <script> import { GlAlert } from '@gitlab/ui'; +import { isEmpty } from 'lodash'; import { __ } from '~/locale'; +import { + PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START, + PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END, + PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + PIPELINES_DETAIL_LINK_DURATION, + PIPELINES_DETAIL_LINKS_TOTAL, + PIPELINES_DETAIL_LINKS_JOB_RATIO, +} from '~/performance/constants'; +import { performanceMarkAndMeasure } from '~/performance/utils'; +import { reportToSentry } from '../graph/utils'; +import { parseData } from '../parsing_utils'; +import { reportPerformance } from './api'; import LinksInner from './links_inner.vue'; export default { @@ -19,6 +32,16 @@ export default { type: Array, required: true, }, + metricsConfig: { + type: Object, + required: false, + default: () => ({}), + }, + neverShowLinks: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -41,16 +64,93 @@ export default { return acc + Number(groups.length); }, 0); }, + shouldCollectMetrics() { + return this.metricsConfig.collectMetrics && this.metricsConfig.path; + }, showAlert() { - return !this.showLinkedLayers && !this.alertDismissed; + /* + This is a hard override that allows us to turn off the links without + needing to remove the component entirely for iteration or based on graph type. + */ + if (this.neverShowLinks) { + return false; + } + + return !this.containerZero && !this.showLinkedLayers && !this.alertDismissed; }, showLinkedLayers() { + /* + This is a hard override that allows us to turn off the links without + needing to remove the component entirely for iteration or based on graph type. + */ + if (this.neverShowLinks) { + return false; + } + return ( !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS) ); }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, + mounted() { + /* + This is code to get metrics for the graph (to observe links performance). + It is currently here because we want values for links without drawing them. + It can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/298930 + is closed and functionality is enabled by default. + */ + + if (this.neverShowLinks && !isEmpty(this.pipelineData)) { + window.requestAnimationFrame(() => { + this.prepareLinkData(); + }); + } + }, methods: { + beginPerfMeasure() { + if (this.shouldCollectMetrics) { + performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START }); + } + }, + finishPerfMeasureAndSend(numLinks) { + if (this.shouldCollectMetrics) { + performanceMarkAndMeasure({ + mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END, + measures: [ + { + name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START, + }, + ], + }); + } + + window.requestAnimationFrame(() => { + const duration = window.performance.getEntriesByName( + PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION, + )[0]?.duration; + + if (!duration) { + return; + } + + const data = { + histograms: [ + { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 }, + { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks }, + { + name: PIPELINES_DETAIL_LINKS_JOB_RATIO, + value: numLinks / this.numGroups, + }, + ], + }; + + reportPerformance(this.metricsConfig.path, data); + }); + }, dismissAlert() { this.alertDismissed = true; }, @@ -58,6 +158,17 @@ export default { this.dismissAlert(); this.showLinksOverride = true; }, + prepareLinkData() { + this.beginPerfMeasure(); + let numLinks; + try { + const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups); + numLinks = parseData(arrayOfJobs).links.length; + } catch (err) { + reportToSentry(this.$options.name, err); + } + this.finishPerfMeasureAndSend(numLinks); + }, }, }; </script> @@ -66,6 +177,8 @@ export default { v-if="showLinkedLayers" :container-measurements="containerMeasurements" :pipeline-data="pipelineData" + :total-groups="numGroups" + :metrics-config="metricsConfig" v-bind="$attrs" v-on="$listeners" > diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue index 4a7ee3b2af7..707d6966e77 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -64,13 +64,6 @@ export default { hasHighlightedJob() { return Boolean(this.highlightedJob); }, - alert() { - if (this.hasError) { - return this.failure; - } - - return this.warning; - }, failure() { switch (this.failureType) { case DRAW_FAILURE: @@ -210,11 +203,11 @@ export default { <div> <gl-alert v-if="hasError" - :variant="alert.variant" - :dismissible="alert.dismissible" - @dismiss="alert.dismissible ? resetFailure : null" + :variant="failure.variant" + :dismissible="failure.dismissible" + @dismiss="resetFailure" > - {{ alert.text }} + {{ failure.text }} </gl-alert> <div v-if="!hideGraph" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue index 8a656bb47f4..f8107d288d9 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -1,23 +1,22 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; export default { i18n: { - infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build, - test, and deploy your code. Let GitLab take care of time - consuming tasks, so you can spend more time creating.`), - buttonMessage: s__('Pipelines|Get started with CI/CD'), + title: s__('Pipelines|Build with confidence'), + description: s__(`Pipelines|GitLab CI/CD can automatically build, + test, and deploy your code. Let GitLab take care of time + consuming tasks, so you can spend more time creating.`), + btnText: s__('Pipelines|Get started with CI/CD'), + noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'), }, name: 'PipelinesEmptyState', components: { - GlButton, + GlEmptyState, }, props: { - helpPagePath: { - type: String, - required: true, - }, emptyStateSvgPath: { type: String, required: true, @@ -27,40 +26,28 @@ export default { required: true, }, }, + computed: { + ciHelpPagePath() { + return helpPagePath('ci/quick_start/index.md'); + }, + }, }; </script> <template> - <div class="row empty-state js-empty-state"> - <div class="col-12"> - <div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div> - </div> - - <div class="col-12"> - <div class="text-content"> - <template v-if="canSetCi"> - <h4 data-testid="header-text" class="gl-text-center"> - {{ s__('Pipelines|Build with confidence') }} - </h4> - <p data-testid="info-text"> - {{ $options.i18n.infoMessage }} - </p> - - <div class="gl-text-center"> - <gl-button - :href="helpPagePath" - variant="info" - category="primary" - data-testid="get-started-pipelines" - > - {{ $options.i18n.buttonMessage }} - </gl-button> - </div> - </template> - - <p v-else class="gl-text-center"> - {{ s__('Pipelines|This project is not currently set up to run pipelines.') }} - </p> - </div> - </div> + <div> + <gl-empty-state + v-if="canSetCi" + :title="$options.i18n.title" + :svg-path="emptyStateSvgPath" + :description="$options.i18n.description" + :primary-button-text="$options.i18n.btnText" + :primary-button-link="ciHelpPagePath" + /> + <gl-empty-state + v-else + title="" + :svg-path="emptyStateSvgPath" + :description="$options.i18n.noCiDescription" + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue new file mode 100644 index 00000000000..05372010d0f --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -0,0 +1,54 @@ +<script> +import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; +/** + * Renders the pipeline mini graph. + */ +export default { + components: { + PipelineStage, + }, + props: { + stages: { + type: Array, + required: true, + }, + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, + stagesClass: { + type: [Array, Object, String], + required: false, + default: '', + }, + isMergeTrain: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + onPipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> +<template> + <div data-testid="widget-mini-pipeline-graph"> + <div + v-for="stage in stages" + :key="stage.name" + :class="stagesClass" + class="stage-container dropdown" + > + <pipeline-stage + :stage="stage" + :update-dropdown="updateDropdown" + :is-merge-train="isMergeTrain" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue new file mode 100644 index 00000000000..81eeead2171 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -0,0 +1,119 @@ +<script> +import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import eventHub from '../../event_hub'; +import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import PipelinesManualActions from './pipelines_manual_actions.vue'; + +export default { + i18n: { + cancelTitle: __('Cancel'), + redeployTitle: __('Retry'), + }, + directives: { + GlTooltip: GlTooltipDirective, + GlModalDirective, + }, + components: { + GlButton, + PipelinesManualActions, + PipelinesArtifactsComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + cancelingPipeline: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + isRetrying: false, + }; + }, + computed: { + displayPipelineActions() { + return ( + this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length + ); + }, + actions() { + if (!this.pipeline || !this.pipeline.details) { + return []; + } + const { details } = this.pipeline; + return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; + }, + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, + }, + watch: { + pipeline() { + this.isRetrying = false; + }, + }, + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipeline: this.pipeline, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, + }, +}; +</script> + +<template> + <div v-if="displayPipelineActions" class="gl-text-right"> + <div class="btn-group"> + <pipelines-manual-actions v-if="actions.length > 0" :actions="actions" /> + + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + :artifacts="pipeline.details.artifacts" + /> + + <gl-button + v-if="pipeline.flags.retryable" + v-gl-tooltip.hover + :aria-label="$options.i18n.redeployTitle" + :title="$options.i18n.redeployTitle" + :disabled="isRetrying" + :loading="isRetrying" + class="js-pipelines-retry-button" + data-qa-selector="pipeline_retry_button" + icon="repeat" + variant="default" + category="secondary" + @click="handleRetryClick" + /> + + <gl-button + v-if="pipeline.flags.cancelable" + v-gl-tooltip.hover + v-gl-modal-directive="'confirmation-modal'" + :aria-label="$options.i18n.cancelTitle" + :title="$options.i18n.cancelTitle" + :loading="isCancelling" + :disabled="isCancelling" + icon="close" + variant="danger" + category="primary" + class="js-pipelines-cancel-button" + @click="handleCancelClick" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue new file mode 100644 index 00000000000..bdb7dd06620 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -0,0 +1,152 @@ +<script> +/** + * Renders each stage of the pipeline mini graph. + * + * Given the provided endpoint will make a request to + * fetch the dropdown data when the stage is clicked. + * + * Request is made inside this component to make it reusable between: + * 1. Pipelines main table + * 2. Pipelines table in commit and Merge request views + * 3. Merge request widget + * 4. Commit widget + */ + +import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { deprecatedCreateFlash as Flash } from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import eventHub from '../../event_hub'; +import JobItem from '../graph/job_item.vue'; + +export default { + components: { + GlIcon, + GlLoadingIcon, + GlDropdown, + JobItem, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + stage: { + type: Object, + required: true, + }, + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, + isMergeTrain: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isLoading: false, + dropdownContent: [], + }; + }, + computed: { + triggerButtonClass() { + return `ci-status-icon-${this.stage.status.group}`; + }, + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; + }, + }, + watch: { + updateDropdown() { + if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { + this.fetchJobs(); + } + }, + }, + methods: { + onShowDropdown() { + eventHub.$emit('clickedDropdown'); + this.isLoading = true; + this.fetchJobs(); + }, + fetchJobs() { + axios + .get(this.stage.dropdown_path) + .then(({ data }) => { + this.dropdownContent = data.latest_statuses; + this.isLoading = false; + }) + .catch(() => { + this.$refs.dropdown.hide(); + this.isLoading = false; + + Flash(__('Something went wrong on our end.')); + }); + }, + isDropdownOpen() { + return this.$el.classList.contains('show'); + }, + pipelineActionRequestComplete() { + // close the dropdown in MR widget + this.$refs.dropdown.hide(); + + // warn the pipelines table to update + this.$emit('pipelineActionRequestComplete'); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="dropdown" + v-gl-tooltip.hover + data-testid="mini-pipeline-graph-dropdown" + :title="stage.title" + variant="link" + :lazy="true" + :popper-opts="{ placement: 'bottom' }" + :toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]" + menu-class="mini-pipeline-graph-dropdown-menu" + @show="onShowDropdown" + > + <template #button-content> + <span class="gl-pointer-events-none"> + <gl-icon :name="borderlessIcon" /> + </span> + </template> + <gl-loading-icon v-if="isLoading" /> + <ul + v-else + class="js-builds-dropdown-list scrollable-menu" + data-testid="mini-pipeline-graph-dropdown-menu-list" + > + <li v-for="job in dropdownContent" :key="job.id"> + <job-item + :dropdown-length="dropdownContent.length" + :job="job" + css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> + <template v-if="isMergeTrain"> + <li class="gl-new-dropdown-divider" role="presentation"> + <hr role="separator" aria-orientation="horizontal" class="dropdown-divider" /> + </li> + <li> + <div + class="gl-display-flex gl-align-items-center" + data-testid="warning-message-merge-trains" + > + <div class="menu-item gl-font-sm gl-text-gray-300!"> + {{ s__('Pipeline|Merge train pipeline jobs can not be retried') }} + </div> + </div> + </li> + </template> + </ul> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue index 6ac60727f23..c707b395192 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -1,10 +1,12 @@ <script> import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { UserAvatarLink, }, + mixins: [glFeatureFlagMixin()], props: { pipeline: { type: Object, @@ -15,11 +17,19 @@ export default { user() { return this.pipeline.user; }, + classes() { + const triggererClass = 'pipeline-triggerer'; + + if (this.glFeatures.newPipelinesTable) { + return triggererClass; + } + return `table-section section-10 d-none d-md-block ${triggererClass}`; + }, }, }; </script> <template> - <div class="table-section section-10 d-none d-md-block pipeline-triggerer"> + <div :class="classes" data-testid="pipeline-triggerer"> <user-avatar-link v-if="user" :link-href="user.path" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 823ada133d2..0de520a2ca7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,5 +1,7 @@ <script> import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { SCHEDULE_ORIGIN } from '../../constants'; export default { @@ -12,6 +14,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], inject: { targetProjectFullPath: { default: '', @@ -26,10 +29,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, }, computed: { user() { @@ -44,11 +43,25 @@ export default { this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`, ); }, + autoDevopsTagId() { + return `pipeline-url-autodevops-${this.pipeline.id}`; + }, + autoDevopsHelpPath() { + return helpPagePath('topics/autodevops/index.md'); + }, + classes() { + const tagsClass = 'pipeline-tags'; + + if (this.glFeatures.newPipelinesTable) { + return tagsClass; + } + return `table-section section-10 d-none d-md-block ${tagsClass}`; + }, }, }; </script> <template> - <div class="table-section section-10 d-none d-md-block pipeline-tags"> + <div :class="classes" data-testid="pipeline-url-table-cell"> <gl-link :href="pipeline.path" data-testid="pipeline-url-link" @@ -103,38 +116,43 @@ export default { data-testid="pipeline-url-failure" >{{ __('error') }}</gl-badge > - <gl-link - v-if="pipeline.flags.auto_devops" - :id="`pipeline-url-autodevops-${pipeline.id}`" - tabindex="0" - data-testid="pipeline-url-autodevops" - role="button" - ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link - > - <gl-popover - :target="`pipeline-url-autodevops-${pipeline.id}`" - triggers="focus" - placement="top" - > - <template #title> - <div class="gl-font-weight-normal gl-line-height-normal"> - <gl-sprintf - :message=" - __( - 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', - ) - " - > - <template #strong="{ content }"> - <b>{{ content }}</b> - </template> - </gl-sprintf> - </div> - </template> - <gl-link :href="autoDevopsHelpPath" target="_blank" rel="noopener noreferrer nofollow">{{ - __('Learn more about Auto DevOps') - }}</gl-link> - </gl-popover> + <template v-if="pipeline.flags.auto_devops"> + <gl-link + :id="autoDevopsTagId" + tabindex="0" + data-testid="pipeline-url-autodevops" + role="button" + > + <gl-badge variant="info" size="sm"> + {{ __('Auto DevOps') }} + </gl-badge> + </gl-link> + <gl-popover :target="autoDevopsTagId" triggers="focus" placement="top"> + <template #title> + <div class="gl-font-weight-normal gl-line-height-normal"> + <gl-sprintf + :message=" + __( + 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', + ) + " + > + <template #strong="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + </div> + </template> + <gl-link + :href="autoDevopsHelpPath" + data-testid="pipeline-url-autodevops-link" + target="_blank" + > + {{ __('Learn more about Auto DevOps') }} + </gl-link> + </gl-popover> + </template> + <gl-badge v-if="pipeline.flags.stuck" variant="warning" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 48009a9fcb8..19d93e7d083 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -52,10 +52,6 @@ export default { required: false, default: '', }, - helpPagePath: { - type: String, - required: true, - }, emptyStateSvgPath: { type: String, required: true, @@ -68,10 +64,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, hasGitlabCi: { type: Boolean, required: true, @@ -337,7 +329,6 @@ export default { <empty-state v-else-if="stateToRender === $options.stateMap.emptyState" - :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" :can-set-ci="canCreatePipeline" /> @@ -362,7 +353,6 @@ export default { :pipelines="state.pipelines" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue index b13460b4c68..9c3990f82df 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue @@ -31,6 +31,8 @@ export default { :text="$options.translations.artifacts" :aria-label="$options.translations.artifacts" icon="download" + right + lazy text-sr-only > <gl-dropdown-item diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue new file mode 100644 index 00000000000..cc676883c1d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue @@ -0,0 +1,85 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CommitComponent from '~/vue_shared/components/commit.vue'; + +export default { + components: { + CommitComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + commitAuthor() { + let commitAuthorInformation; + + if (!this.pipeline || !this.pipeline.commit) { + return null; + } + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // they can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar, they might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = { + ...this.pipeline.commit.author, + avatar_url: this.pipeline.commit.author_gravatar_url, + }; + } + // 4. If committer is not a GitLab User, they can have a Gravatar + } else { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + + return commitAuthorInformation; + }, + commitTag() { + return this.pipeline?.ref?.tag; + }, + commitRef() { + return this.pipeline?.ref; + }, + commitUrl() { + return this.pipeline?.commit?.commit_path; + }, + commitShortSha() { + return this.pipeline?.commit?.short_id; + }, + commitTitle() { + return this.pipeline?.commit?.title; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :merge-request-ref="pipeline.merge_request" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :show-ref-info="!isChildView" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue index 6890cbb9bed..b94f1a42039 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue @@ -82,6 +82,7 @@ export default { :loading="isLoading" data-testid="pipelines-manual-actions-dropdown" right + lazy icon="play" > <gl-dropdown-item diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue new file mode 100644 index 00000000000..cc3c8d522b3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -0,0 +1,37 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; + +export default { + components: { + CiBadge, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + pipelineStatus() { + return this.pipeline?.details?.status ?? {}; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <ci-badge + :status="pipelineStatus" + :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" + data-qa-selector="pipeline_commit_status" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 24c67184e56..aa27aa7e50d 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,22 +1,97 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTable, GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; +import PipelineMiniGraph from './pipeline_mini_graph.vue'; +import PipelineOperations from './pipeline_operations.vue'; import PipelineStopModal from './pipeline_stop_modal.vue'; +import PipelineTriggerer from './pipeline_triggerer.vue'; +import PipelineUrl from './pipeline_url.vue'; +import PipelinesCommit from './pipelines_commit.vue'; +import PipelinesStatusBadge from './pipelines_status_badge.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue'; +import PipelinesTimeago from './time_ago.vue'; + +const DEFAULT_TD_CLASS = 'gl-p-5!'; +const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; +const DEFAULT_TH_CLASSES = + 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!'; -/** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ export default { + fields: [ + { + key: 'status', + label: s__('Pipeline|Status'), + thClass: DEFAULT_TH_CLASSES, + columnClass: 'gl-w-10p', + tdClass: DEFAULT_TD_CLASS, + thAttr: { 'data-testid': 'status-th' }, + }, + { + key: 'pipeline', + label: s__('Pipeline|Pipeline'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'pipeline-th' }, + }, + { + key: 'triggerer', + label: s__('Pipeline|Triggerer'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'triggerer-th' }, + }, + { + key: 'commit', + label: s__('Pipeline|Commit'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'commit-th' }, + }, + { + key: 'stages', + label: s__('Pipeline|Stages'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'stages-th' }, + }, + { + key: 'timeago', + label: s__('Pipeline|Duration'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'timeago-th' }, + }, + { + key: 'actions', + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'actions-th' }, + }, + ], components: { - PipelinesTableRowComponent, + GlTable, + PipelinesCommit, + PipelineMiniGraph, + PipelineOperations, + PipelinesStatusBadge, PipelineStopModal, + PipelinesTableRowComponent, + PipelinesTimeago, + PipelineTriggerer, + PipelineUrl, }, directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], props: { pipelines: { type: Array, @@ -32,10 +107,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -70,42 +141,103 @@ export default { eventHub.$emit('postAction', this.endpoint); this.cancelingPipeline = this.pipelineId; }, + onPipelineActionRequestComplete() { + eventHub.$emit('refreshPipelinesTable'); + }, }, }; </script> <template> <div class="ci-table"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 js-pipeline-status" role="rowheader"> - {{ s__('Pipeline|Status') }} - </div> - <div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader"> - {{ s__('Pipeline|Pipeline') }} - </div> - <div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader"> - {{ s__('Pipeline|Triggerer') }} - </div> - <div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader"> - {{ s__('Pipeline|Commit') }} - </div> - <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader"> - {{ s__('Pipeline|Stages') }} - </div> - <div class="table-section section-15" role="rowheader"></div> - <div class="table-section section-20" role="rowheader"> - <slot name="table-header-actions"></slot> + <div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table"> + <div class="gl-responsive-table-row table-row-header" role="row"> + <div class="table-section section-10 js-pipeline-status" role="rowheader"> + {{ s__('Pipeline|Status') }} + </div> + <div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader"> + {{ s__('Pipeline|Pipeline') }} + </div> + <div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader"> + {{ s__('Pipeline|Triggerer') }} + </div> + <div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader"> + {{ s__('Pipeline|Commit') }} + </div> + <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader"> + {{ s__('Pipeline|Stages') }} + </div> + <div class="table-section section-15" role="rowheader"></div> + <div class="table-section section-20" role="rowheader"> + <slot name="table-header-actions"></slot> + </div> </div> + <pipelines-table-row-component + v-for="model in pipelines" + :key="model.id" + :pipeline="model" + :pipeline-schedule-url="pipelineScheduleUrl" + :update-graph-dropdown="updateGraphDropdown" + :view-type="viewType" + :canceling-pipeline="cancelingPipeline" + /> </div> - <pipelines-table-row-component - v-for="model in pipelines" - :key="model.id" - :pipeline="model" - :pipeline-schedule-url="pipelineScheduleUrl" - :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" - :view-type="viewType" - :canceling-pipeline="cancelingPipeline" - /> + + <gl-table + v-else + :fields="$options.fields" + :items="pipelines" + tbody-tr-class="commit" + :tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }" + stacked="lg" + fixed + > + <template #head(actions)> + <span class="gl-display-block gl-lg-display-none!">{{ s__('Pipeline|Actions') }}</span> + <slot name="table-header-actions"></slot> + </template> + + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(status)="{ item }"> + <pipelines-status-badge :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-url :pipeline="item" :pipeline-schedule-url="pipelineScheduleUrl" /> + </template> + + <template #cell(triggerer)="{ item }"> + <pipeline-triggerer :pipeline="item" /> + </template> + + <template #cell(commit)="{ item }"> + <pipelines-commit :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(stages)="{ item }"> + <div class="stage-cell"> + <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> + <div></div> + <pipeline-mini-graph + v-if="item.details && item.details.stages && item.details.stages.length > 0" + :stages="item.details.stages" + :update-dropdown="updateGraphDropdown" + @pipelineActionRequestComplete="onPipelineActionRequestComplete" + /> + </div> + </template> + + <template #cell(timeago)="{ item }"> + <pipelines-timeago :pipeline="item" /> + </template> + + <template #cell(actions)="{ item }"> + <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> + </template> + </gl-table> + <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index 572abe2a24a..f684a0b0fcd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -3,13 +3,12 @@ import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; import { __ } from '~/locale'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CommitComponent from '~/vue_shared/components/commit.vue'; -import { PIPELINES_TABLE } from '../../constants'; import eventHub from '../../event_hub'; +import PipelineMiniGraph from './pipeline_mini_graph.vue'; import PipelineTriggerer from './pipeline_triggerer.vue'; import PipelineUrl from './pipeline_url.vue'; -import PipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import PipelineStage from './stage.vue'; +import PipelinesManualActionsComponent from './pipelines_manual_actions.vue'; import PipelinesTimeago from './time_ago.vue'; export default { @@ -22,10 +21,10 @@ export default { GlModalDirective, }, components: { - PipelinesActionsComponent, + PipelinesManualActionsComponent, PipelinesArtifactsComponent, CommitComponent, - PipelineStage, + PipelineMiniGraph, PipelineUrl, PipelineTriggerer, CiBadge, @@ -47,10 +46,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -61,7 +56,6 @@ export default { default: null, }, }, - pipelinesTable: PIPELINES_TABLE, data() { return { isRetrying: false, @@ -137,15 +131,12 @@ export default { commitTitle() { return this.pipeline?.commit?.title; }, - pipelineDuration() { - return this.pipeline?.details?.duration ?? 0; - }, - pipelineFinishedAt() { - return this.pipeline?.details?.finished_at ?? ''; - }, pipelineStatus() { return this.pipeline?.details?.status ?? {}; }, + hasStages() { + return this.pipeline?.details?.stages?.length > 0; + }, displayPipelineActions() { return ( this.pipeline.flags.retryable || @@ -177,6 +168,10 @@ export default { this.isRetrying = true; eventHub.$emit('retryPipeline', this.pipeline.retry_path); }, + handlePipelineActionRequestComplete() { + // warn the pipelines table to update + eventHub.$emit('refreshPipelinesTable'); + }, }, }; </script> @@ -194,11 +189,7 @@ export default { </div> </div> - <pipeline-url - :pipeline="pipeline" - :pipeline-schedule-url="pipelineScheduleUrl" - :auto-devops-help-path="autoDevopsHelpPath" - /> + <pipeline-url :pipeline="pipeline" :pipeline-schedule-url="pipelineScheduleUrl" /> <pipeline-triggerer :pipeline="pipeline" /> <div class="table-section section-wrap section-20"> @@ -220,35 +211,23 @@ export default { <div class="table-section section-wrap section-15 stage-cell"> <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Stages') }}</div> <div class="table-mobile-content"> - <template v-if="pipeline.details.stages.length > 0"> - <div - v-for="(stage, index) in pipeline.details.stages" - :key="index" - class="stage-container dropdown" - data-testid="widget-mini-pipeline-graph" - > - <pipeline-stage - :type="$options.pipelinesTable" - :stage="stage" - :update-dropdown="updateGraphDropdown" - /> - </div> - </template> + <pipeline-mini-graph + v-if="hasStages" + :stages="pipeline.details.stages" + :update-dropdown="updateGraphDropdown" + @pipelineActionRequestComplete="handlePipelineActionRequestComplete" + /> </div> </div> - <pipelines-timeago - class="gl-text-right" - :duration="pipelineDuration" - :finished-time="pipelineFinishedAt" - /> + <pipelines-timeago class="gl-text-right" :pipeline="pipeline" /> <div v-if="displayPipelineActions" class="table-section section-20 table-button-footer pipeline-actions" > <div class="btn-group table-action-buttons"> - <pipelines-actions-component v-if="actions.length > 0" :actions="actions" /> + <pipelines-manual-actions-component v-if="actions.length > 0" :actions="actions" /> <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue deleted file mode 100644 index f5dfb9e72d5..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ /dev/null @@ -1,234 +0,0 @@ -<script> -/** - * Renders each stage of the pipeline mini graph. - * - * Given the provided endpoint will make a request to - * fetch the dropdown data when the stage is clicked. - * - * Request is made inside this component to make it reusable between: - * 1. Pipelines main table - * 2. Pipelines table in commit and Merge request views - * 3. Merge request widget - * 4. Commit widget - */ -import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import $ from 'jquery'; -import { deprecatedCreateFlash as Flash } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { PIPELINES_TABLE } from '../../constants'; -import eventHub from '../../event_hub'; -import JobItem from '../graph/job_item.vue'; - -export default { - components: { - GlIcon, - GlLoadingIcon, - GlDropdown, - JobItem, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [glFeatureFlagsMixin()], - props: { - stage: { - type: Object, - required: true, - }, - - updateDropdown: { - type: Boolean, - required: false, - default: false, - }, - - type: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - isLoading: false, - dropdownContent: [], - }; - }, - computed: { - isCiMiniPipelineGlDropdown() { - // Feature flag ci_mini_pipeline_gl_dropdown - // See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400 - return this.glFeatures?.ciMiniPipelineGlDropdown; - }, - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; - }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; - }, - }, - watch: { - updateDropdown() { - if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { - this.fetchJobs(); - } - }, - }, - updated() { - if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) { - this.stopDropdownClickPropagation(); - } - }, - methods: { - onShowDropdown() { - eventHub.$emit('clickedDropdown'); - this.isLoading = true; - this.fetchJobs(); - }, - onClickStage() { - if (!this.isDropdownOpen()) { - eventHub.$emit('clickedDropdown'); - this.isLoading = true; - this.fetchJobs(); - } - }, - fetchJobs() { - axios - .get(this.stage.dropdown_path) - .then(({ data }) => { - this.dropdownContent = data.latest_statuses; - this.isLoading = false; - }) - .catch(() => { - if (this.isCiMiniPipelineGlDropdown) { - this.$refs.stageGlDropdown.hide(); - } else { - this.closeDropdown(); - } - this.isLoading = false; - - Flash(__('Something went wrong on our end.')); - }); - }, - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - * - * Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true. - */ - stopDropdownClickPropagation() { - $( - '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', - this.$el, - ).on('click', (e) => { - e.stopPropagation(); - }); - }, - closeDropdown() { - if (this.isDropdownOpen()) { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, - isDropdownOpen() { - return this.$el.classList.contains('show'); - }, - pipelineActionRequestComplete() { - if (this.type === PIPELINES_TABLE) { - // warn the table to update - eventHub.$emit('refreshPipelinesTable'); - return; - } - // close the dropdown in mr widget - if (this.isCiMiniPipelineGlDropdown) { - this.$refs.stageGlDropdown.hide(); - } else { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, - }, -}; -</script> - -<template> - <div class="dropdown"> - <gl-dropdown - v-if="isCiMiniPipelineGlDropdown" - ref="stageGlDropdown" - v-gl-tooltip.hover - data-testid="mini-pipeline-graph-dropdown" - :title="stage.title" - variant="link" - :lazy="true" - :popper-opts="{ placement: 'bottom' }" - :toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]" - menu-class="mini-pipeline-graph-dropdown-menu" - @show="onShowDropdown" - > - <template #button-content> - <span class="gl-pointer-events-none"> - <gl-icon :name="borderlessIcon" /> - </span> - </template> - <gl-loading-icon v-if="isLoading" /> - <ul - v-else - class="js-builds-dropdown-list scrollable-menu" - data-testid="mini-pipeline-graph-dropdown-menu-list" - > - <li v-for="job in dropdownContent" :key="job.id"> - <job-item - :dropdown-length="dropdownContent.length" - :job="job" - css-class-job-name="mini-pipeline-graph-dropdown-item" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </li> - </ul> - </gl-dropdown> - - <template v-else> - <button - id="stageDropdown" - ref="dropdown" - v-gl-tooltip.hover - :class="triggerButtonClass" - :title="stage.title" - class="mini-pipeline-graph-dropdown-toggle" - data-testid="mini-pipeline-graph-dropdown-toggle" - data-toggle="dropdown" - data-display="static" - type="button" - aria-haspopup="true" - aria-expanded="false" - @click="onClickStage" - > - <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none"> - <gl-icon :name="borderlessIcon" /> - </span> - </button> - - <div - class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown" - > - <gl-loading-icon v-if="isLoading" /> - <ul v-else class="js-builds-dropdown-list scrollable-menu"> - <li v-for="job in dropdownContent" :key="job.id"> - <job-item - :dropdown-length="dropdownContent.length" - :job="job" - css-class-job-name="mini-pipeline-graph-dropdown-item" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </li> - </ul> - </div> - </template> - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 5548a1021f5..543bdf94307 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -1,5 +1,6 @@ <script> import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { @@ -7,23 +8,19 @@ export default { GlTooltip: GlTooltipDirective, }, components: { GlIcon }, - mixins: [timeagoMixin], + mixins: [timeagoMixin, glFeatureFlagMixin()], props: { - finishedTime: { - type: String, - required: true, - }, - duration: { - type: Number, + pipeline: { + type: Object, required: true, }, }, computed: { - hasDuration() { - return this.duration > 0; + duration() { + return this.pipeline?.details?.duration; }, - hasFinishedTime() { - return this.finishedTime !== ''; + finishedTime() { + return this.pipeline?.details?.finished_at; }, durationFormatted() { const date = new Date(this.duration * 1000); @@ -45,20 +42,36 @@ export default { return `${hh}:${mm}:${ss}`; }, + legacySectionClass() { + return !this.glFeatures.newPipelinesTable ? 'table-section section-15' : ''; + }, + legacyTableMobileClass() { + return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : ''; + }, + showInProgress() { + return !this.duration && !this.finishedTime; + }, }, }; </script> <template> - <div class="table-section section-15"> - <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div> - <div class="table-mobile-content"> - <p v-if="hasDuration" class="duration"> - <gl-icon name="timer" class="gl-vertical-align-baseline!" /> + <div :class="legacySectionClass"> + <div v-if="!glFeatures.newPipelinesTable" class="table-mobile-header" role="rowheader"> + {{ s__('Pipeline|Duration') }} + </div> + <div :class="legacyTableMobileClass"> + <span v-if="showInProgress" data-testid="pipeline-in-progress"> + <gl-icon name="hourglass" class="gl-vertical-align-baseline! gl-mr-2" :size="12" /> + {{ s__('Pipeline|In progress') }} + </span> + + <p v-if="duration" class="duration"> + <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" /> {{ durationFormatted }} </p> - <p v-if="hasFinishedTime" class="finished-at d-none d-md-block"> - <gl-icon name="calendar" class="gl-vertical-align-baseline!" /> + <p v-if="finishedTime" class="finished-at d-none d-md-block"> + <gl-icon name="calendar" class="gl-vertical-align-baseline!" :size="12" /> <time v-gl-tooltip diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 757d285ef19..21b114825a6 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,7 +1,6 @@ import { s__, __ } from '~/locale'; export const CANCEL_REQUEST = 'CANCEL_REQUEST'; -export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const LAYOUT_CHANGE_DELAY = 300; export const FILTER_PIPELINES_SEARCH_DELAY = 200; export const ANY_TRIGGER_AUTHOR = 'Any'; @@ -34,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure'; export const PARSE_FAILURE = 'parse_failure'; export const POST_FAILURE = 'post_failure'; export const UNSUPPORTED_DATA = 'unsupported_data'; + +export const CHILD_VIEW = 'child'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index f837851e5c1..c3444f38ea0 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -93,8 +93,7 @@ export default async function initPipelineDetailsBundle() { /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' ); - const { pipelineProjectPath, pipelineIid } = dataset; - createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, pipelineProjectPath, pipelineIid); + createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, dataset); } catch { Flash(__('An error occurred while loading the pipeline.')); } diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js index 55f3731a3ca..9eba39738dc 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_graph.js +++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js @@ -11,12 +11,15 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( {}, { - batchMax: 2, + useGet: true, }, ), }); -const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid) => { +const createPipelinesDetailApp = ( + selector, + { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {}, +) => { // eslint-disable-next-line no-new new Vue({ el: selector, @@ -25,8 +28,10 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid) => }, apolloProvider, provide: { + metricsPath, pipelineProjectPath, pipelineIid, + graphqlResourceEtag, dataMethod: GRAPHQL, }, errorCaptured(err, _vm, info) { diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js index 7bcc51e18e5..0e2e9785956 100644 --- a/app/assets/javascripts/pipelines/pipelines_index.js +++ b/app/assets/javascripts/pipelines/pipelines_index.js @@ -23,11 +23,9 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { const { endpoint, pipelineScheduleUrl, - helpPagePath, emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline, hasGitlabCi, @@ -56,11 +54,9 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { store: this.store, endpoint, pipelineScheduleUrl, - helpPagePath, emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline: parseBoolean(canCreatePipeline), hasGitlabCi: parseBoolean(hasGitlabCi), |