diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
12 files changed, 181 insertions, 38 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 4e9b21a5c55..0ce94d4f02f 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { reportToSentry } from './utils'; /** * Renders either a cancel, retry or play icon button and handles the post request @@ -50,6 +51,9 @@ export default { return `${actionIconDash} js-icon-${actionIconDash}`; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('action_component', `error: ${err}, info: ${info}`); + }, methods: { /** * The request should not be handled here. @@ -70,10 +74,12 @@ export default { this.$emit('pipelineActionRequestComplete'); }) - .catch(() => { + .catch((err) => { this.isDisabled = false; this.isLoading = false; + reportToSentry('action_component', err); + createFlash(__('An error occurred while making the request.')); }); }, diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 67b2ed3b596..cd403757fe6 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,12 +1,15 @@ <script> import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; +import LinksLayer from '../graph_shared/links_layer.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; import { DOWNSTREAM, MAIN, UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { name: 'PipelineGraph', components: { + LinksLayer, LinkedGraphWrapper, LinkedPipelinesColumn, StageColumnComponent, @@ -31,9 +34,16 @@ export default { DOWNSTREAM, UPSTREAM, }, + CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF', + BASE_CONTAINER_ID: 'pipeline-links-container', data() { return { hoveredJobName: '', + highlightedJobs: [], + measurements: { + width: 0, + height: 0, + }, pipelineExpanded: { jobName: '', expanded: false, @@ -41,6 +51,9 @@ export default { }; }, computed: { + containerId() { + return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`; + }, downstreamPipelines() { return this.hasDownstreamPipelines ? this.pipeline.downstream : []; }, @@ -53,12 +66,13 @@ export default { hasUpstreamPipelines() { return Boolean(this.pipeline?.upstream?.length > 0); }, - // The two show checks prevent upstream / downstream from showing redundant linked columns + // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM ); }, + // The show upstream check prevents showing redundant linked columns showUpstreamPipelines() { return ( this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM @@ -68,7 +82,22 @@ export default { return this.hasUpstreamPipelines ? this.pipeline.upstream : []; }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, + mounted() { + this.measurements = this.getMeasurements(); + }, methods: { + getMeasurements() { + return { + width: this.$refs[this.containerId].scrollWidth, + height: this.$refs[this.containerId].scrollHeight, + }; + }, + onError(errorType) { + this.$emit('error', errorType); + }, setJob(jobName) { this.hoveredJobName = jobName; }, @@ -78,14 +107,17 @@ export default { jobName: expanded ? jobName : '', }; }, + updateHighlightedJobs(jobs) { + this.highlightedJobs = jobs; + }, }, }; </script> <template> <div class="js-pipeline-graph"> <div - class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" - :class="{ 'gl-py-5': !isLinkedPipeline }" + class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" + :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }" > <linked-graph-wrapper> <template #upstream> @@ -94,20 +126,36 @@ export default { :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :type="$options.pipelineTypeConstants.UPSTREAM" - @error="emit('error', errorType)" + @error="onError" /> </template> <template #main> - <stage-column-component - v-for="stage in graph" - :key="stage.name" - :title="stage.name" - :groups="stage.groups" - :action="stage.status.action" - :job-hovered="hoveredJobName" - :pipeline-expanded="pipelineExpanded" - @refreshPipelineGraph="$emit('refreshPipelineGraph')" - /> + <div :id="containerId" :ref="containerId"> + <links-layer + :pipeline-data="graph" + :pipeline-id="pipeline.id" + :container-id="containerId" + :container-measurements="measurements" + :highlighted-job="hoveredJobName" + 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" + :highlighted-jobs="highlightedJobs" + :job-hovered="hoveredJobName" + :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipeline.id" + @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @jobHover="setJob" + /> + </links-layer> + </div> </template> <template #downstream> <linked-pipelines-column @@ -117,7 +165,7 @@ export default { :type="$options.pipelineTypeConstants.DOWNSTREAM" @downstreamHovered="setJob" @pipelineExpandToggle="togglePipelineExpanded" - @error="emit('error', errorType)" + @error="onError" /> </template> </linked-graph-wrapper> 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 9ca4dc1e27a..2164dbf4d55 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -5,6 +5,7 @@ import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue'; import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; +import { reportToSentry } from './utils'; export default { name: 'PipelineGraphLegacy', @@ -78,11 +79,11 @@ export default { return ( this.pipeline.triggered_by && Array.isArray(this.pipeline.triggered_by) && - this.pipeline.triggered_by.find(el => el.isExpanded) + this.pipeline.triggered_by.find((el) => el.isExpanded) ); }, expandedDownstream() { - return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded); + return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded); }, pipelineTypeUpstream() { return this.type !== this.$options.downstream && this.expandedUpstream; @@ -94,6 +95,9 @@ export default { return this.pipeline.project.id; }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, methods: { capitalizeStageName(name) { const escapedName = escape(name); 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 d98e3aad054..f596333237d 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -1,10 +1,10 @@ <script> 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 getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql'; import PipelineGraph from './graph_component.vue'; -import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils'; +import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; export default { name: 'PipelineGraphWrapper', @@ -76,6 +76,9 @@ export default { mounted() { toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, methods: { hideAlert() { this.showAlert = false; @@ -86,6 +89,7 @@ export default { reportFailure(type) { this.showAlert = true; this.failureType = type; + reportToSentry(this.$options.name, this.failureType); }, }, }; 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 203d6a12edd..08d6162aeb8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -2,6 +2,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import JobItem from './job_item.vue'; +import { reportToSentry } from './utils'; /** * Renders the dropdown for the pipeline graph. @@ -22,13 +23,24 @@ export default { type: Object, required: true, }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { + computedJobId() { + return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : ''; + }, tooltipText() { const { name, status } = this.group; return `${name} - ${status.label}`; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('job_group_dropdown', `error: ${err}, info: ${info}`); + }, methods: { pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); @@ -37,7 +49,7 @@ export default { }; </script> <template> - <div class="ci-job-dropdown-container dropdown dropright"> + <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> <button v-gl-tooltip.hover="{ boundary: 'viewport' }" :title="tooltipText" diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 93ebe02d4e8..8262d728a24 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -6,6 +6,7 @@ import { sprintf } from '~/locale'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { accessValue } from './accessors'; import { REST } from './constants'; +import { reportToSentry } from './utils'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -73,6 +74,11 @@ export default { required: false, default: () => ({}), }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { boundary() { @@ -84,6 +90,9 @@ export default { hasDetails() { return accessValue(this.dataMethod, 'hasDetails', this.status); }, + computedJobId() { + return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + }, status() { return this.job && this.job.status ? this.job.status : {}; }, @@ -130,6 +139,9 @@ export default { : this.cssClassJobName; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('job_item', `error: ${err}, info: ${info}`); + }, methods: { hideTooltips() { this.$root.$emit('bv::hide::tooltip'); @@ -142,6 +154,7 @@ export default { </script> <template> <div + :id="computedJobId" class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" data-qa-selector="job_item_container" > @@ -151,8 +164,7 @@ export default { :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" + 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" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 1a179de64cd..d18e604f087 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,9 +1,10 @@ <script> -import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; import { accessValue } from './accessors'; import { DOWNSTREAM, REST, UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { directives: { @@ -14,6 +15,7 @@ export default { GlButton, GlLink, GlLoadingIcon, + GlBadge, }, inject: { dataMethod: { @@ -114,6 +116,9 @@ export default { return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`); + }, methods: { onClickLinkedPipeline() { this.hideTooltips(); @@ -168,7 +173,9 @@ export default { </div> </div> <div class="gl-pt-2"> - <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span> + <gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label"> + {{ label }} + </gl-badge> </div> <gl-button :id="buttonId" 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 7d333087874..40e6a01b88c 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -1,9 +1,9 @@ <script> -import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql'; +import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import LinkedPipeline from './linked_pipeline.vue'; import { LOAD_FAILURE } from '../../constants'; import { UPSTREAM } from './constants'; -import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils'; +import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; export default { components: { @@ -42,8 +42,8 @@ export default { computed: { columnClass() { const positionValues = { - right: 'gl-ml-11', - left: 'gl-mr-7', + right: 'gl-ml-6', + left: 'gl-mr-6', }; return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; }, @@ -80,8 +80,13 @@ export default { result() { this.loadingPipelineId = null; }, - error() { + error(err, _vm, _key, type) { this.$emit('error', LOAD_FAILURE); + + reportToSentry( + 'linked_pipelines_column', + `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`, + ); }, }); 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 7d371b33220..2f1390e07d1 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,6 +1,7 @@ <script> import LinkedPipeline from './linked_pipeline.vue'; import { UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { components: { @@ -42,6 +43,9 @@ export default { return this.type === UPSTREAM; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('linked_pipelines_column_legacy', `error: ${err}, info: ${info}`); + }, methods: { onPipelineClick(downstreamNode, pipeline, index) { this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); 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 b9bddc94ce4..65f8c231885 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -6,6 +6,7 @@ import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; import { GRAPHQL } from './constants'; import { accessValue } from './accessors'; +import { reportToSentry } from './utils'; export default { components: { @@ -15,19 +16,28 @@ export default { MainGraphWrapper, }, props: { - title: { - type: String, - required: true, - }, groups: { type: Array, required: true, }, + pipelineId: { + type: Number, + required: true, + }, + title: { + type: String, + required: true, + }, action: { type: Object, required: false, default: () => ({}), }, + highlightedJobs: { + type: Array, + required: false, + default: () => [], + }, jobHovered: { type: String, required: false, @@ -54,6 +64,9 @@ export default { return !isEmpty(this.action); }, }, + errorCaptured(err, _vm, info) { + reportToSentry('stage_column_component', `error: ${err}, info: ${info}`); + }, methods: { getGroupId(group) { return accessValue(GRAPHQL, 'groupId', group); @@ -61,11 +74,18 @@ export default { groupId(group) { return `ci-badge-${escape(group.name)}`; }, + isFadedOut(jobName) { + return ( + this.jobHovered && + this.highlightedJobs.length > 1 && + !this.highlightedJobs.includes(jobName) + ); + }, }, }; </script> <template> - <main-graph-wrapper> + <main-graph-wrapper class="gl-px-6"> <template #stages> <div data-testid="stage-column-title" @@ -90,16 +110,25 @@ export default { :key="getGroupId(group)" data-testid="stage-column-group" class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + @mouseenter="$emit('jobHover', group.name)" + @mouseleave="$emit('jobHover', '')" > <job-item v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipelineId" css-class-job-name="gl-build-content" + :class="{ 'gl-opacity-3': isFadedOut(group.name) }" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <job-group-dropdown v-else :group="group" /> + <job-group-dropdown + v-else + :group="group" + :pipeline-id="pipelineId" + :class="{ 'gl-opacity-3': isFadedOut(group.name) }" + /> </div> </template> </main-graph-wrapper> 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 258b6bf6b6d..059e8f9f8db 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 @@ -4,6 +4,7 @@ import stageColumnMixin from '../../mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; +import { reportToSentry } from './utils'; export default { components: { @@ -52,6 +53,9 @@ export default { return !isEmpty(this.action); }, }, + errorCaptured(err, _vm, info) { + reportToSentry('stage_column_component_legacy', `error: ${err}, info: ${info}`); + }, methods: { groupId(group) { return `ci-badge-${escape(group.name)}`; diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 32588feb426..1a935599bfa 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,5 +1,6 @@ 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) => { @@ -9,7 +10,7 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { }; }; -const transformId = linkedPipeline => { +const transformId = (linkedPipeline) => { return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) }; }; @@ -42,7 +43,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { }; const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { - const stopStartQuery = query => { + const stopStartQuery = (query) => { if (!Visibility.hidden()) { query.startPolling(interval); } else { @@ -55,3 +56,10 @@ const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { }; export { unwrapPipelineData, toggleQueryPollingByVisibility }; + +export const reportToSentry = (component, failureType) => { + Sentry.withScope((scope) => { + scope.setTag('component', component); + Sentry.captureException(failureType); + }); +}; |