summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines/components/graph
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue71
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue35
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js97
4 files changed, 194 insertions, 38 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,
};