diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
8 files changed, 132 insertions, 489 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ea45b5e3ec7..015f0519c72 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -39,10 +39,10 @@ export default { required: false, default: false, }, - pipelineLayers: { - type: Array, + computedPipelineInfo: { + type: Object, required: false, - default: () => [], + default: () => ({}), }, type: { type: String, @@ -81,7 +81,10 @@ export default { layout() { return this.isStageView ? this.pipeline.stages - : generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers); + : generateColumnsFromLayersListMemoized( + this.pipeline, + this.computedPipelineInfo.pipelineLayers, + ); }, hasDownstreamPipelines() { return Boolean(this.pipeline?.downstream?.length > 0); @@ -92,6 +95,9 @@ export default { isStageView() { return this.viewType === STAGE_VIEW; }, + linksData() { + return this.computedPipelineInfo?.linksData ?? null; + }, metricsConfig() { return { path: this.configPaths.metricsPath, @@ -188,6 +194,7 @@ export default { :container-id="containerId" :container-measurements="measurements" :highlighted-job="hoveredJobName" + :links-data="linksData" :metrics-config="metricsConfig" :show-links="showJobLinks" :view-type="viewType" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue deleted file mode 100644 index 39d0fa8a8ca..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ /dev/null @@ -1,269 +0,0 @@ -<script> -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'; - -export default { - name: 'PipelineGraphLegacy', - components: { - GlLoadingIcon, - LinkedPipelinesColumnLegacy, - StageColumnComponentLegacy, - }, - mixins: [GraphBundleMixin], - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, - isLinkedPipeline: { - type: Boolean, - required: false, - default: false, - }, - mediator: { - type: Object, - required: true, - }, - type: { - type: String, - required: false, - default: MAIN, - }, - }, - upstream: UPSTREAM, - downstream: DOWNSTREAM, - data() { - return { - downstreamMarginTop: null, - jobName: null, - pipelineExpanded: { - jobName: '', - expanded: false, - }, - }; - }, - computed: { - graph() { - return this.pipeline.details?.stages; - }, - hasUpstream() { - return ( - this.type !== this.$options.downstream && - this.upstreamPipelines && - this.pipeline.triggered_by !== null - ); - }, - upstreamPipelines() { - return this.pipeline.triggered_by; - }, - hasDownstream() { - return ( - this.type !== this.$options.upstream && - this.downstreamPipelines && - this.pipeline.triggered.length > 0 - ); - }, - downstreamPipelines() { - return this.pipeline.triggered; - }, - expandedUpstream() { - return ( - this.pipeline.triggered_by && - Array.isArray(this.pipeline.triggered_by) && - this.pipeline.triggered_by.find((el) => el.isExpanded) - ); - }, - expandedDownstream() { - return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded); - }, - pipelineTypeUpstream() { - return this.type !== this.$options.downstream && this.expandedUpstream; - }, - pipelineTypeDownstream() { - return this.type !== this.$options.upstream && this.expandedDownstream; - }, - pipelineProjectId() { - return this.pipeline.project.id; - }, - }, - errorCaptured(err, _vm, info) { - reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); - }, - methods: { - capitalizeStageName(name) { - const escapedName = escape(name); - return capitalize(escapedName); - }, - isFirstColumn(index) { - return index === 0; - }, - stageConnectorClass(index, stage) { - let className; - - // If it's the first stage column and only has one job - if (this.isFirstColumn(index) && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } - - return className; - }, - refreshPipelineGraph() { - this.$emit('refreshPipelineGraph'); - }, - /** - * CSS class is applied: - * - if pipeline graph contains only one stage column component - * - * @param {number} index - * @returns {boolean} - */ - shouldAddRightMargin(index) { - return !(index === this.graph.length - 1); - }, - handleClickedDownstream(pipeline, clickedIndex, downstreamNode) { - /** - * Calculates the margin top of the clicked downstream pipeline by - * subtracting the clicked downstream pipelines offsetTop by it's parent's - * offsetTop and then subtracting 15 - */ - this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15); - - /** - * If the expanded trigger is defined and the id is different than the - * pipeline we clicked, then it means we clicked on a sibling downstream link - * and we want to reset the pipeline store. Triggering the reset without - * this condition would mean not allowing downstreams of downstreams to expand - */ - if (this.expandedDownstream?.id !== pipeline.id) { - this.$emit('onResetDownstream', this.pipeline, pipeline); - } - - this.$emit('onClickDownstreamPipeline', pipeline); - }, - calculateMarginTop(downstreamNode, pixelDiff) { - return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`; - }, - hasOnlyOneJob(stage) { - return stage.groups.length === 1; - }, - hasUpstreamColumn(index) { - return index === 0 && this.hasUpstream; - }, - setJob(jobName) { - this.jobName = jobName; - }, - setPipelineExpanded(jobName, expanded) { - if (expanded) { - this.pipelineExpanded = { - jobName, - expanded, - }; - } else { - this.pipelineExpanded = { - expanded, - jobName: '', - }; - } - }, - }, -}; -</script> -<template> - <div class="build-content middle-block js-pipeline-graph"> - <div - class="pipeline-visualization pipeline-graph" - :class="{ 'pipeline-tab-content': !isLinkedPipeline }" - > - <div class="gl-w-full"> - <div class="container-fluid container-limited"> - <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" /> - <pipeline-graph-legacy - v-if="pipelineTypeUpstream" - :type="$options.upstream" - class="d-inline-block upstream-pipeline" - :class="`js-upstream-pipeline-${expandedUpstream.id}`" - :is-loading="false" - :pipeline="expandedUpstream" - :is-linked-pipeline="true" - :mediator="mediator" - @onClickUpstreamPipeline="clickUpstreamPipeline" - @refreshPipelineGraph="requestRefreshPipelineGraph" - /> - - <linked-pipelines-column-legacy - v-if="hasUpstream" - :type="$options.upstream" - :linked-pipelines="upstreamPipelines" - :column-title="__('Upstream')" - :project-id="pipelineProjectId" - @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)" - /> - - <ul - v-if="!isLoading" - :class="{ - 'inline js-has-linked-pipelines': hasDownstream || hasUpstream, - }" - class="stage-column-list align-top" - > - <stage-column-component-legacy - v-for="(stage, index) in graph" - :key="stage.name" - :class="{ - 'has-upstream gl-ml-11': hasUpstreamColumn(index), - 'has-only-one-job': hasOnlyOneJob(stage), - 'gl-mr-26': shouldAddRightMargin(index), - }" - :title="capitalizeStageName(stage.name)" - :groups="stage.groups" - :stage-connector-class="stageConnectorClass(index, stage)" - :is-first-column="isFirstColumn(index)" - :has-upstream="hasUpstream" - :action="stage.status.action" - :job-hovered="jobName" - :pipeline-expanded="pipelineExpanded" - @refreshPipelineGraph="refreshPipelineGraph" - /> - </ul> - - <linked-pipelines-column-legacy - v-if="hasDownstream" - :type="$options.downstream" - :linked-pipelines="downstreamPipelines" - :column-title="__('Downstream')" - :project-id="pipelineProjectId" - @linkedPipelineClick="handleClickedDownstream" - @downstreamHovered="setJob" - @pipelineExpandToggle="setPipelineExpanded" - /> - - <pipeline-graph-legacy - v-if="pipelineTypeDownstream" - :type="$options.downstream" - class="d-inline-block" - :class="`js-downstream-pipeline-${expandedDownstream.id}`" - :is-loading="false" - :pipeline="expandedDownstream" - :is-linked-pipeline="true" - :style="{ 'margin-top': downstreamMarginTop }" - :mediator="mediator" - @onClickDownstreamPipeline="clickDownstreamPipeline" - @refreshPipelineGraph="requestRefreshPipelineGraph" - /> - </div> - </div> - </div> - </div> -</template> 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 a948a57c144..e995d400907 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -4,15 +4,15 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.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 DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql'; +import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql'; import { reportToSentry, reportMessageToSentry } 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 { + calculatePipelineLayersInfo, getQueryHeaders, serializeLoadErrors, toggleQueryPollingByVisibility, @@ -31,7 +31,6 @@ export default { LocalStorageSync, PipelineGraph, }, - mixins: [glFeatureFlagMixin()], inject: { graphqlResourceEtag: { default: '', @@ -50,9 +49,10 @@ export default { return { alertType: null, callouts: [], + computedPipelineInfo: null, currentViewType: STAGE_VIEW, + canRefetchHeaderPipeline: false, pipeline: null, - pipelineLayers: null, showAlert: false, showLinks: false, }; @@ -78,6 +78,26 @@ export default { ); }, }, + headerPipeline: { + query: getPipelineQuery, + // this query is already being called in header_component.vue, which shares the same cache as this component + // the skip here is to prevent sending double network requests on page load + skip() { + return !this.canRefetchHeaderPipeline; + }, + variables() { + return { + fullPath: this.pipelineProjectPath, + iid: this.pipelineIid, + }; + }, + update(data) { + return data.project?.pipeline || {}; + }, + error() { + this.reportFailure({ type: LOAD_FAILURE, skipSentry: true }); + }, + }, pipeline: { context() { return getQueryHeaders(this.graphqlResourceEtag); @@ -178,7 +198,7 @@ export default { return this.$apollo.queries.pipeline.loading && !this.pipeline; }, showGraphViewSelector() { - return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline?.usesNeeds); + return this.pipeline?.usesNeeds; }, }, mounted() { @@ -192,12 +212,16 @@ export default { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); }, methods: { - getPipelineLayers() { - if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) { - this.pipelineLayers = listByLayers(this.pipeline); + getPipelineInfo() { + if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) { + this.computedPipelineInfo = calculatePipelineLayersInfo( + this.pipeline, + this.$options.name, + this.metricsPath, + ); } - return this.pipelineLayers; + return this.computedPipelineInfo; }, handleTipDismissal() { try { @@ -217,6 +241,10 @@ export default { }, refreshPipelineGraph() { this.$apollo.queries.pipeline.refetch(); + + // this will update the status in header_component since they share the same cache + this.canRefetchHeaderPipeline = true; + this.$apollo.queries.headerPipeline.refetch(); }, /* eslint-disable @gitlab/require-i18n-strings */ reportFailure({ type, err = 'No error string passed.', skipSentry = false }) { @@ -262,7 +290,7 @@ export default { v-if="pipeline" :config-paths="configPaths" :pipeline="pipeline" - :pipeline-layers="getPipelineLayers()" + :computed-pipeline-info="getPipelineInfo()" :show-links="showLinks" :view-type="graphViewType" @error="reportFailure" 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 52ee40bd982..d251e0d8bd8 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -2,10 +2,10 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { LOAD_FAILURE } from '../../constants'; import { reportToSentry } from '../../utils'; -import { listByLayers } from '../parsing_utils'; import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants'; import LinkedPipeline from './linked_pipeline.vue'; import { + calculatePipelineLayersInfo, getQueryHeaders, serializeLoadErrors, toggleQueryPollingByVisibility, @@ -138,7 +138,11 @@ export default { }, getPipelineLayers(id) { if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) { - this.pipelineLayers[id] = listByLayers(this.currentPipeline); + this.pipelineLayers[id] = calculatePipelineLayersInfo( + this.currentPipeline, + this.$options.name, + this.configPaths.metricsPath, + ); } return this.pipelineLayers[id]; @@ -223,7 +227,7 @@ export default { class="d-inline-block gl-mt-n2" :config-paths="configPaths" :pipeline="currentPipeline" - :pipeline-layers="getPipelineLayers(pipeline.id)" + :computed-pipeline-info="getPipelineLayers(pipeline.id)" :show-links="showLinks" :is-linked-pipeline="true" :view-type="graphViewType" 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 deleted file mode 100644 index 39baeb6e1c3..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script> -import { reportToSentry } from '../../utils'; -import { UPSTREAM } from './constants'; -import LinkedPipeline from './linked_pipeline.vue'; - -export default { - components: { - LinkedPipeline, - }, - props: { - columnTitle: { - type: String, - required: true, - }, - linkedPipelines: { - type: Array, - required: true, - }, - type: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - }, - computed: { - columnClass() { - const positionValues = { - right: 'gl-ml-11', - left: 'gl-mr-7', - }; - return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; - }, - graphPosition() { - return this.isUpstream ? 'left' : 'right'; - }, - isExpanded() { - return this.pipeline?.isExpanded || false; - }, - isUpstream() { - 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); - }, - onDownstreamHovered(jobName) { - this.$emit('downstreamHovered', jobName); - }, - onPipelineExpandToggle(jobName, expanded) { - // Highlighting only applies to downstream pipelines - if (this.isUpstream) { - return; - } - - this.$emit('pipelineExpandToggle', jobName, expanded); - }, - }, -}; -</script> - -<template> - <div :class="columnClass" class="stage-column linked-pipelines-column"> - <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div> - <div v-if="isUpstream" class="cross-project-triangle"></div> - <ul> - <li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id"> - <linked-pipeline - :class="{ - active: pipeline.isExpanded, - 'left-connector': pipeline.isExpanded && graphPosition === 'left', - }" - :pipeline="pipeline" - :column-title="columnTitle" - :project-id="projectId" - :type="type" - :expanded="isExpanded" - @pipelineClicked="onPipelineClick($event, pipeline, index)" - @downstreamHovered="onDownstreamHovered" - @pipelineExpandToggle="onPipelineExpandToggle" - /> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/pipelines/components/graph/perf_utils.js new file mode 100644 index 00000000000..3737a209f5c --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/perf_utils.js @@ -0,0 +1,50 @@ +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 { reportPerformance } from '../graph_shared/api'; + +export const beginPerfMeasure = () => { + performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START }); +}; + +export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => { + 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 / numGroups, + }, + ], + }; + + reportPerformance(metricsPath, data); + }); +}; 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 deleted file mode 100644 index cbaf07c05cf..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue +++ /dev/null @@ -1,112 +0,0 @@ -<script> -import { isEmpty, escape } from 'lodash'; -import stageColumnMixin from '../../mixins/stage_column_mixin'; -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'; - -export default { - components: { - JobItem, - JobGroupDropdown, - ActionComponent, - }, - mixins: [stageColumnMixin], - props: { - title: { - type: String, - required: true, - }, - groups: { - type: Array, - required: true, - }, - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, - action: { - type: Object, - required: false, - default: () => ({}), - }, - jobHovered: { - type: String, - required: false, - default: '', - }, - pipelineExpanded: { - type: Object, - required: false, - default: () => ({}), - }, - }, - computed: { - hasAction() { - 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)}`; - }, - pipelineActionRequestComplete() { - this.$emit('refreshPipelineGraph'); - }, - }, -}; -</script> -<template> - <li :class="stageConnectorClass" class="stage-column"> - <div class="stage-name position-relative" data-testid="stage-column-title"> - {{ title }} - <action-component - v-if="hasAction" - :action-icon="action.icon" - :tooltip-text="action.title" - :link="action.path" - class="js-stage-action stage-action rounded" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </div> - - <div class="builds-container"> - <ul> - <li - v-for="(group, index) in groups" - :id="groupId(group)" - :key="group.id" - :class="buildConnnectorClass(index)" - class="build" - > - <div class="curve"></div> - - <job-item - v-if="group.size === 1" - :job="group.jobs[0]" - :job-hovered="jobHovered" - :pipeline-expanded="pipelineExpanded" - css-class-job-name="build-content" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - - <job-group-dropdown - v-if="group.size > 1" - :group="group" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </li> - </ul> - </div> - </li> -</template> diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 163b3898c28..3da792cb9df 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,7 +1,10 @@ import { isEmpty } from 'lodash'; import Visibility from 'visibilityjs'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { reportToSentry } from '../../utils'; +import { listByLayers } from '../parsing_utils'; import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils'; +import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils'; const addMulti = (mainPipelineProjectPath, linkedPipeline) => { return { @@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { }; }; +const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => { + const shouldCollectMetrics = Boolean(metricsPath); + + if (shouldCollectMetrics) { + beginPerfMeasure(); + } + + let layers = null; + + try { + layers = listByLayers(pipeline); + + if (shouldCollectMetrics) { + finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath); + } + } catch (err) { + reportToSentry(componentName, err); + } + + return layers; +}; + /* eslint-disable @gitlab/require-i18n-strings */ const getQueryHeaders = (etagResource) => { return { @@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; export { + calculatePipelineLayersInfo, getQueryHeaders, serializeGqlErr, serializeLoadErrors, |