Commit ffff712d authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Reduces EE difference for pipeline graph component

parent 69b4c091
<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 './stage_column_component.vue';
export default {
components: {
LinkedPipelinesColumn,
StageColumnComponent,
GlLoadingIcon,
},
mixins: [EEGraphMixin],
props: {
isLoading: {
type: Boolean,
......@@ -51,9 +47,6 @@ export default {
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
},
};
</script>
......@@ -62,79 +55,17 @@ export default {
<div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div class="text-center"><gl-loading-icon v-if="isLoading" :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"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')"
graph-position="left"
@linkedPipelineClick="pipeline => $emit('onClickTriggeredBy', pipeline)"
/>
<ul
v-if="!isLoading"
:class="{
'has-linked-pipelines': hasTriggered || hasTriggeredBy,
}"
class="stage-column-list align-top"
>
<ul v-if="!isLoading" class="stage-column-list">
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
'has-upstream': index === 0 && hasTriggeredBy,
'has-downstream': index === graph.length - 1 && hasTriggered,
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggered"
:linked-pipelines="triggeredPipelines"
:column-title="__('Downstream')"
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
/>
<ul
v-if="shouldRenderTriggeredPipeline"
class="d-inline-block downstream-pipeline position-relative align-top"
: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"
/>
</ul>
</div>
</div>
</template>
......@@ -3,7 +3,7 @@ import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import PipelinesMediator from 'ee/pipelines/pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineGraph from 'ee/pipelines/components/graph/graph_component.vue';
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
......
<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';
export default {
components: {
LinkedPipelinesColumn,
StageColumnComponent,
GlLoadingIcon,
},
mixins: [EEGraphMixin],
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
},
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
},
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
isFirstColumn(index) {
return index === 0;
},
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;
},
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
},
};
</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>
<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"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')"
graph-position="left"
@linkedPipelineClick="pipeline => $emit('onClickTriggeredBy', pipeline)"
/>
<ul
v-if="!isLoading"
:class="{
'has-linked-pipelines': hasTriggered || hasTriggeredBy,
}"
class="stage-column-list align-top"
>
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
'has-upstream': index === 0 && hasTriggeredBy,
'has-downstream': index === graph.length - 1 && hasTriggered,
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggered"
:linked-pipelines="triggeredPipelines"
:column-title="__('Downstream')"
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
/>
<ul
v-if="shouldRenderTriggeredPipeline"
class="d-inline-block downstream-pipeline position-relative align-top"
: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"
/>
</ul>
</div>
</div>
</template>
---
title: Creates an EE component for the pipeline graph
merge_request:
author:
type: other
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
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,
});
describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent);
let component;
afterEach(() => {
component.$destroy();
});
describe('while is loading', () => {
it('should render a loading icon', () => {
component = mountComponent(GraphComponent, {
isLoading: true,
pipeline: {},
});
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
});
});
describe('when linked pipelines are present', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: linkedPipelineJSON.triggered,
});
});
describe('rendered output', () => {
it('should include the pipelines graph', () => {
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
});
it('should not include the loading icon', () => {
expect(component.$el.querySelector('.fa-spinner')).toBeNull();
});
it('should include the stage column list', () => {
expect(component.$el.querySelector('.stage-column-list')).not.toBeNull();
});
it('should include the no-margin class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
});
it('should include the has-only-one-job class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
});
it('should include the left-margin class on the second child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column:last-child',
);
expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
});
it('should include the has-linked-pipelines flag', () => {
expect(component.$el.querySelector('.has-linked-pipelines')).not.toBeNull();
});
});
describe('computeds and methods', () => {
describe('capitalizeStageName', () => {
it('it capitalizes the stage name', () => {
expect(component.capitalizeStageName('mystage')).toBe('Mystage');
});
});
describe('stageConnectorClass', () => {
it('it returns left-margin when there is a triggerer', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
});
});
describe('linked pipelines components', () => {
it('should render an upstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Upstream');
});
it('should render a downstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Downstream');
});
describe('triggered by', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-129').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
linkedPipelineJSON.triggered_by,
);
});
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,
});
expect(component.$el.querySelector('.upstream-pipeline')).not.toBeNull();
});
});
});
describe('triggered ', () => {
it('should emit `onClickTriggered` when triggered linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-132').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
linkedPipelineJSON.triggered[0],
);
});
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],
});
expect(component.$el.querySelector('.downstream-pipeline')).not.toBeNull();
});
});
});
});
});
describe('when linked pipelines are not present', () => {
beforeEach(() => {
const pipeline = Object.assign(graphJSON, { triggered: null, triggered_by: null });
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline,
});
});
describe('rendered output', () => {
it('should include the first column with a no margin', () => {
const firstColumn = component.$el.querySelector('.stage-column:first-child');
expect(firstColumn.classList.contains('no-margin')).toEqual(true);
});
it('should not render a linked pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).toBeNull();
});
});
describe('stageConnectorClass', () => {
it('it returns left-margin when no triggerer and there is one job', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
it('it returns left-margin when no triggerer and not the first stage', () => {
expect(component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
});
});
});
describe('capitalizeStageName', () => {
it('capitalizes and escapes stage name', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
});
expect(
component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(),
).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
});
});
});
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import graphComponent from '~/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 graphJSON from './mock_data';
describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent);
......@@ -28,170 +22,32 @@ describe('graph component', () => {
});
});
describe('when linked pipelines are present', () => {
beforeEach(() => {
describe('with data', () => {
it('should render the graph', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
triggeredByPipelines: [linkedPipelineJSON.triggered_by],
triggeredPipelines: linkedPipelineJSON.triggered,
});
});
describe('rendered output', () => {
it('should include the pipelines graph', () => {
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
});
it('should not include the loading icon', () => {
expect(component.$el.querySelector('.fa-spinner')).toBeNull();
});
it('should include the stage column list', () => {
expect(component.$el.querySelector('.stage-column-list')).not.toBeNull();
});
it('should include the no-margin class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
});
it('should include the has-only-one-job class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
});
it('should include the left-margin class on the second child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column:last-child',
);
expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
});
it('should include the has-linked-pipelines flag', () => {
expect(component.$el.querySelector('.has-linked-pipelines')).not.toBeNull();
});
});
describe('computeds and methods', () => {
describe('capitalizeStageName', () => {
it('it capitalizes the stage name', () => {
expect(component.capitalizeStageName('mystage')).toBe('Mystage');
});
});
describe('stageConnectorClass', () => {
it('it returns left-margin when there is a triggerer', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
});
});
describe('linked pipelines components', () => {
it('should render an upstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Upstream');
});
it('should render a downstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Downstream');
});
describe('triggered by', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-129').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
linkedPipelineJSON.triggered_by,
);
});
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,
});
expect(component.$el.querySelector('.upstream-pipeline')).not.toBeNull();
});
});
});
describe('triggered ', () => {
it('should emit `onClickTriggered` when triggered linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-132').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
linkedPipelineJSON.triggered[0],
);
});
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],
});
expect(component.$el.querySelector('.downstream-pipeline')).not.toBeNull();
});
});
});
});
});
describe('when linked pipelines are not present', () => {
beforeEach(() => {
const pipeline = Object.assign(graphJSON, { triggered: null, triggered_by: null });
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline,
});
});
describe('rendered output', () => {
it('should include the first column with a no margin', () => {
const firstColumn = component.$el.querySelector('.stage-column:first-child');
expect(
component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
).toEqual(true);
expect(firstColumn.classList.contains('no-margin')).toEqual(true);
});
expect(
component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
).toEqual(true);
it('should not render a linked pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).toBeNull();
});
});
expect(
component.$el
.querySelector('.stage-column:nth-child(2) .build:nth-child(1)')
.classList.contains('left-connector'),
).toEqual(true);
describe('stageConnectorClass', () => {
it('it returns left-margin when no triggerer and there is one job', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
expect(component.$el.querySelector('loading-icon')).toBe(null);
it('it returns left-margin when no triggerer and not the first stage', () => {
expect(component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
});
expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
});
});
......
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