Commit 0a2c5b45 authored by Sean McGivern's avatar Sean McGivern

Merge branch '8688-recursive-pipelines' into 'master'

Recursively expanding upstream/downstream pipelines inline

Closes #8688

See merge request gitlab-org/gitlab-ee!9073
parents 32d37bc9 c11a3a78
......@@ -2,8 +2,8 @@ import Vue from 'vue';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import PipelinesMediator from 'ee/pipelines/pipeline_details_mediator';
import pipelineGraph from 'ee/pipelines/components/graph/graph_component.vue';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import GraphEEMixin from 'ee/pipelines/mixins/graph_pipeline_bundle_mixin'; // eslint-disable-line import/order
......@@ -29,35 +29,19 @@ export default () => {
mediator,
};
},
methods: {
requestRefreshPipelineGraph() {
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator
.refreshPipeline()
.catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
return createElement('pipeline-graph', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
// EE-only start
triggeredPipelines: this.mediator.store.state.triggeredPipelines,
triggered: this.mediator.store.state.triggered,
triggeredByPipelines: this.mediator.store.state.triggeredByPipelines,
triggeredBy: this.mediator.store.state.triggeredBy,
// EE-only end
mediator: this.mediator,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
// EE-only start
refreshTriggeredPipelineGraph: this.mediator.refreshTriggeredByPipelineGraph,
refreshTriggeredByPipelineGraph: this.mediator.refreshTriggeredByPipelineGraph,
onClickTriggeredBy: pipeline => this.clickTriggeredBy(pipeline),
onClickTriggered: pipeline => this.clickTriggered(pipeline),
// EE-only end
onClickTriggeredBy: (parentPipeline, pipeline) =>
this.clickTriggeredByPipeline(parentPipeline, pipeline),
onClickTriggered: (parentPipeline, pipeline) =>
this.clickTriggeredPipeline(parentPipeline, pipeline),
},
});
},
......
......@@ -2,8 +2,8 @@ import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineService from '~/pipelines/services/pipeline_service';
import PipelineStore from 'ee/pipelines/stores/pipeline_store'; // eslint-disable-line import/order
import PipelineService from 'ee/pipelines/services/pipeline_service'; // eslint-disable-line import/order
export default class pipelinesMediator {
constructor(options = {}) {
......
<script>
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue';
import EEGraphMixin from 'ee/pipelines/mixins/graph_component_mixin';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import GraphMixin from '~/pipelines/mixins/graph_component_mixin';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue';
import GraphEEMixin from 'ee/pipelines/mixins/graph_pipeline_bundle_mixin';
export default {
name: 'PipelineGraph',
components: {
LinkedPipelinesColumn,
StageColumnComponent,
GlLoadingIcon,
LinkedPipelinesColumn,
},
mixins: [EEGraphMixin],
mixins: [GraphMixin, GraphEEMixin],
props: {
isLoading: {
type: Boolean,
......@@ -21,35 +23,72 @@ export default {
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 {
triggeredTopIndex: 1,
};
},
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
hasTriggeredBy() {
return (
this.type !== this.$options.downstream &&
this.triggeredByPipelines &&
this.pipeline.triggered_by !== null
);
},
triggeredByPipelines() {
return this.pipeline.triggered_by;
},
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
hasTriggered() {
return (
this.type !== this.$options.upstream &&
this.triggeredPipelines &&
this.pipeline.triggered.length > 0
);
},
isFirstColumn(index) {
return index === 0;
triggeredPipelines() {
return this.pipeline.triggered;
},
expandedTriggeredBy() {
return (
this.pipeline.triggered_by &&
_.isArray(this.pipeline.triggered_by) &&
this.pipeline.triggered_by.find(el => el.isExpanded)
);
},
expandedTriggered() {
return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
},
stageConnectorClass(index, stage) {
let className;
// If it's the first stage column and only has one job
if (index === 0 && stage.groups.length === 1) {
className = 'no-margin';
} else if (index > 0) {
// If it is not the first column
className = 'left-margin';
}
return className;
/**
* Calculates the margin top of the clicked downstream pipeline by
* adding the height of each linked pipeline and the margin
*/
marginTop() {
return `${this.triggeredTopIndex * 52}px`;
},
},
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
methods: {
handleClickedDownstream(pipeline, clickedIndex) {
this.triggeredTopIndex = clickedIndex;
this.$emit('onClickTriggered', this.pipeline, pipeline);
},
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
......@@ -59,30 +98,35 @@ export default {
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div>
<div
class="pipeline-visualization pipeline-graph"
:class="{ 'pipeline-tab-content': !isLinkedPipeline }"
>
<div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div>
<ul v-if="shouldRenderTriggeredByPipeline" class="d-inline-block upstream-pipeline align-top">
<stage-column-component
v-for="(stage, indexUpstream) in triggeredByGraph"
:key="stage.name"
:class="{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexUpstream, stage)"
:is-first-column="isFirstColumn(indexUpstream)"
@refreshPipelineGraph="refreshTriggeredByPipelineGraph"
<pipeline-graph
v-if="type !== $options.downstream && expandedTriggeredBy"
type="upstream"
class="d-inline-block upstream-pipeline"
:class="`js-upstream-pipeline-${expandedTriggeredBy.id}`"
:is-loading="false"
:pipeline="expandedTriggeredBy"
:is-linked-pipeline="true"
:mediator="mediator"
@onClickTriggeredBy="
(parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')"
graph-position="left"
@linkedPipelineClick="pipeline => $emit('onClickTriggeredBy', pipeline)"
@linkedPipelineClick="
linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
"
/>
<ul
......@@ -117,24 +161,21 @@ export default {
@linkedPipelineClick="handleClickedDownstream"
/>
<ul
v-if="shouldRenderTriggeredPipeline"
class="d-inline-block downstream-pipeline position-relative align-top"
<pipeline-graph
v-if="type !== $options.upstream && expandedTriggered"
type="downstream"
class="d-inline-block"
:class="`js-downstream-pipeline-${expandedTriggered.id}`"
:is-loading="false"
:pipeline="expandedTriggered"
:is-linked-pipeline="true"
:style="{ 'margin-top': marginTop }"
>
<stage-column-component
v-for="(stage, indexDownstream) in triggeredGraph"
:key="stage.name"
:class="{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexDownstream, stage)"
:is-first-column="isFirstColumn(indexDownstream)"
@refreshPipelineGraph="refreshTriggeredPipelineGraph"
:mediator="mediator"
@onClickTriggered="
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</ul>
</div>
</div>
</template>
......@@ -12,30 +12,23 @@ export default {
GlButton,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelineStatus: {
pipeline: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
},
buttonId() {
return `js-linked-pipeline-${this.pipelineId}`;
return `js-linked-pipeline-${this.pipeline.id}`;
},
pipelineStatus() {
return this.pipeline.details.status;
},
projectName() {
return this.pipeline.project.name;
},
},
methods: {
......@@ -57,10 +50,9 @@ export default {
class="js-linked-pipeline-content linked-pipeline-content"
@click="onClickLinkedPipeline"
>
<gl-loading-icon v-if="isLoading" class="js-linked-pipeline-loading d-inline" />
<ci-status v-else :status="pipelineStatus" class="js-linked-pipeline-status" />
<ci-status :status="pipelineStatus" class="js-linked-pipeline-status" />
<span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipelineId }} </span>
<span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipeline.id }} </span>
</gl-button>
</li>
</template>
......@@ -37,14 +37,10 @@ export default {
:key="pipeline.id"
:class="{
'flat-connector-before': index === 0 && graphPosition === 'right',
active: pipeline.isExpanded || pipeline.isLoading,
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
:pipeline-id="pipeline.id"
:project-name="pipeline.project.name"
:pipeline-status="pipeline.details.status"
:pipeline-path="pipeline.path"
:is-loading="pipeline.isLoading"
:pipeline="pipeline"
@pipelineClicked="$emit('linkedPipelineClick', pipeline, index)"
/>
</ul>
......
export default {
triggeredPipelines: 'triggeredPipelines',
triggeredByPipelines: 'triggeredByPipelines',
triggeredBy: 'triggeredBy',
triggered: 'triggered',
};
import _ from 'underscore';
export default {
props: {
triggered: {
type: Object,
required: false,
default: () => ({}),
},
triggeredBy: {
type: Object,
required: false,
default: () => ({}),
},
triggeredByPipelines: {
type: Array,
required: false,
default: () => [],
},
triggeredPipelines: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
triggeredTopIndex: 1,
};
},
computed: {
triggeredGraph() {
return this.triggered && this.triggered.details && this.triggered.details.stages;
},
triggeredByGraph() {
return this.triggeredBy && this.triggeredBy.details && this.triggeredBy.details.stages;
},
hasTriggered() {
return this.triggeredPipelines.length > 0;
},
hasTriggeredBy() {
return this.triggeredByPipelines.length > 0;
},
shouldRenderTriggeredPipeline() {
return !this.isLoading && !_.isEmpty(this.triggered);
},
shouldRenderTriggeredByPipeline() {
return !this.isLoading && !_.isEmpty(this.triggeredBy);
},
/**
* Calculates the margin top of the clicked downstream pipeline by
* adding the height of each linked pipeline and the margin
*/
marginTop() {
return `${this.triggeredTopIndex * 52}px`;
},
},
methods: {
refreshTriggeredPipelineGraph() {
this.$emit('refreshTriggeredPipelineGraph');
},
refreshTriggeredByPipelineGraph() {
this.$emit('refreshTriggeredByPipelineGraph');
},
handleClickedDownstream(pipeline, clickedIndex) {
this.triggeredTopIndex = clickedIndex;
this.$emit('onClickTriggered', pipeline);
},
},
};
import pipelinesKeys from 'ee/pipelines/constants';
import flash from '~/flash';
import { __ } from '~/locale';
export default {
methods: {
......@@ -13,30 +14,35 @@ export default {
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
clickPipeline(method, storeKey, resetStoreKey, pipeline, pollKey) {
clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) {
if (!pipeline.isExpanded) {
this.mediator[method](pipeline);
this.mediator.store[openMethod](parentPipeline, pipeline);
} else {
this.mediator.resetPipeline(storeKey, pipeline, resetStoreKey, pollKey);
this.mediator.store[closeMethod](pipeline);
}
},
clickTriggered(triggered) {
clickTriggeredByPipeline(parentPipeline, pipeline) {
this.clickPipeline(
'fetchTriggeredPipeline',
pipelinesKeys.triggeredPipelines,
pipelinesKeys.triggered,
triggered,
'pollTriggered',
parentPipeline,
pipeline,
'openTriggeredByPipeline',
'closeTriggeredByPipeline',
);
},
clickTriggeredBy(triggeredBy) {
clickTriggeredPipeline(parentPipeline, pipeline) {
this.clickPipeline(
'fetchTriggeredByPipeline',
pipelinesKeys.triggeredByPipelines,
pipelinesKeys.triggeredBy,
triggeredBy,
'pollTriggeredBy',
parentPipeline,
pipeline,
'openTriggeredPipeline',
'closeTriggeredPipeline',
);
},
requestRefreshPipelineGraph() {
// When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator
.refreshPipeline()
.catch(() => flash(__('An error occurred while making the request.')));
},
},
};
import CePipelineMediator from '~/pipelines/pipeline_details_mediator';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import PipelineService from 'ee/pipelines/services/pipeline_service';
/**
* Extends CE mediator with the logic to handle the upstream/downstream pipelines
*/
export default class EePipelineMediator extends CePipelineMediator {
/**
* Requests the clicked downstream pipeline pipeline
*
* @param {Object} pipeline
*/
fetchTriggeredPipeline(pipeline) {
if (this.pollTriggered) {
this.pollTriggered.stop();
this.pollTriggered = null;
}
this.store.requestTriggeredPipeline(pipeline);
this.pollTriggered = new Poll({
resource: PipelineService,
method: 'getUpstreamDownstream',
data: pipeline.path,
successCallback: ({ data }) => this.store.receiveTriggeredPipelineSuccess(pipeline, data),
errorCallback: () => {
this.store.receiveTriggeredPipelineError(pipeline);
createFlash(
__('An error occured while fetching this downstream pipeline. Please try again'),
);
},
});
this.pollTriggered.makeRequest();
}
refreshTriggeredPipelineGraph() {
this.pollTriggered.stop();
this.pollTriggered.restart();
}
/**
* Requests the clicked upstream pipeline pipeline
* @param {*} pipeline
*/
fetchTriggeredByPipeline(pipeline) {
if (this.pollTriggeredBy) {
this.pollTriggeredBy.stop();
this.pollTriggeredBy = null;
}
this.store.requestTriggeredByPipeline(pipeline);
this.pollTriggeredBy = new Poll({
resource: PipelineService,
method: 'getUpstreamDownstream',
data: pipeline.path,
successCallback: ({ data }) => this.store.receiveTriggeredByPipelineSuccess(pipeline, data),
errorCallback: () => {
this.store.receiveTriggeredByPipelineError(pipeline);
createFlash(__('An error occured while fetching this upstream pipeline. Please try again'));
},
});
this.pollTriggeredBy.makeRequest();
}
refreshTriggeredByPipelineGraph() {
this.pollTriggeredBy.stop();
this.pollTriggeredBy.restart();
}
resetPipeline(storeKey, pipeline, resetStoreKey, pollKey) {
this[pollKey].stop();
this.store.closePipeline(storeKey, pipeline, resetStoreKey);
}
}
import axios from '~/lib/utils/axios_utils';
import CePipelineService from '~/pipelines/services/pipeline_service';
export default class PipelineStore extends CePipelineService {
static getUpstreamDownstream(endpoint) {
return axios.get(`${endpoint}.json`);
}
}
import Vue from 'vue';
import _ from 'underscore';
import CePipelineStore from '~/pipelines/stores/pipeline_store';
import pipelinesKeys from '../constants';
/**
* Extends CE store with the logic to handle the upstream/downstream pipelines
*/
export default class PipelineStore extends CePipelineStore {
constructor() {
super();
// Stores the dowsntream collapsed pipelines
// with basic info sent in the main request
this.state.triggeredPipelines = [];
// Stores the upstream collapsed pipelines
// with basic info sent in the main request
this.state.triggeredByPipelines = [];
// Visible downstream pipeline
this.state.triggered = {};
// Visible upstream pipeline
this.state.triggeredBy = {};
}
/**
* For the triggered pipelines, parses them to add `isLoading` and `isExpanded` keys
* For the triggered pipelines adds the `isExpanded` key
*
* For the triggered_by pipeline, parsed the object to add `isLoading` and `isExpanded` keys
* For the triggered_by pipeline adds the `isExpanded` key
* and saves it as an array
*
* @param {Object} pipeline
......@@ -32,216 +17,138 @@ export default class PipelineStore extends CePipelineStore {
storePipeline(pipeline = {}) {
super.storePipeline(pipeline);
if (pipeline.triggered && pipeline.triggered.length) {
this.state.triggeredPipelines = pipeline.triggered.map(triggered => {
// because we are polling we need to make sure we do not hijack user's clicks.
const oldPipeline = this.state.triggeredPipelines.find(
oldValue => oldValue.id === triggered.id,
);
if (pipeline.triggered_by) {
this.state.pipeline.triggered_by = [pipeline.triggered_by];
return Object.assign({}, triggered, {
isExpanded: oldPipeline ? oldPipeline.isExpanded : false,
isLoading: oldPipeline ? oldPipeline.isLoading : false,
});
});
this.parseTriggeredByPipelines(this.state.pipeline.triggered_by[0]);
}
if (pipeline.triggered_by) {
this.state.triggeredByPipelines = [
Object.assign({}, pipeline.triggered_by, {
isExpanded: this.state.triggeredByPipelines.length
? this.state.triggeredByPipelines[0].isExpanded
: false,
isLoading: this.state.triggeredByPipelines.length
? this.state.triggeredByPipelines[0].isLoading
: false,
}),
];
if (pipeline.triggered && pipeline.triggered.length) {
this.state.pipeline.triggered.forEach(el => this.parseTriggeredPipelines(el));
}
}
//
// Downstream pipeline's methods
//
/**
* Called when the user clicks on a pipeline that was triggered by the main one.
* Recursiverly parses the triggered by pipelines.
*
* Resets isExpanded and isLoading props for all triggered (downstream) pipelines
* Sets isLoading to true for the requested one.
* Sets triggered_by as an array, there is always only 1 triggered_by pipeline.
* Adds key `isExpanding`
* Keeps old isExpading value due to polling
*
* @param {Array} parentPipeline
* @param {Object} pipeline
*/
requestTriggeredPipeline(pipeline) {
this.updateStoreOnRequest(pipelinesKeys.triggeredPipelines, pipeline);
parseTriggeredByPipelines(pipeline) {
// keep old value in case it's opened because we're polling
Vue.set(pipeline, 'isExpanded', pipeline.isExpanded || false);
if (pipeline.triggered_by) {
if (!_.isArray(pipeline.triggered_by)) {
Object.assign(pipeline, { triggered_by: [pipeline.triggered_by] });
}
this.parseTriggeredByPipelines(pipeline.triggered_by[0]);
}
}
/**
* Called when we receive success callback for the downstream pipeline requested.
*
* Updates loading state for the request pipeline
* Updates the visible pipeline with the response
*
* Recursively parses the triggered pipelines
* @param {Array} parentPipeline
* @param {Object} pipeline
* @param {Object} response
*/
receiveTriggeredPipelineSuccess(pipeline, response) {
this.updatePipeline(
pipelinesKeys.triggeredPipelines,
pipeline,
{ isLoading: false, isExpanded: true },
pipelinesKeys.triggered,
response,
);
parseTriggeredPipelines(pipeline) {
// keep old value in case it's opened because we're polling
Vue.set(pipeline, 'isExpanded', pipeline.isExpanded || false);
if (pipeline.triggered && pipeline.triggered.length > 0) {
pipeline.triggered.forEach(el => this.parseTriggeredPipelines(el));
}
}
/**
* Called when we receive an error callback for the downstream pipeline requested
* Resets the loading state + collpased state
* Resets triggered pipeline
* Recursively resets all triggered by pipelines
*
* @param {Object} pipeline
*/
receiveTriggeredPipelineError(pipeline) {
this.updatePipeline(
pipelinesKeys.triggeredPipelines,
pipeline,
{ isLoading: false, isExpanded: false },
pipelinesKeys.triggered,
{},
);
}
resetTriggeredByPipeline(parentPipeline, pipeline) {
parentPipeline.triggered_by.forEach(el => PipelineStore.closePipeline(el));
//
// Upstream pipeline's methods
//
if (pipeline.triggered_by && pipeline.triggered_by) {
this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by);
}
}
/**
* Called when the user clicks on the pipeline that triggered the main one.
*
* Handle the request for the upstream pipeline
* Updates the given pipeline with isLoading: true and isExpanded: true
*
* Opens the clicked pipeline and closes all other ones.
* @param {Object} pipeline
*/
requestTriggeredByPipeline(pipeline) {
this.updateStoreOnRequest(pipelinesKeys.triggeredByPipelines, pipeline);
openTriggeredByPipeline(parentPipeline, pipeline) {
// first we need to reset all triggeredBy pipelines
this.resetTriggeredByPipeline(parentPipeline, pipeline);
PipelineStore.openPipeline(pipeline);
}
/**
* Success callback for the upstream pipeline received
* On click, will close the given pipeline and all nested triggered by pipelines
*
* @param {Object} pipeline
* @param {Object} response
*/
receiveTriggeredByPipelineSuccess(pipeline, response) {
this.updatePipeline(
pipelinesKeys.triggeredByPipelines,
pipeline,
{ isLoading: false, isExpanded: true },
pipelinesKeys.triggeredBy,
response,
);
}
closeTriggeredByPipeline(pipeline) {
PipelineStore.closePipeline(pipeline);
/**
* Error callback for the upstream callback
* @param {Object} pipeline
*/
receiveTriggeredByPipelineError(pipeline) {
this.updatePipeline(
pipelinesKeys.triggeredByPipelines,
pipeline,
{ isLoading: false, isExpanded: false },
pipelinesKeys.triggeredBy,
{},
);
if (pipeline.triggered_by && pipeline.triggered_by.length) {
pipeline.triggered_by.forEach(triggeredBy => this.closeTriggeredByPipeline(triggeredBy));
}
}
//
// Common utils between upstream & dowsntream pipelines
//
/**
* Adds isLoading and isCollpased keys to the given pipeline
*
* Used to know when to render the spinning icon
* and the blue background when the pipeline is expanded.
* Recursively closes all triggered pipelines for the given one.
*
* @param {Object} pipeline
* @returns {Object}
*/
static parsePipeline(pipeline) {
return Object.assign({}, pipeline, {
isExpanded: false,
isLoading: false,
});
resetTriggeredPipelines(parentPipeline, pipeline) {
parentPipeline.triggered.forEach(el => this.closePipeline(el));
if (pipeline.triggered && pipeline.triggered.length) {
pipeline.triggered.forEach(el => this.resetTriggeredPipelines(pipeline, el));
}
}
/**
* Returns the index of the upstream/downstream that matches the given ID
* Opens the clicked triggered pipeline and closes all other ones.
*
* @param {Object} pipeline
* @returns {Number}
*/
getPipelineIndex(storeKey, pipelineId) {
return this.state[storeKey].findIndex(triggered => triggered.id === pipelineId);
openTriggeredPipeline(parentPipeline, pipeline) {
this.resetTriggeredPipelines(parentPipeline, pipeline);
PipelineStore.openPipeline(pipeline);
}
/**
* Updates the pipelines to reflect which one was requested.
* It sets isLoading to true and isExpanded to false
*
* @param {String} storeKey which property to update: `triggeredPipelines|triggeredByPipelines`
* @param {Object} pipeline the requested pipeline
* On click, will close the given pipeline and all the nested triggered ones
* @param {Object} pipeline
*/
updateStoreOnRequest(storeKey, pipeline) {
this.state[storeKey] = this.state[storeKey].map(triggered => {
if (triggered.id === pipeline.id) {
return Object.assign({}, triggered, { isLoading: true, isExpanded: true });
closeTriggeredPipeline(pipeline) {
PipelineStore.closePipeline(pipeline);
if (pipeline.triggered && pipeline.triggered.length) {
pipeline.triggered.forEach(triggered => this.closeTriggeredPipeline(triggered));
}
// reset the others, in case another was one opened
return PipelineStore.parsePipeline(triggered);
});
}
/**
* Updates a single pipeline with the new props and the visible pipeline
* Used for success and error callbacks for both upstream and downstream requests.
*
* @param {String} storeKey Which array needs to be updated: `triggeredPipelines|triggeredByPipelines`
* @param {Object} pipeline Which pipeline should be updated
* @param {Object} props The new properties to be updated for the given pipeline
* @param {String} visiblePipelineKey Which visible pipeline needs to be updated: `triggered|triggeredBy`
* @param {Object} visiblePipeline The new visible pipeline value
* Utility function, Closes the given pipeline
* @param {Object} pipeline
*/
updatePipeline(storeKey, pipeline, props, visiblePipelineKey, visiblePipeline = {}) {
this.state[storeKey].splice(
this.getPipelineIndex(storeKey, pipeline.id),
1,
Object.assign({}, pipeline, props),
);
this.state[visiblePipelineKey] = visiblePipeline;
static closePipeline(pipeline) {
Vue.set(pipeline, 'isExpanded', false);
}
/**
* When the user clicks on a non collapsed pipeline we need to close it
*
* @param {String} storeKey Which array needs to be updated: `triggeredPipelines|triggeredByPipelines`
* @param {Object} pipeline Which pipeline should be updated
* @param {String} visiblePipelineKey Which visible pipeline needs to be updated: `triggered|triggeredBy`
* Utility function, Opens the given pipeline
* @param {Object} pipeline
*/
closePipeline(storeKey, pipeline, visiblePipelineKey) {
this.updatePipeline(
storeKey,
pipeline,
{
isLoading: false,
isExpanded: false,
},
visiblePipelineKey,
{},
);
static openPipeline(pipeline) {
Vue.set(pipeline, 'isExpanded', true);
}
}
......@@ -3,7 +3,7 @@
module EE
module Projects
module PipelinesController
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
def security
if pipeline.expose_security_dashboard?
......@@ -20,6 +20,11 @@ module EE
redirect_to pipeline_path(pipeline)
end
end
override :show_represent_params
def show_represent_params
super.merge(expanded: params[:expanded].to_a.map(&:to_i))
end
end
end
end
......@@ -3,6 +3,8 @@
class TriggeredPipelineEntity < Grape::Entity
include RequestAwareEntity
MAX_EXPAND_DEPTH = 3
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
......@@ -15,15 +17,49 @@ class TriggeredPipelineEntity < Grape::Entity
expose :details do
expose :detailed_status, as: :status, with: DetailedStatusEntity
expose :ordered_stages,
as: :stages, using: StageEntity,
if: -> (_, opts) { can_read_details? && expand?(opts) }
end
expose :triggered_by_pipeline,
as: :triggered_by, with: TriggeredPipelineEntity,
if: -> (_, opts) { can_read_details? && expand_for_path?(opts) }
expose :triggered_pipelines,
as: :triggered, using: TriggeredPipelineEntity,
if: -> (_, opts) { can_read_details? && expand_for_path?(opts) }
expose :project, using: ProjectEntity
private
alias_method :pipeline, :object
def can_read_details?
can?(request.current_user, :read_pipeline, pipeline)
end
def detailed_status
pipeline.detailed_status(request.current_user)
end
def expand?(opts)
opts[:expanded].to_a.include?(pipeline.id)
end
def expand_for_path?(opts)
# The `opts[:attr_path]` holds a list of all `exposes` in path
# The check ensures that we always expand only `triggered_by`, `triggered_by`, ...
# but not the `triggered_by`, `triggered` which would result in dead loop
attr_path = opts[:attr_path]
current_expose = attr_path.last
# We expand at most to depth of MAX_DEPTH
# We ensure that we expand in one direction: triggered_by,... or triggered, ...
attr_path.length < MAX_EXPAND_DEPTH &&
attr_path.all?(current_expose) &&
expand?(opts)
end
end
---
title: Recursively expands upstream and downstream pipelines
merge_request: 9073
author:
type: changed
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe Projects::PipelinesController do
set(:user) { create(:user) }
set(:project) { create(:project, :repository) }
set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
project.add_developer(user)
......@@ -10,9 +11,193 @@ describe Projects::PipelinesController do
sign_in(user)
end
describe 'GET security' do
set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
describe 'GET show.json' do
set(:source_project) { create(:project) }
set(:target_project) { create(:project) }
set(:root_pipeline) { create_pipeline(project) }
set(:source_pipeline) { create_pipeline(source_project) }
set(:source_of_source_pipeline) { create_pipeline(source_project) }
set(:target_pipeline) { create_pipeline(target_project) }
set(:target_of_target_pipeline) { create_pipeline(target_project) }
before do
create_link(source_of_source_pipeline, source_pipeline)
create_link(source_pipeline, root_pipeline)
create_link(root_pipeline, target_pipeline)
create_link(target_pipeline, target_of_target_pipeline)
end
shared_examples 'not expanded' do
let(:expected_stages) { be_nil }
it 'does return base details' do
get_pipeline_json(root_pipeline)
expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
expect(json_response['triggered']).to contain_exactly(
include('id' => target_pipeline.id))
end
it 'does not expand triggered_by pipeline' do
get_pipeline_json(root_pipeline)
triggered_by = json_response['triggered_by']
expect(triggered_by['triggered_by']).to be_nil
expect(triggered_by['triggered']).to be_nil
expect(triggered_by['details']['stages']).to expected_stages
end
it 'does not expand triggered pipelines' do
get_pipeline_json(root_pipeline)
first_triggered = json_response['triggered'].first
expect(first_triggered['triggered_by']).to be_nil
expect(first_triggered['triggered']).to be_nil
expect(first_triggered['details']['stages']).to expected_stages
end
end
shared_examples 'expanded' do
it 'does return base details' do
get_pipeline_json(root_pipeline)
expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
expect(json_response['triggered']).to contain_exactly(
include('id' => target_pipeline.id))
end
it 'does expand triggered_by pipeline' do
get_pipeline_json(root_pipeline)
triggered_by = json_response['triggered_by']
expect(triggered_by['triggered_by']).to include(
'id' => source_of_source_pipeline.id)
expect(triggered_by['details']['stages']).not_to be_nil
end
it 'does not recursively expand triggered_by' do
get_pipeline_json(root_pipeline)
triggered_by = json_response['triggered_by']
expect(triggered_by['triggered']).to be_nil
end
it 'does expand triggered pipelines' do
get_pipeline_json(root_pipeline)
first_triggered = json_response['triggered'].first
expect(first_triggered['triggered']).to contain_exactly(
include('id' => target_of_target_pipeline.id))
expect(first_triggered['details']['stages']).not_to be_nil
end
it 'does not recursively expand triggered' do
get_pipeline_json(root_pipeline)
first_triggered = json_response['triggered'].first
expect(first_triggered['triggered_by']).to be_nil
end
end
context 'when it does have permission to read other projects' do
before do
source_project.add_developer(user)
target_project.add_developer(user)
end
context 'when not-expanding any pipelines' do
let(:expanded) { nil }
it_behaves_like 'not expanded'
end
context 'when expanding non-existing pipeline' do
let(:expanded) { [-1] }
it_behaves_like 'not expanded'
end
context 'when expanding pipeline that is not directly expandable' do
let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] }
it_behaves_like 'not expanded'
end
context 'when expanding self' do
let(:expanded) { [root_pipeline.id] }
context 'it does not recursively expand pipelines' do
it_behaves_like 'not expanded'
end
end
context 'when expanding source and target pipeline' do
let(:expanded) { [source_pipeline.id, target_pipeline.id] }
it_behaves_like 'expanded'
context 'when expand depth is limited to 1' do
before do
stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1)
end
it_behaves_like 'not expanded' do
# We expect that triggered/triggered_by is not expanded,
# but we still return details.stages for that pipeline
let(:expected_stages) { be_a(Array) }
end
end
end
context 'when expanding all' do
let(:expanded) do
[
source_of_source_pipeline.id,
source_pipeline.id,
root_pipeline.id,
target_pipeline.id,
target_of_target_pipeline.id
]
end
it_behaves_like 'expanded'
end
end
context 'when does not have permission to read other projects' do
let(:expanded) { [source_pipeline.id, target_pipeline.id] }
it_behaves_like 'not expanded'
end
def create_pipeline(project)
create(:ci_empty_pipeline, project: project).tap do |pipeline|
create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
end
end
def create_link(source_pipeline, pipeline)
source_pipeline.sourced_pipelines.create!(
source_job: source_pipeline.builds.all.sample,
source_project: source_pipeline.project,
project: pipeline.project,
pipeline: pipeline
)
end
def get_pipeline_json(pipeline)
params = {
namespace_id: pipeline.project.namespace,
project_id: pipeline.project,
id: pipeline,
expanded: expanded
}
get :show, params: params.compact, format: :json
end
end
describe 'GET security' do
context 'with a sast artifact' do
before do
create(:ee_ci_build, :legacy_sast, pipeline: pipeline)
......@@ -68,8 +253,6 @@ describe Projects::PipelinesController do
end
describe 'GET licenses' do
set(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
context 'with a license management artifact' do
before do
build = create(:ci_build, pipeline: pipeline)
......
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import PipelineStore from 'ee/pipelines/stores/pipeline_store';
import graphComponent from 'ee/pipelines/components/graph/graph_component.vue';
import pipelineJSON from 'spec/pipelines/graph/mock_data';
import linkedPipelineJSON from 'ee_spec/pipelines/graph/linked_pipelines_mock_data';
const graphJSON = Object.assign(pipelineJSON, {
triggered: linkedPipelineJSON.triggered,
triggered_by: linkedPipelineJSON.triggered_by,
});
import linkedPipelineJSON from 'ee_spec/pipelines/linked_pipelines_mock.json';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
import graphJSON from 'spec/pipelines/graph/mock_data';
describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent);
const store = new PipelineStore();
store.storePipeline(linkedPipelineJSON);
const mediator = new PipelinesMediator({ endpoint: '' });
let component;
afterEach(() => {
......@@ -22,6 +23,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: true,
pipeline: {},
mediator,
});
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
......@@ -32,9 +34,8 @@ describe('graph component', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: linkedPipelineJSON.triggered,
pipeline: store.state.pipeline,
mediator,
});
});
......@@ -95,6 +96,14 @@ describe('graph component', () => {
});
describe('linked pipelines components', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: store.state.pipeline,
mediator,
});
});
it('should render an upstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Upstream');
......@@ -106,57 +115,63 @@ describe('graph component', () => {
});
describe('triggered by', () => {
describe('on click', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-129').click();
component.$el.querySelector('#js-linked-pipeline-12').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
linkedPipelineJSON.triggered_by,
component.pipeline,
component.pipeline.triggered_by[0],
);
});
});
describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => {
// expand the pipeline
store.state.pipeline.triggered_by[0].isExpanded = true;
describe('with expanded triggered by pipeline', () => {
it('should render expanded upstream pipeline', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [
Object.assign({}, linkedPipelineJSON.triggered_by, { isExpanded: true }),
],
triggeredPipelines: linkedPipelineJSON.triggered,
triggeredBy: linkedPipelineJSON.triggered_by,
pipeline: store.state.pipeline,
mediator,
});
expect(component.$el.querySelector('.upstream-pipeline')).not.toBeNull();
expect(component.$el.querySelector('.js-upstream-pipeline-12')).not.toBeNull();
});
});
});
describe('triggered ', () => {
it('should emit `onClickTriggered` when triggered linked pipeline is clicked', () => {
describe('triggered', () => {
describe('on click', () => {
it('should emit `onClickTriggered`', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-132').click();
component.$el.querySelector('#js-linked-pipeline-34993051').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
linkedPipelineJSON.triggered[0],
component.pipeline,
component.pipeline.triggered[0],
);
});
});
describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => {
// expand the pipeline
store.state.pipeline.triggered[0].isExpanded = true;
describe('with expanded triggered pipeline', () => {
it('should render expanded downstream pipeline', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: [
Object.assign({}, linkedPipelineJSON.triggered[0], { isExpanded: true }),
],
triggered: linkedPipelineJSON.triggered[0],
pipeline: store.state.pipeline,
mediator,
});
expect(component.$el.querySelector('.downstream-pipeline')).not.toBeNull();
expect(component.$el.querySelector('.js-downstream-pipeline-34993051')).not.toBeNull();
});
});
});
......@@ -165,10 +180,11 @@ describe('graph component', () => {
describe('when linked pipelines are not present', () => {
beforeEach(() => {
const pipeline = Object.assign(graphJSON, { triggered: null, triggered_by: null });
const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null });
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline,
mediator,
});
});
......@@ -200,6 +216,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
mediator,
});
expect(
......
......@@ -13,31 +13,9 @@ describe('Linked pipeline', () => {
vm.$destroy();
});
describe('while is loading', () => {
describe('rendered output', () => {
const props = {
pipelineId: mockPipeline.id,
pipelinePath: mockPipeline.path,
pipelineStatus: mockPipeline.details.status,
projectName: mockPipeline.project.name,
isLoading: true,
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('renders loading icon', () => {
expect(vm.$el.querySelector('.js-linked-pipeline-loading')).not.toBeNull();
});
});
describe('when it is not loading', () => {
const props = {
pipelineId: mockPipeline.id,
pipelinePath: mockPipeline.path,
pipelineStatus: mockPipeline.details.status,
projectName: mockPipeline.project.name,
isLoading: false,
pipeline: mockPipeline,
};
beforeEach(() => {
......@@ -55,7 +33,7 @@ describe('Linked pipeline', () => {
});
it('should render the project name', () => {
expect(vm.$el.innerText).toContain(props.projectName);
expect(vm.$el.innerText).toContain(props.pipeline.project.name);
});
it('should render an svg within the status container', () => {
......@@ -74,7 +52,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline id', () => {
expect(vm.$el.innerText).toContain(`#${props.pipelineId}`);
expect(vm.$el.innerText).toContain(`#${props.pipeline.id}`);
});
it('should correctly compute the tooltip text', () => {
......@@ -93,11 +71,7 @@ describe('Linked pipeline', () => {
describe('on click', () => {
const props = {
pipelineId: mockPipeline.id,
pipelinePath: mockPipeline.path,
pipelineStatus: mockPipeline.details.status,
projectName: mockPipeline.project.name,
isLoading: false,
pipeline: mockPipeline,
};
beforeEach(() => {
......@@ -115,10 +89,10 @@ describe('Linked pipeline', () => {
spyOn(vm.$root, '$emit');
vm.$el.querySelector('button').click();
expect(vm.$root.$emit).toHaveBeenCalledWith(
expect(vm.$root.$emit.calls.argsFor(0)).toEqual([
'bv::hide::tooltip',
`js-linked-pipeline-${props.pipelineId}`,
);
'js-linked-pipeline-132',
]);
});
});
});
This source diff could not be displayed because it is too large. You can view the blob instead.
import PipelineStore from 'ee/pipelines/stores/pipeline_store';
import pipelineWithTriggered from './pipeline_with_triggered.json';
import pipelineWithTriggeredBy from './pipeline_with_triggered_by.json';
import pipelineWithBoth from './pipeline_with_triggered_triggered_by.json';
import pipeline from './pipeline.json';
import LinkedPipelines from '../linked_pipelines_mock.json';
describe('EE Pipeline store', () => {
let store;
let data;
beforeEach(() => {
store = new PipelineStore();
data = Object.assign({}, LinkedPipelines);
});
describe('storePipeline', () => {
describe('triggeredPipelines ', () => {
describe('with triggered pipelines', () => {
it('saves parsed pipelines', () => {
store.storePipeline(pipelineWithTriggered);
expect(store.triggeredPipelines.length).toEqual(pipelineWithTriggered.triggered.length);
expect(store.triggeredPipelines[0]).toEqual(
Object.assign({}, pipelineWithTriggered.triggered[0], {
isLoading: false,
isCollpased: true,
}),
);
});
beforeAll(() => {
store.storePipeline(data);
});
describe('without triggered pipelines', () => {
it('triggeredPipelines should be an empty array', () => {
store.storePipeline({ triggered: [] });
expect(store.triggeredPipelines).toEqual([]);
});
});
describe('triggered_by', () => {
it('sets triggered_by as an array', () => {
expect(store.state.pipeline.triggered_by.length).toEqual(1);
});
describe('triggeredByPipelines', () => {
describe('with triggered_by pipeline', () => {
store.storePipeline(pipelineWithTriggeredBy);
expect(store.pipelineWithTriggeredBy.length).toEqual(1);
expect(store.triggeredByPipelines[0]).toEqual(
Object.assign({}, pipelineWithTriggeredBy.triggered_by, {
isLoading: false,
isCollpased: true,
}),
);
it('adds isExpanding key set to false', () => {
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
});
describe('without triggered_by pipeline', () => {
it('triggeredByPipelines should be an empty array', () => {
store.storePipeline({ triggered_by: null });
expect(store.triggeredByPipelines).toEqual([]);
});
});
it('parses nested triggered_by', () => {
expect(store.state.pipeline.triggered_by[0].triggered_by.length).toEqual(1);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
});
});
describe('downstream', () => {
beforeAll(() => {
store.storePipeline(pipelineWithBoth);
describe('triggered', () => {
it('adds isExpanding key set to false for each triggered pipeline', () => {
store.state.pipeline.triggered.forEach(pipeline => {
expect(pipeline.isExpanded).toEqual(false);
});
describe('requestTriggeredPipeline', () => {
beforeEach(() => {
store.requestTriggeredPipeline(store.triggeredPipelines[0]);
});
it('sets isLoading to true for the requested pipeline', () => {
expect(store.triggeredPipelines[0].isLoading).toEqual(true);
it('parses nested triggered pipelines', () => {
store.state.pipeline.triggered[1].triggered.forEach(pipeline => {
expect(pipeline.isExpanded).toEqual(false);
});
it('sets isExpanded to true for the requested pipeline', () => {
expect(store.triggeredPipelines[0].isExpanded).toEqual(true);
});
it('sets isLoading to false for the other pipelines', () => {
expect(store.triggeredPipelines[1].isLoading).toEqual(false);
});
it('sets isExpanded to false for the other pipelines', () => {
expect(store.triggeredPipelines[1].isExpanded).toEqual(false);
});
});
describe('receiveTriggeredPipelineSuccess', () => {
it('updates the given pipeline and sets it as the visible one', () => {
const receivedPipeline = store.triggeredPipelines[0];
store.receiveTriggeredPipelineSuccess(receivedPipeline);
expect(store.triggeredPipelines[0].isLoading).toEqual(false);
expect(store.triggered).toEqual(receivedPipeline);
});
describe('resetTriggeredByPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
describe('receiveTriggeredPipelineError', () => {
it('resets the given pipeline and resets it as the visible one', () => {
const receivedPipeline = store.triggeredPipelines[0];
store.receiveTriggeredPipelineError(receivedPipeline);
it('closes the pipeline & nested ones', () => {
store.state.pipeline.triggered_by[0].isExpanded = true;
store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true;
expect(store.triggeredPipelines[0].isLoading).toEqual(false);
expect(store.triggeredPipelines[0].isExpanded).toEqual(false);
store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.triggered).toEqual({});
});
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
});
});
describe('upstream', () => {
describe('requestTriggeredByPipeline', () => {
describe('openTriggeredByPipeline', () => {
beforeEach(() => {
store.requestTriggeredByPipeline(store.triggeredByPipelines[0]);
store.storePipeline(data);
});
it('sets isLoading to true for the requested pipeline', () => {
expect(store.triggeredByPipelines[0].isLoading).toEqual(true);
});
it('opens the given pipeline', () => {
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
it('sets isExpanded to true for the requested pipeline', () => {
expect(store.triggeredByPipelines[0].isExpanded).toEqual(true);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
});
});
describe('receiveTriggeredByPipelineSuccess', () => {
it('updates the given pipeline and sets it as the visible one', () => {
const receivedPipeline = store.triggeredByPipelines[0];
store.receiveTriggeredByPipelineSuccess(receivedPipeline);
expect(store.triggeredByPipelines[0].isLoading).toEqual(false);
expect(store.triggeredBy).toEqual(receivedPipeline);
});
describe('closeTriggeredByPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
describe('receiveTriggeredByPipelineError', () => {
it('resets the given pipeline and resets it as the visible one', () => {
const receivedPipeline = store.triggeredByPipelines[0];
store.receiveTriggeredByPipelineError(receivedPipeline);
it('closes the given pipeline', () => {
// open it first
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.triggeredByPipelines[0].isLoading).toEqual(false);
expect(store.triggeredByPipelines[0].isExpanded).toEqual(false);
store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.triggeredBy).toEqual({});
});
});
});
describe('utils', () => {
describe('parsePipeline', () => {
let parsed;
beforeAll(() => {
parsed = PipelineStore.parsePipeline(pipeline);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
});
it('adds isLoading key set to false', () => {
expect(parsed.isLoading).toEqual(false);
});
it('adds isExpanded key set to false', () => {
expect(parsed.isExpanded).toEqual(false);
});
describe('resetTriggeredPipelines', () => {
beforeEach(() => {
store.storePipeline(data);
});
describe('getPipelineIndex', () => {
beforeAll(() => {
store.storePipeline(pipelineWithBoth);
});
it('closes the pipeline & nested ones', () => {
store.state.pipeline.triggered[0].isExpanded = true;
store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
it('returns the pipeline index for the provided pipeline and storeKey', () => {
store.getPipelineIndex('triggeredPipelines', store.triggeredPipelines[1]).toEqual(1);
});
});
store.resetTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
describe('updateStoreOnRequest', () => {
beforeAll(() => {
store.storePipeline(pipelineWithBoth);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
});
it('sets clicked pipeline isLoading to true', () => {
store.updateStoreOnRequest('triggeredPipelines', store.triggeredPipelines[1]);
expect(store.triggeredPipelines[1].isLoading).isLoading(true);
});
it('sets clicked pipeline isExpanded to true', () => {
store.updateStoreOnRequest('triggeredPipelines', store.triggeredPipelines[1]);
expect(store.triggeredPipelines[1].isExpanded).isLoading(true);
});
describe('openTriggeredPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
describe('updatePipeline', () => {
beforeAll(() => {
store.storePipeline(pipelineWithBoth);
store.updatePipeline(
'triggeredPipelines',
store.triggeredPipelines[1],
{ isLoading: true },
'triggered',
store.triggeredPipelines[1],
);
});
it('opens the given pipeline', () => {
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
it('updates the given pipeline in the correct array', () => {
expect(store.triggeredPipelines[1].isLoading).toEqual(true);
expect(store.triggered).toEqual(store.triggeredPipelines[1]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
});
it('updates the visible pipeline to the given value', () => {});
});
describe('closePipeline', () => {
beforeAll(() => {
store.storePipeline(pipelineWithBoth);
describe('closeTriggeredPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('closes the given pipeline', () => {
const clickedPipeline = store.triggeredPipelines[1];
// open it first
clickedPipeline.isExpanded = true;
store.triggered = clickedPipeline;
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
store.closePipeline('triggeredPipelines', clickedPipeline, 'triggered');
store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.triggeredPipelines[1].isExpanded).toEqual(true);
expect(store.triggered).toEqual({});
});
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
});
});
});
......@@ -685,12 +685,6 @@ msgstr ""
msgid "An error occured while fetching the releases. Please try again."
msgstr ""
msgid "An error occured while fetching this downstream pipeline. Please try again"
msgstr ""
msgid "An error occured while fetching this upstream pipeline. Please try again"
msgstr ""
msgid "An error occurred adding a draft to the discussion."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment