Commit bfd7e162 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '299112-remove-graphql-pipeline-details-ff' into 'master'

Remove pipelineGraphLayersView feature flag [RUN AS-IF-FOSS] [RUN ALL RSPEC]

See merge request gitlab-org/gitlab!68040
parents 4b17cc4b 582ecdb8
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { escape, capitalize } from 'lodash';
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import { reportToSentry } from '../../utils';
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
export default {
name: 'PipelineGraphLegacy',
components: {
GlLoadingIcon,
LinkedPipelinesColumnLegacy,
StageColumnComponentLegacy,
},
mixins: [GraphBundleMixin],
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
isLinkedPipeline: {
type: Boolean,
required: false,
default: false,
},
mediator: {
type: Object,
required: true,
},
type: {
type: String,
required: false,
default: MAIN,
},
},
upstream: UPSTREAM,
downstream: DOWNSTREAM,
data() {
return {
downstreamMarginTop: null,
jobName: null,
pipelineExpanded: {
jobName: '',
expanded: false,
},
};
},
computed: {
graph() {
return this.pipeline.details?.stages;
},
hasUpstream() {
return (
this.type !== this.$options.downstream &&
this.upstreamPipelines &&
this.pipeline.triggered_by !== null
);
},
upstreamPipelines() {
return this.pipeline.triggered_by;
},
hasDownstream() {
return (
this.type !== this.$options.upstream &&
this.downstreamPipelines &&
this.pipeline.triggered.length > 0
);
},
downstreamPipelines() {
return this.pipeline.triggered;
},
expandedUpstream() {
return (
this.pipeline.triggered_by &&
Array.isArray(this.pipeline.triggered_by) &&
this.pipeline.triggered_by.find((el) => el.isExpanded)
);
},
expandedDownstream() {
return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded);
},
pipelineTypeUpstream() {
return this.type !== this.$options.downstream && this.expandedUpstream;
},
pipelineTypeDownstream() {
return this.type !== this.$options.upstream && this.expandedDownstream;
},
pipelineProjectId() {
return this.pipeline.project.id;
},
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
methods: {
capitalizeStageName(name) {
const escapedName = escape(name);
return capitalize(escapedName);
},
isFirstColumn(index) {
return index === 0;
},
stageConnectorClass(index, stage) {
let className;
// If it's the first stage column and only has one job
if (this.isFirstColumn(index) && stage.groups.length === 1) {
className = 'no-margin';
} else if (index > 0) {
// If it is not the first column
className = 'left-margin';
}
return className;
},
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
/**
* CSS class is applied:
* - if pipeline graph contains only one stage column component
*
* @param {number} index
* @returns {boolean}
*/
shouldAddRightMargin(index) {
return !(index === this.graph.length - 1);
},
handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
/**
* Calculates the margin top of the clicked downstream pipeline by
* subtracting the clicked downstream pipelines offsetTop by it's parent's
* offsetTop and then subtracting 15
*/
this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
/**
* If the expanded trigger is defined and the id is different than the
* pipeline we clicked, then it means we clicked on a sibling downstream link
* and we want to reset the pipeline store. Triggering the reset without
* this condition would mean not allowing downstreams of downstreams to expand
*/
if (this.expandedDownstream?.id !== pipeline.id) {
this.$emit('onResetDownstream', this.pipeline, pipeline);
}
this.$emit('onClickDownstreamPipeline', pipeline);
},
calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
},
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
hasUpstreamColumn(index) {
return index === 0 && this.hasUpstream;
},
setJob(jobName) {
this.jobName = jobName;
},
setPipelineExpanded(jobName, expanded) {
if (expanded) {
this.pipelineExpanded = {
jobName,
expanded,
};
} else {
this.pipelineExpanded = {
expanded,
jobName: '',
};
}
},
},
};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
<div
class="pipeline-visualization pipeline-graph"
:class="{ 'pipeline-tab-content': !isLinkedPipeline }"
>
<div class="gl-w-full">
<div class="container-fluid container-limited">
<gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
<pipeline-graph-legacy
v-if="pipelineTypeUpstream"
:type="$options.upstream"
class="d-inline-block upstream-pipeline"
:class="`js-upstream-pipeline-${expandedUpstream.id}`"
:is-loading="false"
:pipeline="expandedUpstream"
:is-linked-pipeline="true"
:mediator="mediator"
@onClickUpstreamPipeline="clickUpstreamPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
<linked-pipelines-column-legacy
v-if="hasUpstream"
:type="$options.upstream"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:project-id="pipelineProjectId"
@linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
/>
<ul
v-if="!isLoading"
:class="{
'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
}"
class="stage-column-list align-top"
>
<stage-column-component-legacy
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
'has-upstream gl-ml-11': hasUpstreamColumn(index),
'has-only-one-job': hasOnlyOneJob(stage),
'gl-mr-26': shouldAddRightMargin(index),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:has-upstream="hasUpstream"
:action="stage.status.action"
:job-hovered="jobName"
:pipeline-expanded="pipelineExpanded"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
<linked-pipelines-column-legacy
v-if="hasDownstream"
:type="$options.downstream"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:project-id="pipelineProjectId"
@linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
@pipelineExpandToggle="setPipelineExpanded"
/>
<pipeline-graph-legacy
v-if="pipelineTypeDownstream"
:type="$options.downstream"
class="d-inline-block"
:class="`js-downstream-pipeline-${expandedDownstream.id}`"
:is-loading="false"
:pipeline="expandedDownstream"
:is-linked-pipeline="true"
:style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator"
@onClickDownstreamPipeline="clickDownstreamPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { reportToSentry } from '../../utils';
import { UPSTREAM } from './constants';
import LinkedPipeline from './linked_pipeline.vue';
export default {
components: {
LinkedPipeline,
},
props: {
columnTitle: {
type: String,
required: true,
},
linkedPipelines: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
},
computed: {
columnClass() {
const positionValues = {
right: 'gl-ml-11',
left: 'gl-mr-7',
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
isExpanded() {
return this.pipeline?.isExpanded || false;
},
isUpstream() {
return this.type === UPSTREAM;
},
},
errorCaptured(err, _vm, info) {
reportToSentry('linked_pipelines_column_legacy', `error: ${err}, info: ${info}`);
},
methods: {
onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
},
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
},
onPipelineExpandToggle(jobName, expanded) {
// Highlighting only applies to downstream pipelines
if (this.isUpstream) {
return;
}
this.$emit('pipelineExpandToggle', jobName, expanded);
},
},
};
</script>
<template>
<div :class="columnClass" class="stage-column linked-pipelines-column">
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
<div v-if="isUpstream" class="cross-project-triangle"></div>
<ul>
<li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
<linked-pipeline
:class="{
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left',
}"
:pipeline="pipeline"
:column-title="columnTitle"
:project-id="projectId"
:type="type"
:expanded="isExpanded"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
</li>
</ul>
</div>
</template>
<script>
import { isEmpty, escape } from 'lodash';
import stageColumnMixin from '../../mixins/stage_column_mixin';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
export default {
components: {
JobItem,
JobGroupDropdown,
ActionComponent,
},
mixins: [stageColumnMixin],
props: {
title: {
type: String,
required: true,
},
groups: {
type: Array,
required: true,
},
isFirstColumn: {
type: Boolean,
required: false,
default: false,
},
stageConnectorClass: {
type: String,
required: false,
default: '',
},
action: {
type: Object,
required: false,
default: () => ({}),
},
jobHovered: {
type: String,
required: false,
default: '',
},
pipelineExpanded: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
hasAction() {
return !isEmpty(this.action);
},
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component_legacy', `error: ${err}, info: ${info}`);
},
methods: {
groupId(group) {
return `ci-badge-${escape(group.name)}`;
},
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
},
},
};
</script>
<template>
<li :class="stageConnectorClass" class="stage-column">
<div class="stage-name position-relative" data-testid="stage-column-title">
{{ title }}
<action-component
v-if="hasAction"
:action-icon="action.icon"
:tooltip-text="action.title"
:link="action.path"
class="js-stage-action stage-action rounded"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
<div class="builds-container">
<ul>
<li
v-for="(group, index) in groups"
:id="groupId(group)"
:key="group.id"
:class="buildConnnectorClass(index)"
class="build"
>
<div class="curve"></div>
<job-item
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:pipeline-expanded="pipelineExpanded"
css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<job-group-dropdown
v-if="group.size > 1"
:group="group"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
</div>
</li>
</template>
import createFlash from '~/flash';
import { __ } from '~/locale';
export default {
methods: {
getExpandedPipelines(pipeline) {
this.mediator.service
.getPipeline(this.mediator.getExpandedParameters())
.then((response) => {
this.mediator.store.toggleLoading(pipeline);
this.mediator.store.storePipeline(response.data);
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
})
.catch(() => {
this.mediator.store.toggleLoading(pipeline);
createFlash({
message: __('An error occurred while fetching the pipeline.'),
});
});
},
/**
* Called when a linked pipeline is clicked.
*
* If the pipeline is collapsed we will start polling it & we will reset the other pipelines.
* If the pipeline is expanded we will close it.
*
* @param {String} method Method to fetch the pipeline
* @param {String} storeKey Store property that will be updates
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
clickPipeline(pipeline, openMethod, closeMethod) {
if (!pipeline.isExpanded) {
this.mediator.store[openMethod](pipeline);
this.mediator.store.toggleLoading(pipeline);
this.mediator.poll.stop();
this.getExpandedPipelines(pipeline);
} else {
this.mediator.store[closeMethod](pipeline);
this.mediator.poll.stop();
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
}
},
resetDownstreamPipelines(parentPipeline, pipeline) {
this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
},
clickUpstreamPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
clickDownstreamPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
requestRefreshPipelineGraph() {
// When an action is clicked
// (whether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator.refreshPipeline().catch(() =>
createFlash({
message: __('An error occurred while making the request.'),
}),
);
},
},
};
...@@ -3,15 +3,12 @@ import createFlash from '~/flash'; ...@@ -3,15 +3,12 @@ import createFlash from '~/flash';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
import TestReports from './components/test_reports/test_reports.vue'; import TestReports from './components/test_reports/test_reports.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import createDagApp from './pipeline_details_dag'; import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph'; import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header'; import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client'; import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports'; import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './utils';
Vue.use(Translate); Vue.use(Translate);
...@@ -22,44 +19,6 @@ const SELECTORS = { ...@@ -22,44 +19,6 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail', PIPELINE_TESTS: '#js-pipeline-tests-detail',
}; };
const createLegacyPipelinesDetailApp = (mediator) => {
if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el: SELECTORS.PIPELINE_GRAPH,
components: {
PipelineGraphLegacy,
},
mixins: [GraphBundleMixin],
data() {
return {
mediator,
};
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_bundle_legacy_details', `error: ${err}, info: ${info}`);
},
render(createElement) {
return createElement('pipeline-graph-legacy', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
mediator: this.mediator,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
onResetDownstream: (parentPipeline, pipeline) =>
this.resetDownstreamPipelines(parentPipeline, pipeline),
onClickUpstreamPipeline: (pipeline) => this.clickUpstreamPipeline(pipeline),
onClickDownstreamPipeline: (pipeline) => this.clickDownstreamPipeline(pipeline),
},
});
},
});
};
const createTestDetails = () => { const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS); const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } = const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
...@@ -88,9 +47,6 @@ const createTestDetails = () => { ...@@ -88,9 +47,6 @@ const createTestDetails = () => {
}; };
export default async function initPipelineDetailsBundle() { export default async function initPipelineDetailsBundle() {
const canShowNewPipelineDetails =
gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
try { try {
...@@ -101,22 +57,12 @@ export default async function initPipelineDetailsBundle() { ...@@ -101,22 +57,12 @@ export default async function initPipelineDetailsBundle() {
}); });
} }
if (canShowNewPipelineDetails) { try {
try { createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset); } catch {
} catch { createFlash({
createFlash({ message: __('An error occurred while loading the pipeline.'),
message: __('An error occurred while loading the pipeline.'), });
});
}
} else {
const { default: PipelinesMediator } = await import(
/* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
);
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();
createLegacyPipelinesDetailApp(mediator);
} }
createDagApp(apolloProvider); createDagApp(apolloProvider);
......
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineService from './services/pipeline_service';
import PipelineStore from './stores/pipeline_store';
export default class pipelinesMediator {
constructor(options = {}) {
this.options = options;
this.store = new PipelineStore();
this.service = new PipelineService(options.endpoint);
this.state = {};
this.state.isLoading = false;
}
fetchPipeline() {
this.poll = new Poll({
resource: this.service,
method: 'getPipeline',
data: this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
} else {
this.refreshPipeline();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.stopPipelinePoll();
}
});
}
successCallback(response) {
this.state.isLoading = false;
this.store.storePipeline(response.data);
}
errorCallback() {
this.state.isLoading = false;
createFlash({
message: __('An error occurred while fetching the pipeline.'),
});
}
refreshPipeline() {
this.stopPipelinePoll();
return this.service
.getPipeline()
.then((response) => this.successCallback(response))
.catch(() => this.errorCallback())
.finally(() =>
this.poll.restart(
this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
),
);
}
stopPipelinePoll() {
this.poll.stop();
}
/**
* Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
*/
getExpandedParameters() {
return {
expanded: this.store.state.expandedPipelines,
};
}
}
import axios from '../../lib/utils/axios_utils';
export default class PipelineService {
constructor(endpoint) {
this.pipeline = endpoint;
}
getPipeline(params) {
return axios.get(this.pipeline, { params });
}
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(`${endpoint}.json`);
}
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return axios.post(`${endpoint}.json`);
}
}
import Vue from 'vue';
export default class PipelineStore {
constructor() {
this.state = {};
this.state.pipeline = {};
this.state.expandedPipelines = [];
}
/**
* For the triggered pipelines adds the `isExpanded` key
*
* For the triggered_by pipeline adds the `isExpanded` key
* and saves it as an array
*
* @param {Object} pipeline
*/
storePipeline(pipeline = {}) {
const pipelineCopy = { ...pipeline };
if (pipelineCopy.triggered_by) {
pipelineCopy.triggered_by = [pipelineCopy.triggered_by];
const oldTriggeredBy =
this.state.pipeline &&
this.state.pipeline.triggered_by &&
this.state.pipeline.triggered_by[0];
this.parseTriggeredByPipelines(oldTriggeredBy, pipelineCopy.triggered_by[0]);
}
if (pipelineCopy.triggered && pipelineCopy.triggered.length) {
pipelineCopy.triggered.forEach((el) => {
const oldPipeline =
this.state.pipeline &&
this.state.pipeline.triggered &&
this.state.pipeline.triggered.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldPipeline, el);
});
}
this.state.pipeline = pipelineCopy;
}
/**
* Recursiverly parses the triggered by pipelines.
*
* 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
*/
parseTriggeredByPipelines(oldPipeline = {}, newPipeline) {
// keep old value in case it's opened because we're polling
Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
// add isLoading property
Vue.set(newPipeline, 'isLoading', false);
// Because there can only ever be one `triggered_by` for any given pipeline,
// the API returns an object for the value instead of an Array. However,
// it's easier to deal with an array in the FE so we convert it.
if (newPipeline.triggered_by) {
if (!Array.isArray(newPipeline.triggered_by)) {
Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] });
}
if (newPipeline.triggered_by?.length > 0) {
newPipeline.triggered_by.forEach((el) => {
const oldTriggeredBy = oldPipeline.triggered_by?.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldTriggeredBy, el);
});
}
}
}
/**
* Recursively parses the triggered pipelines
* @param {Array} parentPipeline
* @param {Object} pipeline
*/
parseTriggeredPipelines(oldPipeline = {}, newPipeline) {
// keep old value in case it's opened because we're polling
Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
// add isLoading property
Vue.set(newPipeline, 'isLoading', false);
if (newPipeline.triggered && newPipeline.triggered.length > 0) {
newPipeline.triggered.forEach((el) => {
const oldTriggered =
oldPipeline.triggered && oldPipeline.triggered.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldTriggered, el);
});
}
}
/**
* Recursively resets all triggered by pipelines
*
* @param {Object} pipeline
*/
resetTriggeredByPipeline(parentPipeline, pipeline) {
parentPipeline.triggered_by.forEach((el) => this.closePipeline(el));
if (pipeline.triggered_by && pipeline.triggered_by) {
this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by);
}
}
/**
* Opens the clicked pipeline and closes all other ones.
* @param {Object} pipeline
*/
openTriggeredByPipeline(parentPipeline, pipeline) {
// first we need to reset all triggeredBy pipelines
this.resetTriggeredByPipeline(parentPipeline, pipeline);
this.openPipeline(pipeline);
}
/**
* On click, will close the given pipeline and all nested triggered by pipelines
*
* @param {Object} pipeline
*/
closeTriggeredByPipeline(pipeline) {
this.closePipeline(pipeline);
if (pipeline.triggered_by && pipeline.triggered_by.length) {
pipeline.triggered_by.forEach((triggeredBy) => this.closeTriggeredByPipeline(triggeredBy));
}
}
/**
* Recursively closes all triggered pipelines for the given one.
*
* @param {Object} pipeline
*/
resetTriggeredPipelines(parentPipeline, pipeline) {
parentPipeline.triggered.forEach((el) => this.closePipeline(el));
if (pipeline.triggered && pipeline.triggered.length) {
pipeline.triggered.forEach((el) => this.resetTriggeredPipelines(pipeline, el));
}
}
/**
* Opens the clicked triggered pipeline and closes all other ones.
*
* @param {Object} pipeline
*/
openTriggeredPipeline(parentPipeline, pipeline) {
this.resetTriggeredPipelines(parentPipeline, pipeline);
this.openPipeline(pipeline);
}
/**
* On click, will close the given pipeline and all the nested triggered ones
* @param {Object} pipeline
*/
closeTriggeredPipeline(pipeline) {
this.closePipeline(pipeline);
if (pipeline.triggered && pipeline.triggered.length) {
pipeline.triggered.forEach((triggered) => this.closeTriggeredPipeline(triggered));
}
}
/**
* Utility function, Closes the given pipeline
* @param {Object} pipeline
*/
closePipeline(pipeline) {
Vue.set(pipeline, 'isExpanded', false);
// remove the pipeline from the parameters
this.removeExpandedPipelineToRequestData(pipeline.id);
}
/**
* Utility function, Opens the given pipeline
* @param {Object} pipeline
*/
openPipeline(pipeline) {
Vue.set(pipeline, 'isExpanded', true);
// add the pipeline to the parameters
this.addExpandedPipelineToRequestData(pipeline.id);
}
// eslint-disable-next-line class-methods-use-this
toggleLoading(pipeline) {
Vue.set(pipeline, 'isLoading', !pipeline.isLoading);
}
addExpandedPipelineToRequestData(id) {
this.state.expandedPipelines.push(id);
}
removeExpandedPipelineToRequestData(id) {
this.state.expandedPipelines.splice(
this.state.expandedPipelines.findIndex((el) => el === id),
1,
);
}
}
...@@ -12,10 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -12,10 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_ci_cd_analytics!, only: [:charts] before_action :authorize_read_ci_cd_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
end
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
......
...@@ -5,9 +5,7 @@ ...@@ -5,9 +5,7 @@
- add_page_specific_style 'page_bundles/pipeline' - add_page_specific_style 'page_bundles/pipeline'
- add_page_specific_style 'page_bundles/reports' - add_page_specific_style 'page_bundles/reports'
- add_page_specific_style 'page_bundles/ci_status' - add_page_specific_style 'page_bundles/ci_status'
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
- if Feature.enabled?(:graphql_pipeline_details, @project, default_enabled: :yaml) || Feature.enabled?(:graphql_pipeline_details_users, @current_user, default_enabled: :yaml)
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } } .js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } } #js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
......
---
name: graphql_pipeline_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46380
rollout_issue_url:
type: development
group: group::pipeline authoring
default_enabled: true
milestone: '13.6'
---
name: graphql_pipeline_details_users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52092
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299112
milestone: '13.9'
type: development
group: group::pipeline authoring
default_enabled: false
...@@ -8,7 +8,6 @@ RSpec.describe 'Pipeline', :js do ...@@ -8,7 +8,6 @@ RSpec.describe 'Pipeline', :js do
let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) } let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) }
before do before do
stub_feature_flags(graphql_pipeline_details_users: false)
sign_in(user) sign_in(user)
project.add_developer(user) project.add_developer(user)
...@@ -31,119 +30,63 @@ RSpec.describe 'Pipeline', :js do ...@@ -31,119 +30,63 @@ RSpec.describe 'Pipeline', :js do
create_link(pipeline, downstream_pipeline) create_link(pipeline, downstream_pipeline)
end end
context 'when :graphql_pipeline_details flag is on' do context 'expands the upstream pipeline on click' do
context 'expands the upstream pipeline on click' do it 'renders upstream pipeline' do
it 'renders upstream pipeline' do subject
subject
expect(page).to have_content(upstream_pipeline.id)
expect(page).to have_content(upstream_pipeline.project.name)
end
it 'expands the upstream on click' do
subject
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
wait_for_requests
expect(page).to have_selector("#pipeline-links-container-#{upstream_pipeline.id}")
end
it 'closes the expanded upstream on click' do
subject
# open
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
wait_for_requests
# close
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
expect(page).not_to have_selector("#pipeline-links-container-#{upstream_pipeline.id}") expect(page).to have_content(upstream_pipeline.id)
end expect(page).to have_content(upstream_pipeline.project.name)
end end
it 'renders downstream pipeline' do it 'expands the upstream on click' do
subject subject
expect(page).to have_content(downstream_pipeline.id) page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
expect(page).to have_content(downstream_pipeline.project.name) wait_for_requests
expect(page).to have_selector("#pipeline-links-container-#{upstream_pipeline.id}")
end end
context 'expands the downstream pipeline on click' do it 'closes the expanded upstream on click' do
it 'expands the downstream on click' do subject
subject
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click
wait_for_requests
expect(page).to have_selector("#pipeline-links-container-#{downstream_pipeline.id}")
end
it 'closes the expanded downstream on click' do
subject
# open # open
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
wait_for_requests wait_for_requests
# close # close
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
expect(page).not_to have_selector("#pipeline-links-container-#{downstream_pipeline.id}") expect(page).not_to have_selector("#pipeline-links-container-#{upstream_pipeline.id}")
end
end end
end end
# remove when :graphql_pipeline_details flag is removed it 'renders downstream pipeline' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/299112 subject
context 'when :graphql_pipeline_details flag is off' do
before do
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
context 'expands the upstream pipeline on click' do expect(page).to have_content(downstream_pipeline.id)
it 'expands the upstream on click' do expect(page).to have_content(downstream_pipeline.project.name)
subject end
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
wait_for_requests
expect(page).to have_selector(".js-upstream-pipeline-#{upstream_pipeline.id}")
end
it 'closes the expanded upstream on click' do
subject
# open
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click
wait_for_requests
# close context 'expands the downstream pipeline on click' do
page.find(".js-pipeline-expand-#{upstream_pipeline.id}").click it 'expands the downstream on click' do
subject
expect(page).not_to have_selector(".js-upstream-pipeline-#{upstream_pipeline.id}") page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click
end wait_for_requests
expect(page).to have_selector("#pipeline-links-container-#{downstream_pipeline.id}")
end end
context 'expands the downstream pipeline on click' do it 'closes the expanded downstream on click' do
it 'expands the downstream on click' do subject
subject
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click
wait_for_requests
expect(page).to have_selector(".js-downstream-pipeline-#{downstream_pipeline.id}")
end
it 'closes the expanded downstream on click' do
subject
# open # open
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click
wait_for_requests wait_for_requests
# close # close
page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click page.find(".js-pipeline-expand-#{downstream_pipeline.id}").click
expect(page).not_to have_selector(".js-downstream-pipeline-#{downstream_pipeline.id}") expect(page).not_to have_selector("#pipeline-links-container-#{downstream_pipeline.id}")
end
end end
end end
end end
......
...@@ -3644,9 +3644,6 @@ msgstr "" ...@@ -3644,9 +3644,6 @@ msgstr ""
msgid "An error occurred while fetching the latest pipeline." msgid "An error occurred while fetching the latest pipeline."
msgstr "" msgstr ""
msgid "An error occurred while fetching the pipeline."
msgstr ""
msgid "An error occurred while fetching the releases. Please try again." msgid "An error occurred while fetching the releases. Please try again."
msgstr "" msgstr ""
......
...@@ -778,61 +778,6 @@ RSpec.describe 'Pipeline', :js do ...@@ -778,61 +778,6 @@ RSpec.describe 'Pipeline', :js do
describe 'GET /:project/-/pipelines/:id' do describe 'GET /:project/-/pipelines/:id' do
subject { visit project_pipeline_path(project, pipeline) } subject { visit project_pipeline_path(project, pipeline) }
# remove when :graphql_pipeline_details flag is removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/299112
context 'when :graphql_pipeline_details flag is off' do
before do
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
it 'shows deploy job as created' do
subject
within('.js-pipeline-header-container') do
expect(page).to have_content('pending')
end
within('.js-pipeline-graph') do
within '.stage-column:nth-child(1)' do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-pending')
end
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-created')
end
end
end
context 'when test job succeeded' do
before do
test_job.success!
end
it 'shows deploy job as pending' do
subject
within('.js-pipeline-header-container') do
expect(page).to have_content('running')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(1)' do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-success')
end
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
end
end
end
end
it 'shows deploy job as created' do it 'shows deploy job as created' do
subject subject
...@@ -902,29 +847,6 @@ RSpec.describe 'Pipeline', :js do ...@@ -902,29 +847,6 @@ RSpec.describe 'Pipeline', :js do
end end
end end
# remove when :graphql_pipeline_details flag is removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/299112
context 'when :graphql_pipeline_details flag is off' do
before do
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
it 'shows deploy job as waiting for resource' do
subject
within('.js-pipeline-header-container') do
expect(page).to have_content('waiting')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-waiting-for-resource')
end
end
end
end
context 'when resource is released from another job' do context 'when resource is released from another job' do
before do before do
another_job.success! another_job.success!
...@@ -944,29 +866,6 @@ RSpec.describe 'Pipeline', :js do ...@@ -944,29 +866,6 @@ RSpec.describe 'Pipeline', :js do
end end
end end
end end
# remove when :graphql_pipeline_details flag is removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/299112
context 'when :graphql_pipeline_details flag is off' do
before do
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
it 'shows deploy job as pending' do
subject
within('.js-pipeline-header-container') do
expect(page).to have_content('running')
end
within('.pipeline-graph') do
within '.stage-column:nth-child(2)' do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
end
end
end
end end
context 'when deploy job is a bridge to trigger a downstream pipeline' do context 'when deploy job is a bridge to trigger a downstream pipeline' do
...@@ -1234,23 +1133,6 @@ RSpec.describe 'Pipeline', :js do ...@@ -1234,23 +1133,6 @@ RSpec.describe 'Pipeline', :js do
expect(page).not_to have_content('Failed Jobs') expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.js-pipeline-graph') expect(page).to have_selector('.js-pipeline-graph')
end end
# remove when :graphql_pipeline_details flag is removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/299112
context 'when :graphql_pipeline_details flag is off' do
before do
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
it 'displays the pipeline graph' do
subject
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.pipeline-visualization')
end
end
end end
end end
......
...@@ -12,8 +12,6 @@ RSpec.describe 'Pipelines', :js do ...@@ -12,8 +12,6 @@ RSpec.describe 'Pipelines', :js do
before do before do
sign_in(user) sign_in(user)
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
project.add_developer(user) project.add_developer(user)
project.update!(auto_devops_attributes: { enabled: false }) project.update!(auto_devops_attributes: { enabled: false })
......
import { shallowMount } from '@vue/test-utils';
import { UPSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
import mockData from './linked_pipelines_mock_data';
describe('Linked Pipelines Column', () => {
const propsData = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
projectId: 19,
type: UPSTREAM,
};
let wrapper;
beforeEach(() => {
wrapper = shallowMount(LinkedPipelinesColumnLegacy, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('renders the pipeline orientation', () => {
const titleElement = wrapper.find('.linked-pipelines-column-title');
expect(titleElement.text()).toBe(propsData.columnTitle);
});
it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = wrapper.findAll(LinkedPipeline);
expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length);
});
it('renders cross project triangle when column is upstream', () => {
expect(wrapper.find('.cross-project-triangle').exists()).toBe(true);
});
});
export default {
id: 123,
user: {
name: 'Root',
username: 'root',
id: 1,
state: 'active',
avatar_url: null,
web_url: 'http://localhost:3000/root',
},
active: false,
coverage: null,
path: '/root/ci-mock/pipelines/123',
details: {
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/pipelines/123',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
duration: 9,
finished_at: '2017-04-19T14:30:27.542Z',
stages: [
{
name: 'test',
title: 'test: passed',
groups: [
{
name: 'test',
size: 1,
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4153/retry',
method: 'post',
},
},
jobs: [
{
id: 4153,
name: 'test',
build_path: '/root/ci-mock/builds/4153',
retry_path: '/root/ci-mock/builds/4153/retry',
playable: false,
created_at: '2017-04-13T09:25:18.959Z',
updated_at: '2017-04-13T09:25:23.118Z',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4153/retry',
method: 'post',
},
},
},
],
},
],
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/pipelines/123#test',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#test',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
},
{
name: 'deploy <img src=x onerror=alert(document.domain)>',
title: 'deploy: passed',
groups: [
{
name: 'deploy to production',
size: 1,
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4166/retry',
method: 'post',
},
},
jobs: [
{
id: 4166,
name: 'deploy to production',
build_path: '/root/ci-mock/builds/4166',
retry_path: '/root/ci-mock/builds/4166/retry',
playable: false,
created_at: '2017-04-19T14:29:46.463Z',
updated_at: '2017-04-19T14:30:27.498Z',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4166/retry',
method: 'post',
},
},
},
],
},
{
name: 'deploy to staging',
size: 1,
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4159/retry',
method: 'post',
},
},
jobs: [
{
id: 4159,
name: 'deploy to staging',
build_path: '/root/ci-mock/builds/4159',
retry_path: '/root/ci-mock/builds/4159/retry',
playable: false,
created_at: '2017-04-18T16:32:08.420Z',
updated_at: '2017-04-18T16:32:12.631Z',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4159/retry',
method: 'post',
},
},
},
],
},
],
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
has_details: true,
details_path: '/root/ci-mock/pipelines/123#deploy',
favicon:
'/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#deploy',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
},
],
artifacts: [],
manual_actions: [
{
name: 'deploy to production',
path: '/root/ci-mock/builds/4166/play',
playable: false,
},
],
},
flags: {
latest: true,
triggered: false,
stuck: false,
yaml_errors: false,
retryable: false,
cancelable: false,
},
ref: {
name: 'main',
path: '/root/ci-mock/tree/main',
tag: false,
branch: true,
},
commit: {
id: '798e5f902592192afaba73f4668ae30e56eae492',
short_id: '798e5f90',
title: "Merge branch 'new-branch' into 'main'\r",
created_at: '2017-04-13T10:25:17.000+01:00',
parent_ids: [
'54d483b1ed156fbbf618886ddf7ab023e24f8738',
'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
],
message:
"Merge branch 'new-branch' into 'main'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
author_name: 'Root',
author_email: 'admin@example.com',
authored_date: '2017-04-13T10:25:17.000+01:00',
committer_name: 'Root',
committer_email: 'admin@example.com',
committed_date: '2017-04-13T10:25:17.000+01:00',
author: {
name: 'Root',
username: 'root',
id: 1,
state: 'active',
avatar_url: null,
web_url: 'http://localhost:3000/root',
},
author_gravatar_url: null,
commit_url:
'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
},
created_at: '2017-04-13T09:25:18.881Z',
updated_at: '2017-04-19T14:30:27.561Z',
};
import { shallowMount } from '@vue/test-utils';
import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
describe('stage column component', () => {
const mockJob = {
id: 4250,
name: 'test',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4250',
action: {
icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4250/retry',
method: 'post',
},
},
};
let wrapper;
beforeEach(() => {
const mockGroups = [];
for (let i = 0; i < 3; i += 1) {
const mockedJob = { ...mockJob };
mockedJob.id += i;
mockGroups.push(mockedJob);
}
wrapper = shallowMount(StageColumnComponentLegacy, {
propsData: {
title: 'foo',
groups: mockGroups,
hasTriggeredBy: false,
},
});
});
it('should render provided title', () => {
expect(wrapper.find('.stage-name').text().trim()).toBe('foo');
});
it('should render the provided groups', () => {
expect(wrapper.findAll('.builds-container > ul > li').length).toBe(
wrapper.props('groups').length,
);
});
describe('jobId', () => {
it('escapes job name', () => {
wrapper = shallowMount(StageColumnComponentLegacy, {
propsData: {
groups: [
{
id: 4259,
name: '<img src=x onerror=alert(document.domain)>',
status: {
icon: 'status_success',
label: 'success',
tooltip: '<img src=x onerror=alert(document.domain)>',
},
},
],
title: 'test',
hasTriggeredBy: false,
},
});
expect(wrapper.find('.builds-container li').attributes('id')).toBe(
'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
);
});
});
describe('with action', () => {
it('renders action button', () => {
wrapper = shallowMount(StageColumnComponentLegacy, {
propsData: {
groups: [
{
id: 4259,
name: '<img src=x onerror=alert(document.domain)>',
status: {
icon: 'status_success',
label: 'success',
tooltip: '<img src=x onerror=alert(document.domain)>',
},
},
],
title: 'test',
hasTriggeredBy: false,
action: {
icon: 'play',
title: 'Play all',
path: 'action',
},
},
});
expect(wrapper.find('.js-stage-action').exists()).toBe(true);
});
});
describe('without action', () => {
it('does not render action button', () => {
wrapper = shallowMount(StageColumnComponentLegacy, {
propsData: {
groups: [
{
id: 4259,
name: '<img src=x onerror=alert(document.domain)>',
status: {
icon: 'status_success',
label: 'success',
tooltip: '<img src=x onerror=alert(document.domain)>',
},
},
],
title: 'test',
hasTriggeredBy: false,
},
});
expect(wrapper.find('.js-stage-action').exists()).toBe(false);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineMediator from '~/pipelines/pipeline_details_mediator';
describe('PipelineMdediator', () => {
let mediator;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mediator = new PipelineMediator({ endpoint: 'foo.json' });
});
afterEach(() => {
mock.restore();
});
it('should set defaults', () => {
expect(mediator.options).toEqual({ endpoint: 'foo.json' });
expect(mediator.state.isLoading).toEqual(false);
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
});
describe('request and store data', () => {
it('should store received data', () => {
mock.onGet('foo.json').reply(200, { id: '121123' });
mediator.fetchPipeline();
return waitForPromises().then(() => {
expect(mediator.store.state.pipeline).toEqual({ id: '121123' });
});
});
});
});
import PipelineStore from '~/pipelines/stores/pipeline_store';
describe('Pipeline Store', () => {
let store;
beforeEach(() => {
store = new PipelineStore();
});
it('should set defaults', () => {
expect(store.state.pipeline).toEqual({});
});
describe('storePipeline', () => {
it('should store empty object if none is provided', () => {
store.storePipeline();
expect(store.state.pipeline).toEqual({});
});
it('should store received object', () => {
store.storePipeline({ foo: 'bar' });
expect(store.state.pipeline).toEqual({ foo: 'bar' });
});
});
});
import PipelineStore from '~/pipelines/stores/pipeline_store';
import LinkedPipelines from '../linked_pipelines_mock.json';
describe('EE Pipeline store', () => {
let store;
let data;
beforeEach(() => {
store = new PipelineStore();
data = { ...LinkedPipelines };
store.storePipeline(data);
});
describe('storePipeline', () => {
describe('triggered_by', () => {
it('sets triggered_by as an array', () => {
expect(store.state.pipeline.triggered_by.length).toEqual(1);
});
it('adds isExpanding & isLoading keys set to false', () => {
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].isLoading).toEqual(false);
});
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);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isLoading).toEqual(false);
});
});
describe('triggered', () => {
it('adds isExpanding & isLoading keys set to false for each triggered pipeline', () => {
store.state.pipeline.triggered.forEach((pipeline) => {
expect(pipeline.isExpanded).toEqual(false);
expect(pipeline.isLoading).toEqual(false);
});
});
it('parses nested triggered pipelines', () => {
store.state.pipeline.triggered[1].triggered.forEach((pipeline) => {
expect(pipeline.isExpanded).toEqual(false);
expect(pipeline.isLoading).toEqual(false);
});
});
});
});
describe('resetTriggeredByPipeline', () => {
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;
store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
});
});
describe('openTriggeredByPipeline', () => {
it('opens the given pipeline', () => {
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
});
});
describe('closeTriggeredByPipeline', () => {
it('closes the given pipeline', () => {
// open it first
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
});
});
describe('resetTriggeredPipelines', () => {
it('closes the pipeline & nested ones', () => {
store.state.pipeline.triggered[0].isExpanded = true;
store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
store.resetTriggeredPipelines(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
});
});
describe('openTriggeredPipeline', () => {
it('opens the given pipeline', () => {
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
});
});
describe('closeTriggeredPipeline', () => {
it('closes the given pipeline', () => {
// open it first
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
});
});
describe('toggleLoading', () => {
it('toggles the isLoading property for the given pipeline', () => {
store.toggleLoading(store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isLoading).toEqual(true);
});
});
describe('addExpandedPipelineToRequestData', () => {
it('pushes the given id to expandedPipelines array', () => {
store.addExpandedPipelineToRequestData('213231');
expect(store.state.expandedPipelines).toEqual(['213231']);
});
});
describe('removeExpandedPipelineToRequestData', () => {
it('pushes the given id to expandedPipelines array', () => {
store.removeExpandedPipelineToRequestData('213231');
expect(store.state.expandedPipelines).toEqual([]);
});
});
});
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