Commit 7b74c999 authored by Andrew Fontaine's avatar Andrew Fontaine

Encapsulate Deployment List in Own Component

In 18da847f, the ability to collapse deployments on a merge request if
there were many was added. This made the pipeline component grow
slightly too large in its responsibilities.

The extra behaviour has been moved to the new deployments list
component. I think more might be able to be moved, but I wanted to avoid
taking in the whole merge request as a `prop`. I would be ameniable to
moving more props lower though.
parent 24414956
<script>
import { GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import MrCollapsibleExtension from '../mr_collapsible_extension.vue';
export default {
components: {
Deployment: () => import('./deployment.vue'),
GlSprintf,
MrCollapsibleExtension,
},
props: {
deployments: {
type: Array,
required: true,
},
deploymentClass: {
type: String,
required: true,
},
hasDeploymentMetrics: {
type: Boolean,
required: true,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
showVisualReviewAppLink: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
showCollapsedDeployments() {
return this.deployments.length > 3;
},
multipleDeploymentsTitle() {
return n__(
'Deployments|%{deployments} environment impacted.',
'Deployments|%{deployments} environments impacted.',
this.deployments.length,
);
},
},
};
</script>
<template>
<mr-collapsible-extension
v-if="showCollapsedDeployments"
:title="__('View all environments.')"
data-testid="mr-collapsed-deployments"
>
<template #header>
<div class="gl-mr-3 gl-line-height-normal">
<gl-sprintf :message="multipleDeploymentsTitle">
<template #deployments>
<span class="gl-font-weight-bold gl-mr-2">{{ deployments.length }}</span>
</template>
</gl-sprintf>
</div>
</template>
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
class="gl-bg-gray-50"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</mr-collapsible-extension>
<div v-else class="mr-widget-extension">
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
<script> <script>
import { GlSprintf } from '@gitlab/ui';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ArtifactsApp from './artifacts_list_app.vue'; import ArtifactsApp from './artifacts_list_app.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue'; import DeploymentList from './deployment/deployment_list.vue';
import MrWidgetContainer from './mr_widget_container.vue'; import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue';
...@@ -21,9 +20,7 @@ export default { ...@@ -21,9 +20,7 @@ export default {
name: 'MrWidgetPipelineContainer', name: 'MrWidgetPipelineContainer',
components: { components: {
ArtifactsApp, ArtifactsApp,
Deployment: () => import('./deployment/deployment.vue'), DeploymentList,
GlSprintf,
MrCollapsibleExtension,
MrWidgetContainer, MrWidgetContainer,
MrWidgetPipeline, MrWidgetPipeline,
MergeTrainPositionIndicator: () => MergeTrainPositionIndicator: () =>
...@@ -69,7 +66,9 @@ export default { ...@@ -69,7 +66,9 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
}, },
showVisualReviewAppLink() { showVisualReviewAppLink() {
return this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback; return Boolean(
this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback,
);
}, },
showMergeTrainPositionIndicator() { showMergeTrainPositionIndicator() {
return isNumber(this.mr.mergeTrainIndex); return isNumber(this.mr.mergeTrainIndex);
...@@ -105,44 +104,14 @@ export default { ...@@ -105,44 +104,14 @@ export default {
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts"> <div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
<artifacts-app :endpoint="mr.exposedArtifactsPath" /> <artifacts-app :endpoint="mr.exposedArtifactsPath" />
</div> </div>
<template v-if="deployments.length"> <deployment-list
<mr-collapsible-extension v-if="deployments.length"
v-if="showCollapsedDeployments" :deployments="deployments"
:title="__('View all environments.')" :deployment-class="deploymentClass"
data-testid="mr-collapsed-deployments" :has-deployment-metrics="hasDeploymentMetrics"
> :visual-review-app-meta="visualReviewAppMeta"
<template #header> :show-visual-review-app-link="showVisualReviewAppLink"
<div class="gl-mr-3 gl-line-height-normal"> />
<gl-sprintf :message="multipleDeploymentsTitle">
<template #deployments>
<span class="gl-font-weight-bold gl-mr-2">{{ deployments.length }}</span>
</template>
</gl-sprintf>
</div>
</template>
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
class="gl-bg-gray-50"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</mr-collapsible-extension>
<div v-else class="mr-widget-extension">
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
<merge-train-position-indicator <merge-train-position-indicator
v-if="showMergeTrainPositionIndicator" v-if="showMergeTrainPositionIndicator"
class="mr-widget-extension" class="mr-widget-extension"
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue'; import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import Deployment from '~/vue_merge_request_widget/components/deployment/deployment.vue'; import DeploymentList from '~/vue_merge_request_widget/components/deployment/deployment_list.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue'; import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import { mockStore } from '../mock_data'; import { mockStore } from '../mock_data';
...@@ -30,6 +29,8 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -30,6 +29,8 @@ describe('MrWidgetPipelineContainer', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findDeploymentList = () => wrapper.findComponent(DeploymentList);
describe('when pre merge', () => { describe('when pre merge', () => {
beforeEach(() => { beforeEach(() => {
factory(); factory();
...@@ -57,6 +58,9 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -57,6 +58,9 @@ describe('MrWidgetPipelineContainer', () => {
const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment'); const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
expect(findDeploymentList().exists()).toBe(true);
expect(findDeploymentList().props('deployments')).toBe(mockStore.deployments);
expect(deployments.wrappers.map((x) => x.props())).toEqual(expectedProps); expect(deployments.wrappers.map((x) => x.props())).toEqual(expectedProps);
}); });
}); });
...@@ -102,6 +106,8 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -102,6 +106,8 @@ describe('MrWidgetPipelineContainer', () => {
const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment'); const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
expect(findDeploymentList().exists()).toBe(true);
expect(findDeploymentList().props('deployments')).toBe(mockStore.postMergeDeployments);
expect(deployments.wrappers.map((x) => x.props())).toEqual(expectedProps); expect(deployments.wrappers.map((x) => x.props())).toEqual(expectedProps);
}); });
}); });
...@@ -113,50 +119,4 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -113,50 +119,4 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true); expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
}); });
}); });
describe('with many deployments', () => {
let deployments;
let collapsibleExtension;
beforeEach(() => {
deployments = [
...mockStore.deployments,
...mockStore.deployments.map((deployment) => ({
...deployment,
id: deployment.id + mockStore.deployments.length,
})),
];
factory({
mr: {
...mockStore,
deployments,
},
});
collapsibleExtension = wrapper.find('[data-testid="mr-collapsed-deployments"]');
});
it('renders them collapsed', () => {
expect(collapsibleExtension.exists()).toBe(true);
expect(trimText(collapsibleExtension.text())).toBe(
`${deployments.length} environments impacted. View all environments.`,
);
});
it('shows them when clicked', async () => {
const expectedProps = deployments.map((dep) =>
expect.objectContaining({
deployment: dep,
showMetrics: false,
}),
);
await collapsibleExtension.find('button').trigger('click');
const deploymentWrappers = collapsibleExtension.findAllComponents(Deployment);
expect(deploymentWrappers.wrappers.map((x) => x.props())).toEqual(expectedProps);
deploymentWrappers.wrappers.forEach((x) => {
expect(x.text()).toEqual(expect.any(String));
expect(x.text()).not.toBe('');
});
});
});
}); });
import { mount } from '@vue/test-utils';
import { zip } from 'lodash';
import { trimText } from 'helpers/text_helper';
import Deployment from '~/vue_merge_request_widget/components/deployment/deployment.vue';
import DeploymentList from '~/vue_merge_request_widget/components/deployment/deployment_list.vue';
import MrCollapsibleExtension from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
import { mockStore } from '../mock_data';
const DEFAULT_PROPS = {
showVisualReviewAppLink: false,
hasDeploymentMetrics: false,
deploymentClass: 'js-pre-deployment',
};
describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue', () => {
let wrapper;
let propsData;
const factory = (props = {}) => {
propsData = {
...DEFAULT_PROPS,
deployments: mockStore.deployments,
...props,
};
wrapper = mount(DeploymentList, {
propsData,
});
};
afterEach(() => {
wrapper?.destroy?.();
wrapper = null;
});
describe('with few deployments', () => {
beforeEach(() => {
factory();
});
it('shows all deployments', () => {
const deploymentWrappers = wrapper.findAllComponents(Deployment);
expect(wrapper.findComponent(MrCollapsibleExtension).exists()).toBe(false);
expect(deploymentWrappers).toHaveLength(propsData.deployments.length);
zip(deploymentWrappers.wrappers, propsData.deployments).forEach(
([deploymentWrapper, deployment]) => {
expect(deploymentWrapper.props('deployment')).toEqual(deployment);
expect(deploymentWrapper.props()).toMatchObject({
showVisualReviewApp: DEFAULT_PROPS.showVisualReviewAppLink,
showMetrics: DEFAULT_PROPS.hasDeploymentMetrics,
});
expect(deploymentWrapper.classes(DEFAULT_PROPS.deploymentClass)).toBe(true);
expect(deploymentWrapper.text()).toEqual(expect.any(String));
expect(deploymentWrapper.text()).not.toBe('');
},
);
});
});
describe('with many deployments', () => {
let deployments;
let collapsibleExtension;
beforeEach(() => {
deployments = [
...mockStore.deployments,
...mockStore.deployments.map((deployment) => ({
...deployment,
id: deployment.id + mockStore.deployments.length,
})),
];
factory({ deployments });
collapsibleExtension = wrapper.findComponent(MrCollapsibleExtension);
});
it('shows collapsed deployments', () => {
expect(collapsibleExtension.exists()).toBe(true);
expect(trimText(collapsibleExtension.text())).toBe(
`${deployments.length} environments impacted. View all environments.`,
);
});
it('shows all deployments on click', async () => {
await collapsibleExtension.find('button').trigger('click');
const deploymentWrappers = wrapper.findAllComponents(Deployment);
expect(deploymentWrappers).toHaveLength(deployments.length);
zip(deploymentWrappers.wrappers, propsData.deployments).forEach(
([deploymentWrapper, deployment]) => {
expect(deploymentWrapper.props('deployment')).toEqual(deployment);
expect(deploymentWrapper.props()).toMatchObject({
showVisualReviewApp: DEFAULT_PROPS.showVisualReviewAppLink,
showMetrics: DEFAULT_PROPS.hasDeploymentMetrics,
});
expect(deploymentWrapper.classes(DEFAULT_PROPS.deploymentClass)).toBe(true);
expect(deploymentWrapper.text()).toEqual(expect.any(String));
expect(deploymentWrapper.text()).not.toBe('');
},
);
});
});
});
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