Commit 3a0a841c authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch '300403-refactor-pipeline-mini-graph' into 'master'

Create single component for Pipeline Mini Graph

See merge request gitlab-org/gitlab!55199
parents e397f989 6f73930f
<script>
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
/**
* Renders the pipeline mini graph.
*/
export default {
components: {
PipelineStage,
},
props: {
stages: {
type: Array,
required: true,
},
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
stagesClass: {
type: [Array, Object, String],
required: false,
default: '',
},
isMergeTrain: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
onPipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
},
};
</script>
<template>
<div data-testid="widget-mini-pipeline-graph">
<div
v-for="stage in stages"
:key="stage.name"
:class="stagesClass"
class="stage-container dropdown"
>
<pipeline-stage
:stage="stage"
:update-dropdown="updateDropdown"
:is-merge-train="isMergeTrain"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
</div>
</div>
</template>
......@@ -11,6 +11,7 @@
* 3. Merge request widget
* 4. Commit widget
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
......
......@@ -3,6 +3,7 @@ import { GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
......@@ -10,7 +11,6 @@ import PipelineUrl from './pipeline_url.vue';
import PipelinesCommit from './pipelines_commit.vue';
import PipelinesStatusBadge from './pipelines_status_badge.vue';
import PipelinesTableRowComponent from './pipelines_table_row.vue';
import PipelineStage from './stage.vue';
import PipelinesTimeago from './time_ago.vue';
const DEFAULT_TD_CLASS = 'gl-p-5!';
......@@ -79,8 +79,8 @@ export default {
components: {
GlTable,
PipelinesCommit,
PipelineMiniGraph,
PipelineOperations,
PipelineStage,
PipelinesStatusBadge,
PipelineStopModal,
PipelinesTableRowComponent,
......@@ -141,6 +141,9 @@ export default {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
},
onPipelineActionRequestComplete() {
eventHub.$emit('refreshPipelinesTable');
},
},
};
</script>
......@@ -215,23 +218,16 @@ export default {
<template #cell(stages)="{ item }">
<div class="stage-cell">
<!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 -->
<div></div>
<template v-if="item.details.stages.length > 0">
<div
v-for="(stage, index) in item.details.stages"
:key="index"
class="stage-container dropdown"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
<pipeline-mini-graph
v-if="item.details && item.details.stages && item.details.stages.length > 0"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
</div>
</template>
</div>
</template>
<template #cell(timeago)="{ item }">
<pipelines-timeago :pipeline="item" />
......
......@@ -4,11 +4,11 @@ import { __ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelineUrl from './pipeline_url.vue';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import PipelinesManualActionsComponent from './pipelines_manual_actions.vue';
import PipelineStage from './stage.vue';
import PipelinesTimeago from './time_ago.vue';
export default {
......@@ -24,7 +24,7 @@ export default {
PipelinesManualActionsComponent,
PipelinesArtifactsComponent,
CommitComponent,
PipelineStage,
PipelineMiniGraph,
PipelineUrl,
PipelineTriggerer,
CiBadge,
......@@ -134,6 +134,9 @@ export default {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
hasStages() {
return this.pipeline?.details?.stages?.length > 0;
},
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
......@@ -208,21 +211,13 @@ export default {
<div class="table-section section-wrap section-15 stage-cell">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Stages') }}</div>
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
<div
v-for="(stage, index) in pipeline.details.stages"
:key="index"
class="stage-container dropdown"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage
:stage="stage"
<pipeline-mini-graph
v-if="hasStages"
:stages="pipeline.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="handlePipelineActionRequestComplete"
/>
</div>
</template>
</div>
</div>
<pipelines-timeago class="gl-text-right" :pipeline="pipeline" />
......
......@@ -11,8 +11,8 @@ import {
} from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
import { s__, n__ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
......@@ -27,7 +27,7 @@ export default {
GlSprintf,
GlTooltip,
PipelineArtifacts,
PipelineStage,
PipelineMiniGraph,
TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
......@@ -100,9 +100,7 @@ export default {
: {};
},
hasStages() {
return (
this.pipeline.details && this.pipeline.details.stages && this.pipeline.details.stages.length
);
return this.pipeline?.details?.stages?.length > 0;
},
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
......@@ -251,16 +249,13 @@ export default {
<span class="mr-widget-pipeline-graph">
<span class="stage-cell">
<linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" />
<template v-if="hasStages">
<div
v-for="(stage, i) in pipeline.details.stages"
:key="i"
class="stage-container dropdown mr-widget-pipeline-stages"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage :stage="stage" :is-merge-train="isMergeTrain" />
</div>
</template>
<pipeline-mini-graph
v-if="hasStages"
class="gl-display-inline-block"
stages-class="mr-widget-pipeline-stages"
:stages="pipeline.details.stages"
:is-merge-train="isMergeTrain"
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
<pipeline-artifacts
......
import { shallowMount } from '@vue/test-utils';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
const { pipelines } = getJSONFixture('pipelines/pipelines.json');
const mockStages = pipelines[0].details.stages;
describe('Pipeline Mini Graph', () => {
let wrapper;
const findPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineStagesAt = (i) => findPipelineStages().at(i);
const createComponent = (props = {}) => {
wrapper = shallowMount(PipelineMiniGraph, {
propsData: {
stages: mockStages,
...props,
},
});
};
it('renders stages', () => {
createComponent();
expect(findPipelineStages()).toHaveLength(mockStages.length);
});
it('renders stages with a custom class', () => {
createComponent({ stagesClass: 'my-class' });
expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length);
});
it('does not fail when stages are empty', () => {
createComponent({ stages: [] });
expect(wrapper.exists()).toBe(true);
expect(findPipelineStages()).toHaveLength(0);
});
it('triggers events in "action request complete" in stages', () => {
createComponent();
findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete');
findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete');
expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
});
it('update dropdown is false by default', () => {
createComponent();
expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(false);
expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(false);
});
it('update dropdown is set to true', () => {
createComponent({ updateDropdown: true });
expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(true);
expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(true);
});
it('is merge train is false by default', () => {
createComponent();
expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(false);
expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(false);
});
it('is merge train is set to true', () => {
createComponent({ isMergeTrain: true });
expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true);
expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
});
......@@ -2,9 +2,9 @@ import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
import { stageReply } from './mock_data';
import { stageReply } from '../../mock_data';
const dropdownPath = 'path.json';
......@@ -13,7 +13,7 @@ describe('Pipelines stage component', () => {
let mock;
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
wrapper = mount(PipelineStage, {
attachTo: document.body,
propsData: {
stage: {
......
......@@ -147,15 +147,22 @@ describe('Pipelines Table Row', () => {
});
describe('stages column', () => {
beforeEach(() => {
const findAllMiniPipelineStages = () =>
wrapper.findAll('.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown"]');
it('should render an icon for each stage', () => {
wrapper = createWrapper(pipeline);
expect(findAllMiniPipelineStages()).toHaveLength(pipeline.details.stages.length);
});
it('should render an icon for each stage', () => {
const stages = wrapper.findAll(
'.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown"]',
);
expect(stages).toHaveLength(pipeline.details.stages.length);
it('should not render stages when stages are empty', () => {
const withoutStages = { ...pipeline };
withoutStages.details = { ...withoutStages.details, stages: null };
wrapper = createWrapper(withoutStages);
expect(findAllMiniPipelineStages()).toHaveLength(0);
});
});
......
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import eventHub from '~/pipelines/event_hub';
import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub');
describe('Pipelines Table', () => {
let pipeline;
let wrapper;
......@@ -20,10 +25,18 @@ describe('Pipelines Table', () => {
viewType: 'root',
};
const createComponent = (props = defaultProps, flagState = false) => {
const createMockPipeline = () => {
const { pipelines } = getJSONFixture(jsonFixtureName);
return pipelines.find((p) => p.user !== null && p.commit !== null);
};
const createComponent = (props = {}, flagState = false) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: props,
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
newPipelinesTable: flagState,
......@@ -39,6 +52,7 @@ describe('Pipelines Table', () => {
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
......@@ -53,8 +67,7 @@ describe('Pipelines Table', () => {
const findActionsTh = () => wrapper.findByTestId('actions-th');
beforeEach(() => {
const { pipelines } = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
pipeline = createMockPipeline();
});
afterEach(() => {
......@@ -165,6 +178,48 @@ describe('Pipelines Table', () => {
});
});
describe('stages cell', () => {
it('should render a pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true);
});
it('should render the right number of stages', () => {
const stagesLength = pipeline.details.stages.length;
expect(
findPipelineMiniGraph().findAll('[data-testid="mini-pipeline-graph-dropdown"]'),
).toHaveLength(stagesLength);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
pipeline.details.stages = null;
createComponent({ pipelines: [pipeline] }, true);
});
it('stages are not rendered', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
it('should not update dropdown', () => {
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(false);
});
it('when update graph dropdown is set, should update graph dropdown', () => {
createComponent({ pipelines: [pipeline], updateGraphDropdown: true }, true);
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true);
});
it('when action request is complete, should refresh table', () => {
findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
});
});
describe('duration cell', () => {
it('should render duration information', () => {
expect(findTimeAgo().exists()).toBe(true);
......
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
import mockData from '../mock_data';
......@@ -25,7 +26,7 @@ describe('MRWidgetPipeline', () => {
const findPipelineID = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineInfoContainer = () => wrapper.find('[data-testid="pipeline-info-container"]');
const findCommitLink = () => wrapper.find('[data-testid="commit-link"]');
const findPipelineGraph = () => wrapper.find('[data-testid="widget-mini-pipeline-graph"]');
const findPipelineMiniGraph = () => wrapper.find(PipelineMiniGraph);
const findAllPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineCoverage = () => wrapper.find('[data-testid="pipeline-coverage"]');
const findPipelineCoverageDelta = () => wrapper.find('[data-testid="pipeline-coverage-delta"]');
......@@ -35,7 +36,7 @@ describe('MRWidgetPipeline', () => {
wrapper.find('[data-testid="monitoring-pipeline-message"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createWrapper = (props, mountFn = shallowMount) => {
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(PipelineComponent, {
propsData: {
...defaultProps,
......@@ -65,10 +66,13 @@ describe('MRWidgetPipeline', () => {
describe('with a pipeline', () => {
beforeEach(() => {
createWrapper({
createWrapper(
{
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
buildsWithCoverage: mockData.buildsWithCoverage,
});
},
mount,
);
});
it('should render pipeline ID', () => {
......@@ -84,8 +88,8 @@ describe('MRWidgetPipeline', () => {
});
it('should render pipeline graph', () => {
expect(findPipelineGraph().exists()).toBe(true);
expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
expect(findPipelineMiniGraph().exists()).toBe(true);
expect(findAllPipelineStages()).toHaveLength(mockData.pipeline.details.stages.length);
});
describe('should render pipeline coverage information', () => {
......@@ -136,7 +140,7 @@ describe('MRWidgetPipeline', () => {
const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.commit;
createWrapper({});
createWrapper({}, mount);
});
it('should render pipeline ID', () => {
......@@ -147,9 +151,15 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
});
it('should render pipeline graph', () => {
expect(findPipelineGraph().exists()).toBe(true);
expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
it('should render pipeline graph with correct styles', () => {
const stagesCount = mockData.pipeline.details.stages.length;
expect(findPipelineMiniGraph().exists()).toBe(true);
expect(findPipelineMiniGraph().findAll('.mr-widget-pipeline-stages')).toHaveLength(
stagesCount,
);
expect(findAllPipelineStages()).toHaveLength(stagesCount);
});
it('should render coverage information', () => {
......@@ -181,7 +191,7 @@ describe('MRWidgetPipeline', () => {
});
it('should not render a pipeline graph', () => {
expect(findPipelineGraph().exists()).toBe(false);
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
......
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