Commit a116a750 authored by pburdette's avatar pburdette Committed by Matthias Käppler

Added linked pipelines to commit

Added linked pipelines component
to commit box. Created apollo wrapper
to fetch data for component.

Changelog: changed
EE: true
parent a71dffeb
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql';
export default {
i18n: {
linkedPipelinesFetchError: __('There was a problem fetching linked pipelines.'),
},
components: {
GlLoadingIcon,
PipelineMiniGraph,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
inject: {
fullPath: {
default: '',
},
iid: {
default: '',
},
},
props: {
stages: {
type: Array,
required: true,
},
},
apollo: {
pipeline: {
query: getLinkedPipelinesQuery,
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
skip() {
return !this.fullPath || !this.iid;
},
update({ project }) {
return project?.pipeline;
},
error() {
createFlash({ message: this.$options.i18n.linkedPipelinesFetchError });
},
},
},
data() {
return {
pipeline: null,
};
},
computed: {
hasDownstream() {
return this.pipeline?.downstream?.nodes.length > 0;
},
downstreamPipelines() {
return this.pipeline?.downstream?.nodes;
},
upstreamPipeline() {
return this.pipeline?.upstream;
},
},
};
</script>
<template>
<div>
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
<div v-else>
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="[upstreamPipeline]"
data-testid="commit-box-mini-graph-upstream"
/>
<pipeline-mini-graph
:stages="stages"
class="gl-display-inline"
data-testid="commit-box-mini-graph"
/>
<linked-pipelines-mini-list
v-if="hasDownstream"
:triggered="downstreamPipelines"
data-testid="commit-box-mini-graph-downstream"
/>
</div>
</div>
</template>
query getLinkedPipelines($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
pipeline(iid: $iid) {
downstream {
nodes {
id
path
project {
name
}
detailedStatus {
group
icon
label
}
}
}
upstream {
id
path
project {
name
}
detailedStatus {
group
icon
label
}
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => { export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
if (!el) { if (!el) {
return; return;
} }
const { stages, fullPath, iid } = el.dataset;
// Some commits have no pipeline, code splitting to load the pipeline optionally // Some commits have no pipeline, code splitting to load the pipeline optionally
const { stages } = el.dataset; const { default: CommitBoxPipelineMiniGraph } = await import(
const { default: PipelineMiniGraph } = await import( /* webpackChunkName: 'commitBoxPipelineMiniGraph' */ './components/commit_box_pipeline_mini_graph.vue'
/* webpackChunkName: 'pipelineMiniGraph' */ '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'
); );
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
apolloProvider,
provide: {
fullPath,
iid,
dataMethod: 'graphql',
},
render(createElement) { render(createElement) {
return createElement(PipelineMiniGraph, { return createElement(CommitBoxPipelineMiniGraph, {
props: { props: {
stages: JSON.parse(stages), // if stages do not exist for some reason, protect JSON.parse from erroring out
stages: stages ? JSON.parse(stages) : [],
}, },
}); });
}, },
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) } #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) }
.mr-widget-pipeline-graph .mr-widget-pipeline-graph
.stage-cell .stage-cell
.js-commit-pipeline-mini-graph{ data: { stages: @last_pipeline_stages.to_json.html_safe } } .js-commit-pipeline-mini-graph{ data: { stages: @last_pipeline_stages.to_json.html_safe, full_path: @project.full_path, iid: @last_pipeline.iid } }
- if @last_pipeline.duration - if @last_pipeline.duration
in in
= time_interval_in_words @last_pipeline.duration = time_interval_in_words @last_pipeline.duration
......
import { get } from 'lodash';
export const accessors = {
rest: {
detailedStatus: ['details', 'status'],
},
graphql: {
detailedStatus: 'detailedStatus',
},
};
export const accessValue = (pipeline, dataMethod, path) => {
return get(pipeline, accessors[dataMethod][path]);
};
<script> <script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import { accessValue } from '../accessors/linked_pipelines_accessors';
export default { export default {
directives: { directives: {
...@@ -9,6 +10,11 @@ export default { ...@@ -9,6 +10,11 @@ export default {
components: { components: {
GlIcon, GlIcon,
}, },
inject: {
dataMethod: {
default: 'rest',
},
},
props: { props: {
triggeredBy: { triggeredBy: {
type: Array, type: Array,
...@@ -64,12 +70,18 @@ export default { ...@@ -64,12 +70,18 @@ export default {
}, },
methods: { methods: {
pipelineTooltipText(pipeline) { pipelineTooltipText(pipeline) {
return `${pipeline.project.name} - ${pipeline.details.status.label}`; const { label } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `${pipeline.project.name} - ${label}`;
}, },
getStatusIcon(iconName) { getStatusIcon(pipeline) {
return `${iconName}_borderless`; const { icon } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `${icon}_borderless`;
}, },
triggerButtonClass(group) { triggerButtonClass(pipeline) {
const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `ci-status-icon-${group}`; return `ci-status-icon-${group}`;
}, },
}, },
...@@ -92,10 +104,10 @@ export default { ...@@ -92,10 +104,10 @@ export default {
:key="pipeline.id" :key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }" v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:href="pipeline.path" :href="pipeline.path"
:class="triggerButtonClass(pipeline.details.status.group)" :class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item" class="linked-pipeline-mini-item"
> >
<gl-icon :name="getStatusIcon(pipeline.details.status.icon)" /> <gl-icon :name="getStatusIcon(pipeline)" />
</a> </a>
<a <a
......
import { GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import {
mockDownstreamQueryResponse,
mockUpstreamQueryResponse,
mockUpstreamDownstreamQueryResponse,
mockStages,
} from '../mock_data';
const fullPath = 'gitlab-org/gitlab';
const iid = '315';
const localVue = createLocalVue();
localVue.use(VueApollo);
jest.mock('~/flash');
describe('Commit box pipeline mini graph', () => {
let wrapper;
const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse);
const upstreamHandler = jest.fn().mockResolvedValue(mockUpstreamQueryResponse);
const upstreamDownstreamHandler = jest
.fn()
.mockResolvedValue(mockUpstreamDownstreamQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findMiniGraph = () => wrapper.findByTestId('commit-box-mini-graph');
const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream');
const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream');
const createMockApolloProvider = (handler) => {
const requestHandlers = [[getLinkedPipelinesQuery, handler]];
return createMockApollo(requestHandlers);
};
const createComponent = (handler) => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
stages: mockStages,
},
provide: {
fullPath,
iid,
dataMethod: 'graphql',
},
localVue,
apolloProvider: createMockApolloProvider(handler),
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent(downstreamHandler);
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('loaded state', () => {
it('should not display loading state after the query is resolved', async () => {
createComponent(downstreamHandler);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(findMiniGraph().exists()).toBe(true);
});
describe.each`
handler | downstreamRenders | upstreamRenders
${downstreamHandler} | ${true} | ${false}
${upstreamHandler} | ${false} | ${true}
${upstreamDownstreamHandler} | ${true} | ${true}
`('given a linked pipeline', ({ handler, downstreamRenders, upstreamRenders }) => {
it('should render the correct linked pipelines', async () => {
createComponent(handler);
await waitForPromises();
expect(findDownstream().exists()).toBe(downstreamRenders);
expect(findUpstream().exists()).toBe(upstreamRenders);
});
});
});
describe('error state', () => {
it('createFlash should show if there is an error fetching the data', async () => {
createComponent({ handler: failedHandler });
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching linked pipelines.',
});
});
});
});
export const mockDownstreamQueryResponse = {
data: {
project: {
pipeline: {
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',
},
upstream: null,
},
__typename: 'Project',
},
},
};
export const mockUpstreamQueryResponse = {
data: {
project: {
pipeline: {
downstream: {
nodes: [],
__typename: 'PipelineConnection',
},
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',
},
},
__typename: 'Project',
},
},
};
export const mockUpstreamDownstreamQueryResponse = {
data: {
project: {
pipeline: {
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',
},
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',
},
},
__typename: 'Project',
},
},
};
export const mockStages = [
{
name: 'build',
title: 'build: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#build',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#build',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=build',
},
{
name: 'test',
title: 'test: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test',
},
{
name: 'test_two',
title: 'test_two: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test_two',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test_two',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test_two',
},
{
name: 'manual',
title: 'manual: skipped',
status: {
icon: 'status_skipped',
text: 'skipped',
label: 'skipped',
group: 'skipped',
tooltip: 'skipped',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#manual',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
action: {
icon: 'play',
title: 'Play all manual',
path: '/root/ci-project/-/pipelines/611/stages/manual/play_manual',
method: 'post',
button_title: 'Play all manual',
},
},
path: '/root/ci-project/-/pipelines/611#manual',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=manual',
},
{
name: 'deploy',
title: 'deploy: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#deploy',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#deploy',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=deploy',
},
{
name: 'qa',
title: 'qa: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#qa',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#qa',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa',
},
];
...@@ -33513,6 +33513,9 @@ msgstr "" ...@@ -33513,6 +33513,9 @@ msgstr ""
msgid "There was a problem fetching labels." msgid "There was a problem fetching labels."
msgstr "" msgstr ""
msgid "There was a problem fetching linked pipelines."
msgstr ""
msgid "There was a problem fetching milestones." msgid "There was a problem fetching milestones."
msgstr "" msgstr ""
......
...@@ -19,6 +19,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do ...@@ -19,6 +19,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
before do before do
build.run build.run
visit project_commit_path(project, project.commit.id) visit project_commit_path(project, project.commit.id)
wait_for_requests
end end
it 'display icon with status' do it 'display icon with status' do
...@@ -26,7 +27,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do ...@@ -26,7 +27,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
end end
it 'displays a mini pipeline graph' do it 'displays a mini pipeline graph' do
expect(page).to have_selector('[data-testid="pipeline-mini-graph"]') expect(page).to have_selector('[data-testid="commit-box-mini-graph"]')
first('.mini-pipeline-graph-dropdown-toggle').click first('.mini-pipeline-graph-dropdown-toggle').click
......
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
import { mockStages } from './mock_data';
describe('Commit box pipeline mini graph', () => {
let wrapper;
const findMiniGraph = () => wrapper.findByTestId('commit-box-mini-graph');
const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream');
const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream');
const createComponent = () => {
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
stages: mockStages,
},
mocks: {
$apollo: {
queries: {
pipeline: {
loading: false,
},
},
},
},
}),
);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('linked pipelines', () => {
it('should display the mini pipeine graph', () => {
expect(findMiniGraph().exists()).toBe(true);
});
it('should not display linked pipelines', () => {
expect(findUpstream().exists()).toBe(false);
expect(findDownstream().exists()).toBe(false);
});
});
});
export const mockStages = [
{
name: 'build',
title: 'build: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#build',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#build',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=build',
},
{
name: 'test',
title: 'test: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test',
},
{
name: 'test_two',
title: 'test_two: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#test_two',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#test_two',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test_two',
},
{
name: 'manual',
title: 'manual: skipped',
status: {
icon: 'status_skipped',
text: 'skipped',
label: 'skipped',
group: 'skipped',
tooltip: 'skipped',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#manual',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
action: {
icon: 'play',
title: 'Play all manual',
path: '/root/ci-project/-/pipelines/611/stages/manual/play_manual',
method: 'post',
button_title: 'Play all manual',
},
},
path: '/root/ci-project/-/pipelines/611#manual',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=manual',
},
{
name: 'deploy',
title: 'deploy: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#deploy',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#deploy',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=deploy',
},
{
name: 'qa',
title: 'qa: passed',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/root/ci-project/-/pipelines/611#qa',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
path: '/root/ci-project/-/pipelines/611#qa',
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa',
},
];
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