Commit a062e45a authored by Mireya Andres's avatar Mireya Andres Committed by Frédéric Caplette

Show linked pipelines mini graph in the pipeline editor

parent 53dbde8b
......@@ -63,6 +63,7 @@ export default {
v-if="showPipelineStatus"
:commit-sha="commitSha"
:class="$options.pipelineStatusClasses"
v-on="$listeners"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
......
<script>
import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '../../constants';
export default {
i18n: {
linkedPipelinesFetchError: __('Unable to fetch upstream and downstream pipelines.'),
},
components: {
PipelineMiniGraph,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
inject: ['projectFullPath'],
props: {
pipeline: {
type: Object,
required: true,
},
},
apollo: {
linkedPipelines: {
query: getLinkedPipelinesQuery,
variables() {
return {
fullPath: this.projectFullPath,
iid: this.pipeline.iid,
};
},
skip() {
return !this.pipeline.iid;
},
update({ project }) {
return project?.pipeline;
},
error() {
this.$emit('showError', {
type: PIPELINE_FAILURE,
reasons: [this.$options.i18n.linkedPipelinesFetchError],
});
},
},
},
computed: {
downstreamPipelines() {
return this.linkedPipelines?.downstream?.nodes || [];
},
pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || '';
},
......@@ -38,12 +73,29 @@ export default {
};
});
},
showDownstreamPipelines() {
return this.downstreamPipelines.length > 0;
},
upstreamPipeline() {
return this.linkedPipelines?.upstream;
},
},
};
</script>
<template>
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="[upstreamPipeline]"
data-testid="pipeline-editor-mini-graph-upstream"
/>
<pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
<linked-pipelines-mini-list
v-if="showDownstreamPipelines"
:triggered="downstreamPipelines"
:pipeline-path="pipelinePath"
data-testid="pipeline-editor-mini-graph-downstream"
/>
</div>
</template>
......@@ -59,11 +59,12 @@ export default {
};
},
update(data) {
const { id, commitPath = '', detailedStatus = {}, stages, status } =
const { id, iid, commitPath = '', detailedStatus = {}, stages, status } =
data.project?.pipeline || {};
return {
id,
iid,
commitPath,
detailedStatus,
stages,
......@@ -159,6 +160,7 @@ export default {
<pipeline-editor-mini-graph
v-if="glFeatures.pipelineEditorMiniGraph"
:pipeline="pipeline"
v-on="$listeners"
/>
<gl-button
class="gl-mt-2 gl-md-mt-0"
......
......@@ -8,6 +8,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
} from '../../constants';
import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
import {
......@@ -24,6 +25,7 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
[PIPELINE_FAILURE]: s__('Pipelines|There was a problem with loading the pipeline data.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
......@@ -74,6 +76,11 @@ export default {
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
case PIPELINE_FAILURE:
return {
text: this.$options.errorTexts[PIPELINE_FAILURE],
variant: 'danger',
};
default:
return {
text: this.$options.errorTexts[DEFAULT_FAILURE],
......
......@@ -16,6 +16,7 @@ export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const PIPELINE_FAILURE = 'PIPELINE_FAILURE';
export const CREATE_TAB = 'CREATE_TAB';
export const LINT_TAB = 'LINT_TAB';
......
......@@ -93,6 +93,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciExamplesHelpPagePath,
ciHelpPagePath,
configurationPaths,
dataMethod: 'graphql',
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
......
......@@ -58,6 +58,7 @@ export default {
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
......
......@@ -25249,6 +25249,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|There was a problem with loading the pipeline data."
msgstr ""
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr ""
......@@ -36292,6 +36295,9 @@ msgstr ""
msgid "Unable to fetch branches list, please close the form and try again"
msgstr ""
msgid "Unable to fetch upstream and downstream pipelines."
msgstr ""
msgid "Unable to fetch vulnerable projects"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import { mockProjectPipeline } from '../../mock_data';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockLinkedPipelinesQuery;
const createComponent = ({ hasStages = true } = {}) => {
const createComponent = ({ hasStages = true, options } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, {
provide: {
dataMethod: 'graphql',
projectFullPath: mockProjectFullPath,
},
propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline,
},
...options,
});
};
const createComponentWithApollo = (hasStages = true) => {
const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
mockApollo = createMockApollo(handlers);
createComponent({
hasStages,
options: {
localVue,
apolloProvider: mockApollo,
},
});
};
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findUpstream = () => wrapper.find('[data-testid="pipeline-editor-mini-graph-upstream"]');
const findDownstream = () =>
wrapper.find('[data-testid="pipeline-editor-mini-graph-downstream"]');
beforeEach(() => {
mockLinkedPipelinesQuery = jest.fn();
});
afterEach(() => {
mockLinkedPipelinesQuery.mockReset();
wrapper.destroy();
});
......@@ -39,4 +75,60 @@ describe('Pipeline Status', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
describe('when querying upstream and downstream pipelines', () => {
describe('when query succeeds', () => {
beforeEach(() => {
mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
createComponentWithApollo();
});
it('should call the query with the correct variables', () => {
expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
fullPath: mockProjectFullPath,
iid: mockProjectPipeline().pipeline.iid,
});
});
describe('linked pipeline rendering based on given data', () => {
it.each`
hasDownstream | hasUpstream | downstreamRenderAction | upstreamRenderAction
${true} | ${true} | ${'renders'} | ${'renders'}
${true} | ${false} | ${'renders'} | ${'hides'}
${false} | ${true} | ${'hides'} | ${'renders'}
${false} | ${false} | ${'hides'} | ${'hides'}
`(
'$downstreamRenderAction downstream and $upstreamRenderAction upstream',
async ({ hasDownstream, hasUpstream }) => {
mockLinkedPipelinesQuery.mockResolvedValue(
mockLinkedPipelines({ hasDownstream, hasUpstream }),
);
createComponentWithApollo();
await waitForPromises();
expect(findUpstream().exists()).toBe(hasUpstream);
expect(findDownstream().exists()).toBe(hasDownstream);
},
);
});
});
describe('when query fails', () => {
beforeEach(() => {
mockLinkedPipelinesQuery.mockRejectedValue(new Error());
createComponentWithApollo();
});
it('should emit an error event when query fails', async () => {
expect(wrapper.emitted('showError')).toHaveLength(1);
expect(wrapper.emitted('showError')[0]).toEqual([
{
type: PIPELINE_FAILURE,
reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
},
]);
});
});
});
});
......@@ -11,6 +11,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
} from '~/pipeline_editor/constants';
beforeEach(() => {
......@@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => {
failureType | message | expectedFailureType
${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
${PIPELINE_FAILURE} | ${'pipeline failure'} | ${PIPELINE_FAILURE}
${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
`('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true });
......
......@@ -290,6 +290,62 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => {
};
};
export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true } = {}) => {
let upstream = null;
let downstream = {
nodes: [],
__typename: 'PipelineConnection',
};
if (hasDownstream) {
downstream = {
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
project: { name: 'job-log-sections', __typename: 'Project' },
detailedStatus: {
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
},
],
__typename: 'PipelineConnection',
};
}
if (hasUpstream) {
upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
project: { name: 'trigger-downstream', __typename: 'Project' },
detailedStatus: {
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
__typename: 'Pipeline',
};
}
return {
data: {
project: {
pipeline: {
path: '/root/ci-project/-/pipelines/790',
downstream,
upstream,
},
__typename: 'Project',
},
},
};
};
export const mockLintResponse = {
valid: true,
mergedYaml: mockCiYml,
......
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