Commit 6f73930f authored by Miguel Rincon's avatar Miguel Rincon

Create single component for mini pipeline

Previously the mini pipeline had to be created for each situation by
rendering each stage separately. This change wraps multiple stages in
a single component.
parent 608675f7
<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,21 +218,14 @@ 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"
:update-dropdown="updateGraphDropdown"
/>
</div>
</template>
<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>
......
......@@ -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,20 +211,12 @@ 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"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="handlePipelineActionRequestComplete"
/>
</div>
</template>
<pipeline-mini-graph
v-if="hasStages"
:stages="pipeline.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="handlePipelineActionRequestComplete"
/>
</div>
</div>
......
......@@ -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({
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
buildsWithCoverage: mockData.buildsWithCoverage,
});
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