diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/assets/javascripts/pipelines/components/graph | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) | |
download | gitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
15 files changed, 394 insertions, 231 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue deleted file mode 100644 index 1df693704d4..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ /dev/null @@ -1,103 +0,0 @@ -<script> -import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { dasherize } from '~/lib/utils/text_utility'; -import { __ } from '~/locale'; -import { reportToSentry } from './utils'; - -/** - * Renders either a cancel, retry or play icon button and handles the post request - * - * Used in: - * - mr widget mini pipeline graph: `mr_widget_pipeline.vue` - * - pipelines table - * - pipelines table in merge request page - * - pipelines table in commit page - * - pipelines detail page in big graph - */ -export default { - components: { - GlIcon, - GlButton, - GlLoadingIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - tooltipText: { - type: String, - required: true, - }, - link: { - type: String, - required: true, - }, - actionIcon: { - type: String, - required: true, - }, - }, - data() { - return { - isDisabled: false, - isLoading: false, - }; - }, - computed: { - cssClass() { - const actionIconDash = dasherize(this.actionIcon); - return `${actionIconDash} js-icon-${actionIconDash}`; - }, - }, - errorCaptured(err, _vm, info) { - reportToSentry('action_component', `error: ${err}, info: ${info}`); - }, - methods: { - /** - * The request should not be handled here. - * However due to this component being used in several - * different apps it avoids repetition & complexity. - * - */ - onClickAction() { - this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`); - this.isDisabled = true; - this.isLoading = true; - - axios - .post(`${this.link}.json`) - .then(() => { - this.isDisabled = false; - this.isLoading = false; - - this.$emit('pipelineActionRequestComplete'); - }) - .catch((err) => { - this.isDisabled = false; - this.isLoading = false; - - reportToSentry('action_component', err); - - createFlash(__('An error occurred while making the request.')); - }); - }, - }, -}; -</script> -<template> - <gl-button - :id="`js-ci-action-${link}`" - v-gl-tooltip="{ boundary: 'viewport' }" - :title="tooltipText" - :class="cssClass" - :disabled="isDisabled" - class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" - @click.stop="onClickAction" - > - <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" /> - <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" /> - </gl-button> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index caa269f5095..dd9cdae518f 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -10,3 +10,12 @@ export const ONE_COL_WIDTH = 180; export const REST = 'rest'; export const GRAPHQL = 'graphql'; + +export const STAGE_VIEW = 'stage'; +export const LAYER_VIEW = 'layer'; +export const VIEW_TYPE_KEY = 'pipeline_graph_view_type'; + +export const SINGLE_JOB = 'single_job'; +export const JOB_DROPDOWN = 'job_dropdown'; + +export const IID_FAILURE = 'missing_iid'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 363226a0d85..63048777724 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,10 +1,11 @@ <script> +import { reportToSentry } from '../../utils'; import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; import LinksLayer from '../graph_shared/links_layer.vue'; -import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants'; +import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; -import { reportToSentry, validateConfigPaths } from './utils'; +import { validateConfigPaths } from './utils'; export default { name: 'PipelineGraph', @@ -24,11 +25,20 @@ export default { type: Object, required: true, }, + viewType: { + type: String, + required: true, + }, isLinkedPipeline: { type: Boolean, required: false, default: false, }, + pipelineLayers: { + type: Array, + required: false, + default: () => [], + }, type: { type: String, required: false, @@ -44,6 +54,7 @@ export default { data() { return { hoveredJobName: '', + hoveredSourceJobName: '', highlightedJobs: [], measurements: { width: 0, @@ -62,8 +73,8 @@ export default { downstreamPipelines() { return this.hasDownstreamPipelines ? this.pipeline.downstream : []; }, - graph() { - return this.pipeline.stages; + layout() { + return this.isStageView ? this.pipeline.stages : this.generateColumnsFromLayersList(); }, hasDownstreamPipelines() { return Boolean(this.pipeline?.downstream?.length > 0); @@ -71,12 +82,21 @@ export default { hasUpstreamPipelines() { return Boolean(this.pipeline?.upstream?.length > 0); }, + isStageView() { + return this.viewType === STAGE_VIEW; + }, metricsConfig() { return { path: this.configPaths.metricsPath, collectMetrics: true, }; }, + shouldHideLinks() { + return this.isStageView; + }, + shouldShowStageName() { + return !this.isStageView; + }, // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( @@ -100,6 +120,26 @@ export default { this.getMeasurements(); }, methods: { + generateColumnsFromLayersList() { + return this.pipelineLayers.map((layers, idx) => { + /* + look up the groups in each layer, + then add each set of layer groups to a stage-like object + */ + + const groups = layers.map((id) => { + const { stageIdx, groupIdx } = this.pipeline.stagesLookup[id]; + return this.pipeline.stages?.[stageIdx]?.groups?.[groupIdx]; + }); + + return { + name: '', + id: `layer-${idx}`, + status: { action: null }, + groups: groups.filter(Boolean), + }; + }); + }, getMeasurements() { this.measurements = { width: this.$refs[this.containerId].scrollWidth, @@ -112,6 +152,9 @@ export default { setJob(jobName) { this.hoveredJobName = jobName; }, + setSourceJob(jobName) { + this.hoveredSourceJobName = jobName; + }, slidePipelineContainer() { this.$refs.mainPipelineContainer.scrollBy({ left: ONE_COL_WIDTH, @@ -146,31 +189,35 @@ export default { :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :type="$options.pipelineTypeConstants.UPSTREAM" + :view-type="viewType" @error="onError" /> </template> <template #main> <div :id="containerId" :ref="containerId"> <links-layer - :pipeline-data="graph" + :pipeline-data="layout" :pipeline-id="pipeline.id" :container-id="containerId" :container-measurements="measurements" :highlighted-job="hoveredJobName" :metrics-config="metricsConfig" - :never-show-links="true" + :never-show-links="shouldHideLinks" + :view-type="viewType" default-link-color="gl-stroke-transparent" @error="onError" @highlightedJobsChange="updateHighlightedJobs" > <stage-column-component - v-for="stage in graph" - :key="stage.name" - :title="stage.name" - :groups="stage.groups" - :action="stage.status.action" + v-for="column in layout" + :key="column.id || column.name" + :name="column.name" + :groups="column.groups" + :action="column.status.action" :highlighted-jobs="highlightedJobs" + :show-stage-name="shouldShowStageName" :job-hovered="hoveredJobName" + :source-job-hovered="hoveredSourceJobName" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipeline.id" @refreshPipelineGraph="$emit('refreshPipelineGraph')" @@ -188,7 +235,8 @@ export default { :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" :type="$options.pipelineTypeConstants.DOWNSTREAM" - @downstreamHovered="setJob" + :view-type="viewType" + @downstreamHovered="setSourceJob" @pipelineExpandToggle="togglePipelineExpanded" @scrollContainer="slidePipelineContainer" @error="onError" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue index abbf8df6eed..39d0fa8a8ca 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -2,10 +2,10 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { escape, capitalize } from 'lodash'; import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; +import { reportToSentry } from '../../utils'; import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue'; import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; -import { reportToSentry } from './utils'; export default { name: 'PipelineGraphLegacy', 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 962f2ca2a4c..0bc6d883245 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -2,11 +2,16 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { __ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; +import { reportToSentry } from '../../utils'; +import { listByLayers } from '../parsing_utils'; +import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants'; import PipelineGraph from './graph_component.vue'; +import GraphViewSelector from './graph_view_selector.vue'; import { getQueryHeaders, - reportToSentry, serializeLoadErrors, toggleQueryPollingByVisibility, unwrapPipelineData, @@ -17,8 +22,11 @@ export default { components: { GlAlert, GlLoadingIcon, + GraphViewSelector, + LocalStorageSync, PipelineGraph, }, + mixins: [glFeatureFlagMixin()], inject: { graphqlResourceEtag: { default: '', @@ -35,13 +43,18 @@ export default { }, data() { return { - pipeline: null, alertType: null, + currentViewType: STAGE_VIEW, + pipeline: null, + pipelineLayers: null, showAlert: false, }; }, errorTexts: { [DRAW_FAILURE]: __('An error occurred while drawing job relationship links.'), + [IID_FAILURE]: __( + 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.', + ), [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'), [DEFAULT]: __('An unknown error occurred while loading this graph.'), }, @@ -58,6 +71,9 @@ export default { iid: this.pipelineIid, }; }, + skip() { + return !(this.pipelineProjectPath && this.pipelineIid); + }, update(data) { /* This check prevents the pipeline from being overwritten @@ -98,6 +114,11 @@ export default { text: this.$options.errorTexts[DRAW_FAILURE], variant: 'danger', }; + case IID_FAILURE: + return { + text: this.$options.errorTexts[IID_FAILURE], + variant: 'info', + }; case LOAD_FAILURE: return { text: this.$options.errorTexts[LOAD_FAILURE], @@ -123,14 +144,28 @@ export default { */ return this.$apollo.queries.pipeline.loading && !this.pipeline; }, + showGraphViewSelector() { + return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline?.usesNeeds); + }, }, mounted() { + if (!this.pipelineIid) { + this.reportFailure({ type: IID_FAILURE, skipSentry: true }); + } + toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); }, errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, methods: { + getPipelineLayers() { + if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) { + this.pipelineLayers = listByLayers(this.pipeline); + } + + return this.pipelineLayers; + }, hideAlert() { this.showAlert = false; this.alertType = null; @@ -147,7 +182,11 @@ export default { } }, /* eslint-enable @gitlab/require-i18n-strings */ + updateViewType(type) { + this.currentViewType = type; + }, }, + viewTypeKey: VIEW_TYPE_KEY, }; </script> <template> @@ -155,11 +194,24 @@ export default { <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert"> {{ alert.text }} </gl-alert> + <local-storage-sync + :storage-key="$options.viewTypeKey" + :value="currentViewType" + @input="updateViewType" + > + <graph-view-selector + v-if="showGraphViewSelector" + :type="currentViewType" + @updateViewType="updateViewType" + /> + </local-storage-sync> <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" + :pipeline-layers="getPipelineLayers()" + :view-type="currentViewType" @error="reportFailure" @refreshPipelineGraph="refreshPipelineGraph" /> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue new file mode 100644 index 00000000000..f33e6290e37 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue @@ -0,0 +1,85 @@ +<script> +import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { STAGE_VIEW, LAYER_VIEW } from './constants'; + +export default { + name: 'GraphViewSelector', + components: { + GlDropdown, + GlDropdownItem, + GlIcon, + GlSprintf, + }, + props: { + type: { + type: String, + required: true, + }, + }, + data() { + return { + currentViewType: STAGE_VIEW, + }; + }, + i18n: { + labelText: __('Order jobs by'), + }, + views: { + [STAGE_VIEW]: { + type: STAGE_VIEW, + text: { + primary: __('Stage'), + secondary: __('View the jobs grouped into stages'), + }, + }, + [LAYER_VIEW]: { + type: LAYER_VIEW, + text: { + primary: __('%{codeStart}needs:%{codeEnd} relationships'), + secondary: __('View what jobs are needed for a job to run'), + }, + }, + }, + computed: { + currentDropdownText() { + return this.$options.views[this.type].text.primary; + }, + }, + methods: { + itemClick(type) { + this.$emit('updateViewType', type); + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center gl-my-4"> + <span>{{ $options.i18n.labelText }}</span> + <gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4"> + <template #button-content> + <gl-sprintf :message="currentDropdownText"> + <template #code="{ content }"> + <code> {{ content }} </code> + </template> + </gl-sprintf> + <gl-icon class="gl-px-2" name="angle-down" :size="16" /> + </template> + <gl-dropdown-item + v-for="view in $options.views" + :key="view.type" + :secondary-text="view.text.secondary" + @click="itemClick(view.type)" + > + <b> + <gl-sprintf :message="view.text.primary"> + <template #code="{ content }"> + <code> {{ content }} </code> + </template> + </gl-sprintf> + </b> + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index f6aee8c5fcf..6451605a222 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -1,8 +1,7 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { reportToSentry } from '../../utils'; +import { JOB_DROPDOWN, SINGLE_JOB } from './constants'; import JobItem from './job_item.vue'; -import { reportToSentry } from './utils'; /** * Renders the dropdown for the pipeline graph. @@ -11,12 +10,8 @@ import { reportToSentry } from './utils'; * */ export default { - directives: { - GlTooltip: GlTooltipDirective, - }, components: { JobItem, - CiIcon, }, props: { group: { @@ -28,6 +23,15 @@ export default { required: false, default: -1, }, + stageName: { + type: String, + required: false, + default: '', + }, + }, + jobItemTypes: { + jobDropdown: JOB_DROPDOWN, + singleJob: SINGLE_JOB, }, computed: { computedJobId() { @@ -51,22 +55,20 @@ export default { <template> <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> <button - v-gl-tooltip.hover="{ boundary: 'viewport' }" - :title="tooltipText" type="button" data-toggle="dropdown" data-display="static" - class="dropdown-menu-toggle build-content gl-build-content" + class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!" > <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> - <span class="gl-display-flex gl-align-items-center gl-min-w-0"> - <ci-icon :status="group.status" :size="24" class="gl-line-height-0" /> - <span class="gl-text-truncate mw-70p gl-pl-3"> - {{ group.name }} - </span> - </span> + <job-item + :type="$options.jobItemTypes.jobDropdown" + :group-tooltip="tooltipText" + :job="group" + :stage-name="stageName" + /> - <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span> + <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div> </div> </button> @@ -77,6 +79,7 @@ export default { <job-item :dropdown-length="group.size" :job="job" + :type="$options.jobItemTypes.singleJob" css-class-job-name="mini-pipeline-graph-dropdown-item" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 46ef0457d40..6584d89d87c 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -3,11 +3,12 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { sprintf } from '~/locale'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { reportToSentry } from '../../utils'; +import ActionComponent from '../jobs_shared/action_component.vue'; +import JobNameComponent from '../jobs_shared/job_name_component.vue'; import { accessValue } from './accessors'; -import ActionComponent from './action_component.vue'; -import { REST } from './constants'; -import JobNameComponent from './job_name_component.vue'; -import { reportToSentry } from './utils'; +import { REST, SINGLE_JOB } from './constants'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -38,6 +39,7 @@ export default { hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, + CiIcon, JobNameComponent, GlLink, }, @@ -65,6 +67,11 @@ export default { required: false, default: Infinity, }, + groupTooltip: { + type: String, + required: false, + default: '', + }, jobHovered: { type: String, required: false, @@ -80,24 +87,55 @@ export default { required: false, default: -1, }, + sourceJobHovered: { + type: String, + required: false, + default: '', + }, + stageName: { + type: String, + required: false, + default: '', + }, + type: { + type: String, + required: false, + default: SINGLE_JOB, + }, }, computed: { boundary() { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; }, + computedJobId() { + return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + }, detailsPath() { return accessValue(this.dataMethod, 'detailsPath', this.status); }, hasDetails() { return accessValue(this.dataMethod, 'hasDetails', this.status); }, - computedJobId() { - return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + isSingleItem() { + return this.type === SINGLE_JOB; + }, + nameComponent() { + return this.hasDetails ? 'gl-link' : 'div'; + }, + showStageName() { + return Boolean(this.stageName); }, status() { return this.job && this.job.status ? this.job.status : {}; }, + testId() { + return this.hasDetails ? 'job-with-link' : 'job-without-link'; + }, tooltipText() { + if (this.groupTooltip) { + return this.groupTooltip; + } + const textBuilder = []; const { name: jobName } = this.job; @@ -129,7 +167,7 @@ export default { return this.job.status && this.job.status.action && this.job.status.action.path; }, relatedDownstreamHovered() { - return this.job.name === this.jobHovered; + return this.job.name === this.sourceJobHovered; }, relatedDownstreamExpanded() { return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded; @@ -147,6 +185,17 @@ export default { hideTooltips() { this.$root.$emit(BV_HIDE_TOOLTIP); }, + jobItemClick(evt) { + if (this.isSingleItem) { + /* + This is so the jobDropdown still toggles. Issue to refactor: + https://gitlab.com/gitlab-org/gitlab/-/issues/267117 + */ + evt.stopPropagation(); + } + + this.hideTooltips(); + }, pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); }, @@ -156,40 +205,45 @@ export default { <template> <div :id="computedJobId" - class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" + class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between gl-w-full" data-qa-selector="job_item_container" > - <gl-link - v-if="hasDetails" - v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" - :href="detailsPath" - :title="tooltipText" - :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" - data-testid="job-with-link" - @click.stop="hideTooltips" - @mouseout="hideTooltips" - > - <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> - </gl-link> - - <div - v-else - v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" + <component + :is="nameComponent" + v-gl-tooltip="{ + boundary: 'viewport', + placement: 'bottom', + customClass: 'gl-pointer-events-none', + }" :title="tooltipText" :class="jobClasses" - class="js-job-component-tooltip non-details-job-component menu-item" - data-testid="job-without-link" + :href="detailsPath" + class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full" + :data-testid="testId" + @click="jobItemClick" @mouseout="hideTooltips" > - <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> - </div> + <div class="ci-job-name-component gl-display-flex gl-align-items-center"> + <ci-icon :size="24" :status="job.status" class="gl-line-height-0" /> + <div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full"> + <div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div> + <div + v-if="showStageName" + data-testid="stage-name-in-job" + class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal" + > + {{ stageName }} + </div> + </div> + </div> + </component> <action-component v-if="hasAction" :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" + class="gl-mr-1" data-qa-selector="action_button" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue deleted file mode 100644 index fffd8e1818a..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ /dev/null @@ -1,38 +0,0 @@ -<script> -import ciIcon from '../../../vue_shared/components/ci_icon.vue'; - -/** - * Component that renders both the CI icon status and the job name. - * Used in - * - Badge component - * - Dropdown badge components - */ -export default { - components: { - ciIcon, - }, - props: { - name: { - type: String, - required: true, - }, - status: { - type: Object, - required: true, - }, - iconSize: { - type: Number, - required: false, - default: 16, - }, - }, -}; -</script> -<template> - <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center"> - <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" /> - <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block"> - {{ name }} - </span> - </span> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index add7b3445f7..3f746731e34 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -3,9 +3,9 @@ import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@g import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; +import { reportToSentry } from '../../utils'; import { accessValue } from './accessors'; import { DOWNSTREAM, REST, UPSTREAM } from './constants'; -import { reportToSentry } from './utils'; export default { directives: { @@ -183,6 +183,7 @@ export default { class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" :icon="expandedIcon" + :aria-label="__('Expand pipeline')" data-testid="expand-pipeline-button" data-qa-selector="expand_pipeline_button" @click="onClickLinkedPipeline" 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 b55a77a3c4f..7f772e35e55 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -1,11 +1,12 @@ <script> import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { LOAD_FAILURE } from '../../constants'; -import { ONE_COL_WIDTH, UPSTREAM } from './constants'; +import { reportToSentry } from '../../utils'; +import { listByLayers } from '../parsing_utils'; +import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW } from './constants'; import LinkedPipeline from './linked_pipeline.vue'; import { getQueryHeaders, - reportToSentry, serializeLoadErrors, toggleQueryPollingByVisibility, unwrapPipelineData, @@ -35,11 +36,16 @@ export default { type: String, required: true, }, + viewType: { + type: String, + required: true, + }, }, data() { return { currentPipeline: null, loadingPipelineId: null, + pipelineLayers: {}, pipelineExpanded: false, }; }, @@ -123,6 +129,13 @@ export default { toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline); }, + getPipelineLayers(id) { + if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) { + this.pipelineLayers[id] = listByLayers(this.currentPipeline); + } + + return this.pipelineLayers[id]; + }, isExpanded(id) { return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id); }, @@ -203,7 +216,9 @@ export default { class="d-inline-block gl-mt-n2" :config-paths="configPaths" :pipeline="currentPipeline" + :pipeline-layers="getPipelineLayers(pipeline.id)" :is-linked-pipeline="true" + :view-type="viewType" /> </div> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue index 0d1ff94c275..39baeb6e1c3 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue @@ -1,7 +1,7 @@ <script> +import { reportToSentry } from '../../utils'; import { UPSTREAM } from './constants'; import LinkedPipeline from './linked_pipeline.vue'; -import { reportToSentry } from './utils'; export default { components: { diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 0a762563114..fa2f381c8a4 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,12 +1,13 @@ <script> import { capitalize, escape, isEmpty } from 'lodash'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { reportToSentry } from '../../utils'; import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; +import ActionComponent from '../jobs_shared/action_component.vue'; import { accessValue } from './accessors'; -import ActionComponent from './action_component.vue'; import { GRAPHQL } from './constants'; import JobGroupDropdown from './job_group_dropdown.vue'; import JobItem from './job_item.vue'; -import { reportToSentry } from './utils'; export default { components: { @@ -15,17 +16,18 @@ export default { JobItem, MainGraphWrapper, }, + mixins: [glFeatureFlagMixin()], props: { groups: { type: Array, required: true, }, - pipelineId: { - type: Number, + name: { + type: String, required: true, }, - title: { - type: String, + pipelineId: { + type: Number, required: true, }, action: { @@ -48,6 +50,16 @@ export default { required: false, default: () => ({}), }, + showStageName: { + type: Boolean, + required: false, + default: false, + }, + sourceJobHovered: { + type: String, + required: false, + default: '', + }, }, titleClasses: [ 'gl-font-weight-bold', @@ -57,8 +69,23 @@ export default { 'gl-pl-3', ], computed: { + /* + currentGroups and filteredGroups are part of + a test to hunt down a bug + (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57142). + + They should be removed when the bug is rectified. + */ + currentGroups() { + return this.glFeatures.pipelineFilterJobs ? this.filteredGroups : this.groups; + }, + filteredGroups() { + return this.groups.map((group) => { + return { ...group, jobs: group.jobs.filter(Boolean) }; + }); + }, formattedTitle() { - return capitalize(escape(this.title)); + return capitalize(escape(this.name)); }, hasAction() { return !isEmpty(this.action); @@ -80,6 +107,18 @@ export default { isFadedOut(jobName) { return this.highlightedJobs.length > 1 && !this.highlightedJobs.includes(jobName); }, + isParallel(group) { + return group.size > 1 && group.jobs.length > 1; + }, + singleJobExists(group) { + const firstJobDefined = Boolean(group.jobs?.[0]); + + if (!firstJobDefined) { + reportToSentry('stage_column_component', 'undefined_job_hunt'); + } + + return group.size === 1 && firstJobDefined; + }, }, }; </script> @@ -104,7 +143,7 @@ export default { </template> <template #jobs> <div - v-for="group in groups" + v-for="group in currentGroups" :id="groupId(group)" :key="getGroupId(group)" data-testid="stage-column-group" @@ -113,17 +152,23 @@ export default { @mouseleave="$emit('jobHover', '')" > <job-item - v-if="group.size === 1" + v-if="singleJobExists(group)" :job="group.jobs[0]" :job-hovered="jobHovered" + :source-job-hovered="sourceJobHovered" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" + :stage-name="showStageName ? group.stageName : ''" css-class-job-name="gl-build-content" :class="{ 'gl-opacity-3': isFadedOut(group.name) }" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <div v-else :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> - <job-group-dropdown :group="group" :pipeline-id="pipelineId" /> + <div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> + <job-group-dropdown + :group="group" + :stage-name="showStageName ? group.stageName : ''" + :pipeline-id="pipelineId" + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue index 2cee2fbbd8f..cbaf07c05cf 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue @@ -1,10 +1,10 @@ <script> import { isEmpty, escape } from 'lodash'; import stageColumnMixin from '../../mixins/stage_column_mixin'; -import ActionComponent from './action_component.vue'; +import { reportToSentry } from '../../utils'; +import ActionComponent from '../jobs_shared/action_component.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import JobItem from './job_item.vue'; -import { reportToSentry } from './utils'; export default { components: { diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index b9a8e2638bc..373aa6bf9a1 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,7 +1,6 @@ -import * as Sentry from '@sentry/browser'; import Visibility from 'visibilityjs'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { unwrapStagesWithNeeds } from '../unwrapping_utils'; +import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils'; const addMulti = (mainPipelineProjectPath, linkedPipeline) => { return { @@ -24,13 +23,6 @@ const getQueryHeaders = (etagResource) => { }; }; -const reportToSentry = (component, failureType) => { - Sentry.withScope((scope) => { - scope.setTag('component', component); - Sentry.captureException(failureType); - }); -}; - const serializeGqlErr = (gqlError) => { const { locations = [], message = '', path = [] } = gqlError; @@ -94,12 +86,13 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { stages: { nodes: stages }, } = pipeline; - const nodes = unwrapStagesWithNeeds(stages); + const { stages: updatedStages, lookup } = unwrapStagesWithNeedsAndLookup(stages); return { ...pipeline, id: getIdFromGraphQLId(pipeline.id), - stages: nodes, + stages: updatedStages, + stagesLookup: lookup, upstream: upstream ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId) : [], @@ -113,7 +106,6 @@ const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; export { getQueryHeaders, - reportToSentry, serializeGqlErr, serializeLoadErrors, toggleQueryPollingByVisibility, |