Commit 8d07beae authored by Matthias Käppler's avatar Matthias Käppler

Merge branch '36118-add-linked-pipelines-to-commit-info-box' into 'master'

Added linked pipelines to commit box on commit [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!66564
parents 70b72322 a116a750
<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 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') => {
const el = document.querySelector(selector);
if (!el) {
return;
}
const { stages, fullPath, iid } = el.dataset;
// Some commits have no pipeline, code splitting to load the pipeline optionally
const { stages } = el.dataset;
const { default: PipelineMiniGraph } = await import(
/* webpackChunkName: 'pipelineMiniGraph' */ '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'
const { default: CommitBoxPipelineMiniGraph } = await import(
/* webpackChunkName: 'commitBoxPipelineMiniGraph' */ './components/commit_box_pipeline_mini_graph.vue'
);
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
provide: {
fullPath,
iid,
dataMethod: 'graphql',
},
render(createElement) {
return createElement(PipelineMiniGraph, {
return createElement(CommitBoxPipelineMiniGraph, {
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 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) }
.mr-widget-pipeline-graph
.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
in
= 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>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { accessValue } from '../accessors/linked_pipelines_accessors';
export default {
directives: {
......@@ -9,6 +10,11 @@ export default {
components: {
GlIcon,
},
inject: {
dataMethod: {
default: 'rest',
},
},
props: {
triggeredBy: {
type: Array,
......@@ -64,12 +70,18 @@ export default {
},
methods: {
pipelineTooltipText(pipeline) {
return `${pipeline.project.name} - ${pipeline.details.status.label}`;
const { label } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
return `${pipeline.project.name} - ${label}`;
},
getStatusIcon(iconName) {
return `${iconName}_borderless`;
getStatusIcon(pipeline) {
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}`;
},
},
......@@ -92,10 +104,10 @@ export default {
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:href="pipeline.path"
:class="triggerButtonClass(pipeline.details.status.group)"
:class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item"
>
<gl-icon :name="getStatusIcon(pipeline.details.status.icon)" />
<gl-icon :name="getStatusIcon(pipeline)" />
</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',
},
];
......@@ -33519,6 +33519,9 @@ msgstr ""
msgid "There was a problem fetching labels."
msgstr ""
msgid "There was a problem fetching linked pipelines."
msgstr ""
msgid "There was a problem fetching milestones."
msgstr ""
......
......@@ -19,6 +19,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
before do
build.run
visit project_commit_path(project, project.commit.id)
wait_for_requests
end
it 'display icon with status' do
......@@ -26,7 +27,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
end
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
......
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