Commit 1e83b773 authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch 'pipeline-editor/linked-pipelines' into 'master'

Show linked pipelines mini graph in the pipeline editor

See merge request gitlab-org/gitlab!72255
parents a4c58e6b a062e45a
...@@ -63,6 +63,7 @@ export default { ...@@ -63,6 +63,7 @@ export default {
v-if="showPipelineStatus" v-if="showPipelineStatus"
:commit-sha="commitSha" :commit-sha="commitSha"
:class="$options.pipelineStatusClasses" :class="$options.pipelineStatusClasses"
v-on="$listeners"
/> />
<validation-segment :class="validationStyling" :ci-config="ciConfigData" /> <validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div> </div>
......
<script> <script>
import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; 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 { export default {
i18n: {
linkedPipelinesFetchError: __('Unable to fetch upstream and downstream pipelines.'),
},
components: { components: {
PipelineMiniGraph, PipelineMiniGraph,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
}, },
inject: ['projectFullPath'],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
required: true, 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: { computed: {
downstreamPipelines() {
return this.linkedPipelines?.downstream?.nodes || [];
},
pipelinePath() { pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || ''; return this.pipeline.detailedStatus?.detailsPath || '';
}, },
...@@ -38,12 +73,29 @@ export default { ...@@ -38,12 +73,29 @@ export default {
}; };
}); });
}, },
showDownstreamPipelines() {
return this.downstreamPipelines.length > 0;
},
upstreamPipeline() {
return this.linkedPipelines?.upstream;
},
}, },
}; };
</script> </script>
<template> <template>
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5"> <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" /> <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> </div>
</template> </template>
...@@ -59,11 +59,12 @@ export default { ...@@ -59,11 +59,12 @@ export default {
}; };
}, },
update(data) { update(data) {
const { id, commitPath = '', detailedStatus = {}, stages, status } = const { id, iid, commitPath = '', detailedStatus = {}, stages, status } =
data.project?.pipeline || {}; data.project?.pipeline || {};
return { return {
id, id,
iid,
commitPath, commitPath,
detailedStatus, detailedStatus,
stages, stages,
...@@ -159,6 +160,7 @@ export default { ...@@ -159,6 +160,7 @@ export default {
<pipeline-editor-mini-graph <pipeline-editor-mini-graph
v-if="glFeatures.pipelineEditorMiniGraph" v-if="glFeatures.pipelineEditorMiniGraph"
:pipeline="pipeline" :pipeline="pipeline"
v-on="$listeners"
/> />
<gl-button <gl-button
class="gl-mt-2 gl-md-mt-0" class="gl-mt-2 gl-md-mt-0"
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
DEFAULT_FAILURE, DEFAULT_FAILURE,
DEFAULT_SUCCESS, DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN, LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
} from '../../constants'; } from '../../constants';
import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue'; import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
import { import {
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'), [DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'), [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: { successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'), [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
...@@ -74,6 +76,11 @@ export default { ...@@ -74,6 +76,11 @@ export default {
text: this.$options.errorTexts[COMMIT_FAILURE], text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger', variant: 'danger',
}; };
case PIPELINE_FAILURE:
return {
text: this.$options.errorTexts[PIPELINE_FAILURE],
variant: 'danger',
};
default: default:
return { return {
text: this.$options.errorTexts[DEFAULT_FAILURE], text: this.$options.errorTexts[DEFAULT_FAILURE],
......
...@@ -16,6 +16,7 @@ export const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; ...@@ -16,6 +16,7 @@ export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS'; export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const PIPELINE_FAILURE = 'PIPELINE_FAILURE';
export const CREATE_TAB = 'CREATE_TAB'; export const CREATE_TAB = 'CREATE_TAB';
export const LINT_TAB = 'LINT_TAB'; export const LINT_TAB = 'LINT_TAB';
......
...@@ -93,6 +93,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -93,6 +93,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciExamplesHelpPagePath, ciExamplesHelpPagePath,
ciHelpPagePath, ciHelpPagePath,
configurationPaths, configurationPaths,
dataMethod: 'graphql',
defaultBranch, defaultBranch,
emptyStateIllustrationPath, emptyStateIllustrationPath,
helpPaths, helpPaths,
......
...@@ -111,6 +111,7 @@ export default { ...@@ -111,6 +111,7 @@ export default {
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:commit-sha="commitSha" :commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile" :is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners"
/> />
<pipeline-editor-tabs <pipeline-editor-tabs
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
......
...@@ -25249,6 +25249,9 @@ msgstr "" ...@@ -25249,6 +25249,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines." msgid "Pipelines|There are currently no pipelines."
msgstr "" 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." msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr "" msgstr ""
...@@ -36295,6 +36298,9 @@ msgstr "" ...@@ -36295,6 +36298,9 @@ msgstr ""
msgid "Unable to fetch branches list, please close the form and try again" msgid "Unable to fetch branches list, please close the form and try again"
msgstr "" msgstr ""
msgid "Unable to fetch upstream and downstream pipelines."
msgstr ""
msgid "Unable to fetch vulnerable projects" msgid "Unable to fetch vulnerable projects"
msgstr "" 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 PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_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', () => { describe('Pipeline Status', () => {
let wrapper; let wrapper;
let mockApollo;
let mockLinkedPipelinesQuery;
const createComponent = ({ hasStages = true } = {}) => { const createComponent = ({ hasStages = true, options } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, { wrapper = shallowMount(PipelineEditorMiniGraph, {
provide: {
dataMethod: 'graphql',
projectFullPath: mockProjectFullPath,
},
propsData: { propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline, 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 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(() => { afterEach(() => {
mockLinkedPipelinesQuery.mockReset();
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -39,4 +75,60 @@ describe('Pipeline Status', () => { ...@@ -39,4 +75,60 @@ describe('Pipeline Status', () => {
expect(findPipelineMiniGraph().exists()).toBe(false); 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 { ...@@ -11,6 +11,7 @@ import {
DEFAULT_FAILURE, DEFAULT_FAILURE,
DEFAULT_SUCCESS, DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN, LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
} from '~/pipeline_editor/constants'; } from '~/pipeline_editor/constants';
beforeEach(() => { beforeEach(() => {
...@@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => { ...@@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => {
failureType | message | expectedFailureType failureType | message | expectedFailureType
${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE} ${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN} ${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
${PIPELINE_FAILURE} | ${'pipeline failure'} | ${PIPELINE_FAILURE}
${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE} ${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
`('shows a message for $message', ({ failureType, expectedFailureType }) => { `('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true }); createComponent({ failureType, showFailure: true });
......
...@@ -290,6 +290,62 @@ export const mockProjectPipeline = ({ hasStages = 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 = { export const mockLintResponse = {
valid: true, valid: true,
mergedYaml: mockCiYml, 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