diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
15 files changed, 974 insertions, 341 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js new file mode 100644 index 00000000000..6ece855bcd8 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/accessors.js @@ -0,0 +1,25 @@ +import { get } from 'lodash'; +import { REST, GRAPHQL } from './constants'; + +const accessors = { + [REST]: { + detailsPath: 'details_path', + groupId: 'id', + hasDetails: 'has_details', + pipelineStatus: ['details', 'status'], + sourceJob: ['source_job', 'name'], + }, + [GRAPHQL]: { + detailsPath: 'detailsPath', + groupId: 'name', + hasDetails: 'hasDetails', + pipelineStatus: 'status', + sourceJob: ['sourceJob', 'name'], + }, +}; + +const accessValue = (dataMethod, prop, item) => { + return get(item, accessors[dataMethod][prop]); +}; + +export { accessors, accessValue }; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index a580ee11627..4e9b21a5c55 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -87,10 +87,10 @@ export default { :title="tooltipText" :class="cssClass" :disabled="isDisabled" - class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" + 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!" /> + <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 ba1922b6dae..6f0deccfef6 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -1,3 +1,6 @@ export const DOWNSTREAM = 'downstream'; export const MAIN = 'main'; export const UPSTREAM = 'upstream'; + +export const REST = 'rest'; +export const GRAPHQL = 'graphql'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 16ce279a591..67b2ed3b596 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,35 +1,23 @@ <script> -import { escape, capitalize } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; -import StageColumnComponent from './stage_column_component.vue'; -import GraphWidthMixin from '../../mixins/graph_width_mixin'; +import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; -import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; -import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; +import StageColumnComponent from './stage_column_component.vue'; +import { DOWNSTREAM, MAIN, UPSTREAM } from './constants'; export default { name: 'PipelineGraph', components: { - StageColumnComponent, - GlLoadingIcon, + LinkedGraphWrapper, LinkedPipelinesColumn, + StageColumnComponent, }, - mixins: [GraphWidthMixin, GraphBundleMixin], props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, isLinkedPipeline: { type: Boolean, required: false, default: false, }, - mediator: { + pipeline: { type: Object, required: true, }, @@ -39,12 +27,13 @@ export default { default: MAIN, }, }, - upstream: UPSTREAM, - downstream: DOWNSTREAM, + pipelineTypeConstants: { + DOWNSTREAM, + UPSTREAM, + }, data() { return { - downstreamMarginTop: null, - jobName: null, + hoveredJobName: '', pipelineExpanded: { jobName: '', expanded: false, @@ -52,219 +41,86 @@ export default { }; }, computed: { + downstreamPipelines() { + return this.hasDownstreamPipelines ? this.pipeline.downstream : []; + }, graph() { - return this.pipeline.details?.stages; + return this.pipeline.stages; }, - hasUpstream() { - return ( - this.type !== this.$options.downstream && - this.upstreamPipelines && - this.pipeline.triggered_by !== null - ); + hasDownstreamPipelines() { + return Boolean(this.pipeline?.downstream?.length > 0); }, - upstreamPipelines() { - return this.pipeline.triggered_by; + hasUpstreamPipelines() { + return Boolean(this.pipeline?.upstream?.length > 0); }, - hasDownstream() { + // The two show checks prevent upstream / downstream from showing redundant linked columns + showDownstreamPipelines() { return ( - this.type !== this.$options.upstream && - this.downstreamPipelines && - this.pipeline.triggered.length > 0 + this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM ); }, - downstreamPipelines() { - return this.pipeline.triggered; - }, - expandedUpstream() { + showUpstreamPipelines() { return ( - this.pipeline.triggered_by && - Array.isArray(this.pipeline.triggered_by) && - this.pipeline.triggered_by.find(el => el.isExpanded) + this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM ); }, - 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; + upstreamPipelines() { + return this.hasUpstreamPipelines ? this.pipeline.upstream : []; }, }, 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; + this.hoveredJobName = jobName; }, - setPipelineExpanded(jobName, expanded) { - if (expanded) { - this.pipelineExpanded = { - jobName, - expanded, - }; - } else { - this.pipelineExpanded = { - expanded, - jobName: '', - }; - } + togglePipelineExpanded(jobName, expanded) { + this.pipelineExpanded = { + expanded, + jobName: expanded ? jobName : '', + }; }, }, }; </script> <template> - <div class="build-content middle-block js-pipeline-graph"> + <div class="js-pipeline-graph"> <div - class="pipeline-visualization pipeline-graph" - :class="{ 'pipeline-tab-content': !isLinkedPipeline }" + 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 }" > - <div - :style="{ - paddingLeft: `${graphLeftPadding}px`, - paddingRight: `${graphRightPadding}px`, - }" - > - <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" /> - - <pipeline-graph - 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 - 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" - > + <linked-graph-wrapper> + <template #upstream> + <linked-pipelines-column + v-if="showUpstreamPipelines" + :linked-pipelines="upstreamPipelines" + :column-title="__('Upstream')" + :type="$options.pipelineTypeConstants.UPSTREAM" + @error="emit('error', errorType)" + /> + </template> + <template #main> <stage-column-component - v-for="(stage, index) in graph" + v-for="stage 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)" + :title="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" + :job-hovered="hoveredJobName" :pipeline-expanded="pipelineExpanded" - @refreshPipelineGraph="refreshPipelineGraph" + @refreshPipelineGraph="$emit('refreshPipelineGraph')" /> - </ul> - - <linked-pipelines-column - v-if="hasDownstream" - :type="$options.downstream" - :linked-pipelines="downstreamPipelines" - :column-title="__('Downstream')" - :project-id="pipelineProjectId" - @linkedPipelineClick="handleClickedDownstream" - @downstreamHovered="setJob" - @pipelineExpandToggle="setPipelineExpanded" - /> - - <pipeline-graph - 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> + </template> + <template #downstream> + <linked-pipelines-column + v-if="showDownstreamPipelines" + :linked-pipelines="downstreamPipelines" + :column-title="__('Downstream')" + :type="$options.pipelineTypeConstants.DOWNSTREAM" + @downstreamHovered="setJob" + @pipelineExpandToggle="togglePipelineExpanded" + @error="emit('error', errorType)" + /> + </template> + </linked-graph-wrapper> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue new file mode 100644 index 00000000000..9ca4dc1e27a --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -0,0 +1,265 @@ +<script> +import { escape, capitalize } from 'lodash'; +import { GlLoadingIcon } from '@gitlab/ui'; +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'; + +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; + }, + }, + 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 new file mode 100644 index 00000000000..d98e3aad054 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -0,0 +1,106 @@ +<script> +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +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'; + +export default { + name: 'PipelineGraphWrapper', + components: { + GlAlert, + GlLoadingIcon, + PipelineGraph, + }, + inject: { + pipelineIid: { + default: '', + }, + pipelineProjectPath: { + default: '', + }, + }, + data() { + return { + pipeline: null, + alertType: null, + showAlert: false, + }; + }, + errorTexts: { + [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'), + [DEFAULT]: __('An unknown error occurred while loading this graph.'), + }, + apollo: { + pipeline: { + query: getPipelineDetails, + pollInterval: 10000, + variables() { + return { + projectPath: this.pipelineProjectPath, + iid: this.pipelineIid, + }; + }, + update(data) { + return unwrapPipelineData(this.pipelineProjectPath, data); + }, + error() { + this.reportFailure(LOAD_FAILURE); + }, + }, + }, + computed: { + alert() { + switch (this.alertType) { + case LOAD_FAILURE: + return { + text: this.$options.errorTexts[LOAD_FAILURE], + variant: 'danger', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } + }, + showLoadingIcon() { + /* + Shows the icon only when the graph is empty, not when it is is + being refetched, for instance, on action completion + */ + return this.$apollo.queries.pipeline.loading && !this.pipeline; + }, + }, + mounted() { + toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); + }, + methods: { + hideAlert() { + this.showAlert = false; + }, + refreshPipelineGraph() { + this.$apollo.queries.pipeline.refetch(); + }, + reportFailure(type) { + this.showAlert = true; + this.failureType = type; + }, + }, +}; +</script> +<template> + <div> + <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert"> + {{ alert.text }} + </gl-alert> + <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" /> + <pipeline-graph + v-if="pipeline" + :pipeline="pipeline" + @error="reportFailure" + @refreshPipelineGraph="refreshPipelineGraph" + /> + </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 49591a80752..203d6a12edd 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -44,17 +44,18 @@ export default { type="button" data-toggle="dropdown" data-display="static" - class="dropdown-menu-toggle build-content" + class="dropdown-menu-toggle build-content gl-build-content" > - <ci-icon :status="group.status" /> + <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" /> + <span class="gl-text-truncate mw-70p gl-pl-3"> + {{ group.name }} + </span> + </span> - <span - class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom" - > - {{ group.name }} - </span> - - <span class="dropdown-counter-badge"> {{ group.size }} </span> + <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span> + </div> </button> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4ed0aae0d1e..93ebe02d4e8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -4,6 +4,8 @@ import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import { sprintf } from '~/locale'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import { accessValue } from './accessors'; +import { REST } from './constants'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -41,6 +43,11 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [delayedJobMixin], + inject: { + dataMethod: { + default: REST, + }, + }, props: { job: { type: Object, @@ -71,10 +78,15 @@ export default { boundary() { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; }, + detailsPath() { + return accessValue(this.dataMethod, 'detailsPath', this.status); + }, + hasDetails() { + return accessValue(this.dataMethod, 'hasDetails', this.status); + }, status() { return this.job && this.job.status ? this.job.status : {}; }, - tooltipText() { const textBuilder = []; const { name: jobName } = this.job; @@ -129,19 +141,23 @@ export default { }; </script> <template> - <div class="ci-job-component" data-qa-selector="job_item_container"> + <div + class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" + data-qa-selector="job_item_container" + > <gl-link - v-if="status.has_details" + v-if="hasDetails" v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" - :href="status.details_path" + :href="detailsPath" :title="tooltipText" :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item" + 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" /> + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> </gl-link> <div @@ -149,11 +165,11 @@ export default { v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" :title="tooltipText" :class="jobClasses" - class="js-job-component-tooltip non-details-job-component" + class="js-job-component-tooltip non-details-job-component menu-item" data-testid="job-without-link" @mouseout="hideTooltips" > - <job-name-component :name="job.name" :status="job.status" /> + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> </div> <action-component diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 1b71949784a..23a38fc053e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -16,18 +16,22 @@ export default { 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"> - <ci-icon :status="status" /> - <span class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom"> + <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center"> + <ci-icon :size="iconSize" :status="status" /> + <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block"> {{ name }} </span> </span> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 11f06a25984..1a179de64cd 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -2,7 +2,8 @@ import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; -import { UPSTREAM, DOWNSTREAM } from './constants'; +import { accessValue } from './accessors'; +import { DOWNSTREAM, REST, UPSTREAM } from './constants'; export default { directives: { @@ -14,28 +15,43 @@ export default { GlLink, GlLoadingIcon, }, + inject: { + dataMethod: { + default: REST, + }, + }, props: { columnTitle: { type: String, required: true, }, - pipeline: { - type: Object, + expanded: { + type: Boolean, required: true, }, - projectId: { - type: Number, + pipeline: { + type: Object, required: true, }, type: { type: String, required: true, }, - }, - data() { - return { - expanded: false, - }; + /* + The next two props will be removed or required + once the graph transition is done. + See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043 + */ + isLoading: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: false, + default: -1, + }, }, computed: { tooltipText() { @@ -46,7 +62,7 @@ export default { return `js-linked-pipeline-${this.pipeline.id}`; }, pipelineStatus() { - return this.pipeline.details.status; + return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline); }, projectName() { return this.pipeline.project.name; @@ -68,6 +84,9 @@ export default { } return __('Multi-project'); }, + pipelineIsLoading() { + return Boolean(this.isLoading || this.pipeline.isLoading); + }, isDownstream() { return this.type === DOWNSTREAM; }, @@ -75,12 +94,15 @@ export default { return this.type === UPSTREAM; }, isSameProject() { - return this.projectId === this.pipeline.project.id; + return this.projectId > -1 + ? this.projectId === this.pipeline.project.id + : !this.pipeline.multiproject; + }, + sourceJobName() { + return accessValue(this.dataMethod, 'sourceJob', this.pipeline); }, sourceJobInfo() { - return this.isDownstream - ? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name }) - : ''; + return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; }, expandedIcon() { if (this.isUpstream) { @@ -94,16 +116,15 @@ export default { }, methods: { onClickLinkedPipeline() { - this.$root.$emit('bv::hide::tooltip', this.buttonId); - this.expanded = !this.expanded; + this.hideTooltips(); this.$emit('pipelineClicked', this.$refs.linkedPipeline); - this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded); + this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded); }, hideTooltips() { this.$root.$emit('bv::hide::tooltip'); }, onDownstreamHovered() { - this.$emit('downstreamHovered', this.pipeline.source_job.name); + this.$emit('downstreamHovered', this.sourceJobName); }, onDownstreamHoverLeave() { this.$emit('downstreamHovered', ''); @@ -113,10 +134,10 @@ export default { </script> <template> - <li + <div ref="linkedPipeline" v-gl-tooltip - class="linked-pipeline build" + class="linked-pipeline build gl-pipeline-job-width" :title="tooltipText" :class="{ 'downstream-pipeline': isDownstream }" data-qa-selector="child_pipeline" @@ -129,8 +150,9 @@ export default { > <div class="gl-display-flex"> <ci-status - v-if="!pipeline.isLoading" + v-if="!pipelineIsLoading" :status="pipelineStatus" + :size="24" css-classes="gl-top-0 gl-pr-2" /> <div v-else class="gl-pr-2"><gl-loading-icon inline /></div> @@ -153,10 +175,10 @@ 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" - data-testid="expandPipelineButton" + data-testid="expand-pipeline-button" data-qa-selector="expand_pipeline_button" @click="onClickLinkedPipeline" /> </div> - </li> + </div> </template> 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 2ca33e6d33e..7d333087874 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -1,10 +1,14 @@ <script> +import getPipelineDetails from '../../graphql/queries/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'; export default { components: { LinkedPipeline, + PipelineGraph: () => import('./graph_component.vue'), }, props: { columnTitle: { @@ -19,11 +23,22 @@ export default { type: String, required: true, }, - projectId: { - type: Number, - required: true, - }, }, + data() { + return { + currentPipeline: null, + loadingPipelineId: null, + pipelineExpanded: false, + }; + }, + titleClasses: [ + 'gl-font-weight-bold', + 'gl-pipeline-job-width', + 'gl-text-truncate', + 'gl-line-height-36', + 'gl-pl-3', + 'gl-mb-5', + ], computed: { columnClass() { const positionValues = { @@ -35,14 +50,69 @@ export default { graphPosition() { return this.isUpstream ? 'left' : 'right'; }, - // Refactor string match when BE returns Upstream/Downstream indicators isUpstream() { return this.type === UPSTREAM; }, + computedTitleClasses() { + const positionalClasses = this.isUpstream + ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding'] + : []; + + return [...this.$options.titleClasses, ...positionalClasses]; + }, }, methods: { - onPipelineClick(downstreamNode, pipeline, index) { - this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); + getPipelineData(pipeline) { + const projectPath = pipeline.project.fullPath; + + this.$apollo.addSmartQuery('currentPipeline', { + query: getPipelineDetails, + pollInterval: 10000, + variables() { + return { + projectPath, + iid: pipeline.iid, + }; + }, + update(data) { + return unwrapPipelineData(projectPath, data); + }, + result() { + this.loadingPipelineId = null; + }, + error() { + this.$emit('error', LOAD_FAILURE); + }, + }); + + toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline); + }, + isExpanded(id) { + return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id); + }, + isLoadingPipeline(id) { + return this.loadingPipelineId === id; + }, + onPipelineClick(pipeline) { + /* If the clicked pipeline has been expanded already, close it, clear, exit */ + if (this.currentPipeline?.id === pipeline.id) { + this.pipelineExpanded = false; + this.currentPipeline = null; + return; + } + + /* Set the loading id */ + this.loadingPipelineId = pipeline.id; + + /* + Expand the pipeline. + If this was not a toggle close action, and + it was already showing a different pipeline, then + this will be a no-op, but that doesn't matter. + */ + this.pipelineExpanded = true; + + this.getPipelineData(pipeline); }, onDownstreamHovered(jobName) { this.$emit('downstreamHovered', jobName); @@ -60,25 +130,40 @@ export default { </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> - <linked-pipeline - v-for="(pipeline, index) in linkedPipelines" - :key="pipeline.id" - :class="{ - active: pipeline.isExpanded, - 'left-connector': pipeline.isExpanded && graphPosition === 'left', - }" - :pipeline="pipeline" - :column-title="columnTitle" - :project-id="projectId" - :type="type" - @pipelineClicked="onPipelineClick($event, pipeline, index)" - @downstreamHovered="onDownstreamHovered" - @pipelineExpandToggle="onPipelineExpandToggle" - /> - </ul> + <div class="gl-display-flex"> + <div :class="columnClass" class="linked-pipelines-column"> + <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses"> + {{ columnTitle }} + </div> + <ul class="gl-pl-0"> + <li + v-for="pipeline in linkedPipelines" + :key="pipeline.id" + class="gl-display-flex gl-mb-4" + :class="{ 'gl-flex-direction-row-reverse': isUpstream }" + > + <linked-pipeline + class="gl-display-inline-block" + :is-loading="isLoadingPipeline(pipeline.id)" + :pipeline="pipeline" + :column-title="columnTitle" + :type="type" + :expanded="isExpanded(pipeline.id)" + @downstreamHovered="onDownstreamHovered" + @pipelineClicked="onPipelineClick(pipeline)" + @pipelineExpandToggle="onPipelineExpandToggle" + /> + <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block"> + <pipeline-graph + v-if="currentPipeline" + :type="type" + class="d-inline-block gl-mt-n2" + :pipeline="currentPipeline" + :is-linked-pipeline="true" + /> + </div> + </li> + </ul> + </div> </div> </template> 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 new file mode 100644 index 00000000000..7d371b33220 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue @@ -0,0 +1,87 @@ +<script> +import LinkedPipeline from './linked_pipeline.vue'; +import { UPSTREAM } from './constants'; + +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; + }, + }, + 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/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index a75ec585b95..b9bddc94ce4 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,17 +1,19 @@ <script> -import { isEmpty, escape } from 'lodash'; -import stageColumnMixin from '../../mixins/stage_column_mixin'; +import { capitalize, escape, isEmpty } from 'lodash'; +import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; +import { GRAPHQL } from './constants'; +import { accessValue } from './accessors'; export default { components: { - JobItem, - JobGroupDropdown, ActionComponent, + JobGroupDropdown, + JobItem, + MainGraphWrapper, }, - mixins: [stageColumnMixin], props: { title: { type: String, @@ -21,16 +23,6 @@ export default { type: Array, required: true, }, - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, action: { type: Object, required: false, @@ -47,62 +39,68 @@ export default { default: () => ({}), }, }, + titleClasses: [ + 'gl-font-weight-bold', + 'gl-pipeline-job-width', + 'gl-text-truncate', + 'gl-line-height-36', + 'gl-pl-3', + ], computed: { + formattedTitle() { + return capitalize(escape(this.title)); + }, hasAction() { return !isEmpty(this.action); }, }, methods: { + getGroupId(group) { + return accessValue(GRAPHQL, 'groupId', group); + }, 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"> - {{ 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> + <main-graph-wrapper> + <template #stages> + <div + data-testid="stage-column-title" + class="gl-display-flex gl-justify-content-space-between gl-relative" + :class="$options.titleClasses" + > + <div>{{ formattedTitle }}</div> + <action-component + v-if="hasAction" + :action-icon="action.icon" + :tooltip-text="action.title" + :link="action.path" + class="js-stage-action stage-action rounded" + @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" + /> + </div> + </template> + <template #jobs> + <div + v-for="group in groups" + :id="groupId(group)" + :key="getGroupId(group)" + data-testid="stage-column-group" + class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + > + <job-item + v-if="group.size === 1" + :job="group.jobs[0]" + :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" + css-class-job-name="gl-build-content" + @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" + /> + <job-group-dropdown v-else :group="group" /> + </div> + </template> + </main-graph-wrapper> </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 new file mode 100644 index 00000000000..258b6bf6b6d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue @@ -0,0 +1,108 @@ +<script> +import { isEmpty, escape } from 'lodash'; +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'; + +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); + }, + }, + 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 new file mode 100644 index 00000000000..32588feb426 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -0,0 +1,57 @@ +import Visibility from 'visibilityjs'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { unwrapStagesWithNeeds } from '../unwrapping_utils'; + +const addMulti = (mainPipelineProjectPath, linkedPipeline) => { + return { + ...linkedPipeline, + multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath, + }; +}; + +const transformId = linkedPipeline => { + return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) }; +}; + +const unwrapPipelineData = (mainPipelineProjectPath, data) => { + if (!data?.project?.pipeline) { + return null; + } + + const { pipeline } = data.project; + + const { + upstream, + downstream, + stages: { nodes: stages }, + } = pipeline; + + const nodes = unwrapStagesWithNeeds(stages); + + return { + ...pipeline, + id: getIdFromGraphQLId(pipeline.id), + stages: nodes, + upstream: upstream + ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId) + : [], + downstream: downstream + ? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId) + : [], + }; +}; + +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 }; |