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 {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
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;
export const i18n = {
......@@ -30,7 +32,9 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
PipelineEditorMiniGraph,
},
mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
......@@ -55,12 +59,15 @@ export default {
};
},
update(data) {
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
const { id, commitPath = '', detailedStatus = {}, stages, status } =
data.project?.pipeline || {};
return {
id,
commitPath,
detailedStatus,
stages,
status,
};
},
result(res) {
......@@ -111,9 +118,7 @@ export default {
</script>
<template>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full"
>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
<template v-if="showLoadingState">
<div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
......@@ -129,19 +134,12 @@ export default {
<template v-else>
<div>
<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>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
<gl-link
:href="status.detailsPath"
class="pipeline-id gl-font-weight-normal pipeline-number"
target="_blank"
data-testid="pipeline-id"
>
{{ content }}{{ pipelineId }}</gl-link
>
<span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
......@@ -157,8 +155,13 @@ export default {
</gl-sprintf>
</span>
</div>
<div>
<div class="gl-display-flex gl-flex-wrap">
<pipeline-editor-mini-graph
v-if="glFeatures.pipelineEditorMiniGraph"
:pipeline="pipeline"
/>
<gl-button
class="gl-mt-2 gl-md-mt-0"
target="_blank"
category="secondary"
variant="confirm"
......
......@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) {
group
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
before_action :check_can_collaborate!
before_action do
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)
end
......
- add_page_specific_style 'page_bundles/pipelines'
- page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do
- 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';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
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';
const localVue = createLocalVue();
localVue.use(VueApollo);
const mockProvide = {
projectFullPath: mockProjectFullPath,
};
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockPipelineQuery;
const createComponentWithApollo = () => {
const createComponentWithApollo = (glFeatures = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
......@@ -30,19 +26,23 @@ describe('Pipeline Status', () => {
propsData: {
commitSha: mockCommitSha,
},
provide: mockProvide,
provide: {
glFeatures,
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
beforeEach(() => {
mockPipelineQuery = jest.fn();
......@@ -50,9 +50,7 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
wrapper.destroy();
wrapper = null;
});
describe('loading icon', () => {
......@@ -73,13 +71,13 @@ describe('Pipeline Status', () => {
describe('when querying data', () => {
describe('when data is set', () => {
beforeEach(async () => {
beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
data: { project: mockProjectPipeline },
data: { project: mockProjectPipeline() },
});
createComponentWithApollo();
await waitForPromises();
waitForPromises();
});
it('query is called with correct variables', async () => {
......@@ -91,20 +89,24 @@ describe('Pipeline Status', () => {
});
it('does not render error', () => {
expect(findIcon().exists()).toBe(false);
expect(findPipelineErrorMsg().exists()).toBe(false);
});
it('renders pipeline data', () => {
const {
id,
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(findPipelineCommit().text()).toBe(mockCommitSha);
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', () => {
......@@ -121,11 +123,26 @@ describe('Pipeline Status', () => {
});
it('does not render pipeline data', () => {
expect(findCiIcon().exists()).toBe(false);
expect(findStatusIcon().exists()).toBe(false);
expect(findPipelineId().exists()).toBe(false);
expect(findPipelineCommit().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,7 +247,32 @@ export const mockEmptySearchBranches = {
export const mockBranchPaginationLimit = 10;
export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination
export const mockProjectPipeline = {
export const mockProjectPipeline = ({ hasStages = true } = {}) => {
const stages = hasStages
? {
edges: [
{
node: {
id: 'gid://gitlab/Ci::Stage/605',
name: 'prepare',
status: 'success',
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/268#prepare',
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',
......@@ -255,12 +280,14 @@ export const mockProjectPipeline = {
shortSha: mockCommitSha,
status: 'SUCCESS',
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/118"',
detailsPath: '/root/sample-ci-project/-/pipelines/118',
group: 'success',
icon: 'status_success',
text: 'passed',
},
stages,
},
};
};
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