Commit 87b66bf4 authored by Mireya Andres's avatar Mireya Andres Committed by Nicolò Maria Mezzopera

Show pipeline mini graph in pipeline editor

parent 39a234b1
<script>
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
export default {
components: {
PipelineMiniGraph,
},
props: {
pipeline: {
type: Object,
required: true,
},
},
computed: {
pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || '';
},
pipelineStages() {
const stages = this.pipeline.stages?.edges;
if (!stages) {
return [];
}
return stages.map(({ node }) => {
const { name, detailedStatus } = node;
return {
// TODO: fetch dropdown_path from graphql when available
// see https://gitlab.com/gitlab-org/gitlab/-/issues/342585
dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`,
name,
path: `${this.pipelinePath}#${name}`,
status: {
details_path: `${this.pipelinePath}#${name}`,
has_details: detailedStatus.hasDetails,
...detailedStatus,
},
title: `${name}: ${detailedStatus.text}`,
};
});
},
},
};
</script>
<template>
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
<pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
</div>
</template>
...@@ -10,6 +10,8 @@ import { ...@@ -10,6 +10,8 @@ import {
toggleQueryPollingByVisibility, toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils'; } from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000; const POLL_INTERVAL = 10000;
export const i18n = { export const i18n = {
...@@ -30,7 +32,9 @@ export default { ...@@ -30,7 +32,9 @@ export default {
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
PipelineEditorMiniGraph,
}, },
mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'], inject: ['projectFullPath'],
props: { props: {
commitSha: { commitSha: {
...@@ -55,12 +59,15 @@ export default { ...@@ -55,12 +59,15 @@ export default {
}; };
}, },
update(data) { update(data) {
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {}; const { id, commitPath = '', detailedStatus = {}, stages, status } =
data.project?.pipeline || {};
return { return {
id, id,
commitPath, commitPath,
detailedStatus, detailedStatus,
stages,
status,
}; };
}, },
result(res) { result(res) {
...@@ -111,9 +118,7 @@ export default { ...@@ -111,9 +118,7 @@ export default {
</script> </script>
<template> <template>
<div <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full"
>
<template v-if="showLoadingState"> <template v-if="showLoadingState">
<div> <div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" /> <gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
...@@ -129,19 +134,12 @@ export default { ...@@ -129,19 +134,12 @@ export default {
<template v-else> <template v-else>
<div> <div>
<a :href="status.detailsPath" class="gl-mr-auto"> <a :href="status.detailsPath" class="gl-mr-auto">
<ci-icon :status="status" :size="16" /> <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
</a> </a>
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo"> <gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }"> <template #id="{ content }">
<gl-link <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
:href="status.detailsPath"
class="pipeline-id gl-font-weight-normal pipeline-number"
target="_blank"
data-testid="pipeline-id"
>
{{ content }}{{ pipelineId }}</gl-link
>
</template> </template>
<template #status>{{ status.text }}</template> <template #status>{{ status.text }}</template>
<template #commit> <template #commit>
...@@ -157,8 +155,13 @@ export default { ...@@ -157,8 +155,13 @@ export default {
</gl-sprintf> </gl-sprintf>
</span> </span>
</div> </div>
<div> <div class="gl-display-flex gl-flex-wrap">
<pipeline-editor-mini-graph
v-if="glFeatures.pipelineEditorMiniGraph"
:pipeline="pipeline"
/>
<gl-button <gl-button
class="gl-mt-2 gl-md-mt-0"
target="_blank" target="_blank"
category="secondary" category="secondary"
variant="confirm" variant="confirm"
......
...@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) { ...@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) {
group group
text text
} }
stages {
edges {
node {
id
name
status
detailedStatus {
detailsPath
group
hasDetails
icon
id
label
text
tooltip
}
}
}
}
} }
} }
} }
...@@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate! before_action :check_can_collaborate!
before_action do before_action do
push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml) push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end end
......
- add_page_specific_style 'page_bundles/pipelines'
- page_title s_('Pipelines|Pipeline Editor') - page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do - content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco') - webpack_preload_asset_tag('monaco')
......
---
name: pipeline_editor_mini_graph
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217
milestone: '14.4'
type: development
group: group::pipeline authoring
default_enabled: false
...@@ -5,22 +5,18 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -5,22 +5,18 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue'; import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql'; import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data'; import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const mockProvide = {
projectFullPath: mockProjectFullPath,
};
describe('Pipeline Status', () => { describe('Pipeline Status', () => {
let wrapper; let wrapper;
let mockApollo; let mockApollo;
let mockPipelineQuery; let mockPipelineQuery;
const createComponentWithApollo = () => { const createComponentWithApollo = (glFeatures = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]]; const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers); mockApollo = createMockApollo(handlers);
...@@ -30,19 +26,23 @@ describe('Pipeline Status', () => { ...@@ -30,19 +26,23 @@ describe('Pipeline Status', () => {
propsData: { propsData: {
commitSha: mockCommitSha, commitSha: mockCommitSha,
}, },
provide: mockProvide, provide: {
glFeatures,
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf }, stubs: { GlLink, GlSprintf },
}); });
}; };
const findIcon = () => wrapper.findComponent(GlIcon); const findIcon = () => wrapper.findComponent(GlIcon);
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]'); const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]'); const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]'); const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]'); const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]'); const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
beforeEach(() => { beforeEach(() => {
mockPipelineQuery = jest.fn(); mockPipelineQuery = jest.fn();
...@@ -50,9 +50,7 @@ describe('Pipeline Status', () => { ...@@ -50,9 +50,7 @@ describe('Pipeline Status', () => {
afterEach(() => { afterEach(() => {
mockPipelineQuery.mockReset(); mockPipelineQuery.mockReset();
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('loading icon', () => { describe('loading icon', () => {
...@@ -73,13 +71,13 @@ describe('Pipeline Status', () => { ...@@ -73,13 +71,13 @@ describe('Pipeline Status', () => {
describe('when querying data', () => { describe('when querying data', () => {
describe('when data is set', () => { describe('when data is set', () => {
beforeEach(async () => { beforeEach(() => {
mockPipelineQuery.mockResolvedValue({ mockPipelineQuery.mockResolvedValue({
data: { project: mockProjectPipeline }, data: { project: mockProjectPipeline() },
}); });
createComponentWithApollo(); createComponentWithApollo();
await waitForPromises(); waitForPromises();
}); });
it('query is called with correct variables', async () => { it('query is called with correct variables', async () => {
...@@ -91,20 +89,24 @@ describe('Pipeline Status', () => { ...@@ -91,20 +89,24 @@ describe('Pipeline Status', () => {
}); });
it('does not render error', () => { it('does not render error', () => {
expect(findIcon().exists()).toBe(false); expect(findPipelineErrorMsg().exists()).toBe(false);
}); });
it('renders pipeline data', () => { it('renders pipeline data', () => {
const { const {
id, id,
detailedStatus: { detailsPath }, detailedStatus: { detailsPath },
} = mockProjectPipeline.pipeline; } = mockProjectPipeline().pipeline;
expect(findCiIcon().exists()).toBe(true); expect(findStatusIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`); expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
expect(findPipelineCommit().text()).toBe(mockCommitSha); expect(findPipelineCommit().text()).toBe(mockCommitSha);
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath); expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
}); });
it('does not render the pipeline mini graph', () => {
expect(findPipelineEditorMiniGraph().exists()).toBe(false);
});
}); });
describe('when data cannot be fetched', () => { describe('when data cannot be fetched', () => {
...@@ -121,11 +123,26 @@ describe('Pipeline Status', () => { ...@@ -121,11 +123,26 @@ describe('Pipeline Status', () => {
}); });
it('does not render pipeline data', () => { it('does not render pipeline data', () => {
expect(findCiIcon().exists()).toBe(false); expect(findStatusIcon().exists()).toBe(false);
expect(findPipelineId().exists()).toBe(false); expect(findPipelineId().exists()).toBe(false);
expect(findPipelineCommit().exists()).toBe(false); expect(findPipelineCommit().exists()).toBe(false);
expect(findPipelineViewBtn().exists()).toBe(false); expect(findPipelineViewBtn().exists()).toBe(false);
}); });
}); });
}); });
describe('when feature flag for pipeline mini graph is enabled', () => {
beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
data: { project: mockProjectPipeline() },
});
createComponentWithApollo({ pipelineEditorMiniGraph: true });
waitForPromises();
});
it('renders the pipeline mini graph', () => {
expect(findPipelineEditorMiniGraph().exists()).toBe(true);
});
});
}); });
import { shallowMount } from '@vue/test-utils';
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';
describe('Pipeline Status', () => {
let wrapper;
const createComponent = ({ hasStages = true } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, {
propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline,
},
});
};
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
afterEach(() => {
wrapper.destroy();
});
describe('when there are stages', () => {
beforeEach(() => {
createComponent();
});
it('renders pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
});
describe('when there are no stages', () => {
beforeEach(() => {
createComponent({ hasStages: false });
});
it('does not render pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
});
...@@ -247,20 +247,47 @@ export const mockEmptySearchBranches = { ...@@ -247,20 +247,47 @@ export const mockEmptySearchBranches = {
export const mockBranchPaginationLimit = 10; export const mockBranchPaginationLimit = 10;
export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination
export const mockProjectPipeline = { export const mockProjectPipeline = ({ hasStages = true } = {}) => {
pipeline: { const stages = hasStages
commitPath: '/-/commit/aabbccdd', ? {
id: 'gid://gitlab/Ci::Pipeline/118', edges: [
iid: '28', {
shortSha: mockCommitSha, node: {
status: 'SUCCESS', id: 'gid://gitlab/Ci::Stage/605',
detailedStatus: { name: 'prepare',
detailsPath: '/root/sample-ci-project/-/pipelines/118"', status: 'success',
group: 'success', detailedStatus: {
icon: 'status_success', detailsPath: '/root/sample-ci-project/-/pipelines/268#prepare',
text: 'passed', group: 'success',
hasDetails: true,
icon: 'status_success',
id: 'success-605-605',
label: 'passed',
text: 'passed',
tooltip: 'passed',
},
},
},
],
}
: null;
return {
pipeline: {
commitPath: '/-/commit/aabbccdd',
id: 'gid://gitlab/Ci::Pipeline/118',
iid: '28',
shortSha: mockCommitSha,
status: 'SUCCESS',
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/118',
group: 'success',
icon: 'status_success',
text: 'passed',
},
stages,
}, },
}, };
}; };
export const mockLintResponse = { export const mockLintResponse = {
......
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