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
);
},
},
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
triggeredByPipelines() {
return this.pipeline.triggered_by;
},
isFirstColumn(index) {
return index === 0;
hasTriggered() {
return (
this.type !== this.$options.upstream &&
this.triggeredPipelines &&
this.pipeline.triggered.length > 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"
/>
</ul>
<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"
/>
<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"
/>
</ul>
:mediator="mediator"
@onClickTriggered="
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</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`);
}
}
......@@ -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', () => {
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('on click', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-12').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
component.pipeline,
component.pipeline.triggered_by[0],
);
});
});
describe('with expanded triggered by pipeline', () => {
it('should render expanded upstream pipeline', () => {
describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => {
// expand the pipeline
store.state.pipeline.triggered_by[0].isExpanded = true;
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', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-132').click();
describe('triggered', () => {
describe('on click', () => {
it('should emit `onClickTriggered`', () => {
spyOn(component, '$emit');
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
linkedPipelineJSON.triggered[0],
);
component.$el.querySelector('#js-linked-pipeline-34993051').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
component.pipeline,
component.pipeline.triggered[0],
);
});
});
describe('with expanded triggered pipeline', () => {
it('should render expanded downstream pipeline', () => {
describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => {
// expand the pipeline
store.state.pipeline.triggered[0].isExpanded = true;
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.
......@@ -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