Commit 18da847f authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Sarah Groff Hennigh-Palermo

Collapse Deployments in Merge Request if Many

If there are more than 4 deployments an MR has been deployed to (either
pre- or post-merge), put them in a collapsible container and collapse it
by default.

This does require a bit of expansion on the MrCollapsibleContainer
slightly, providing a slot to add "header text", which is not blue nor
clickable. This was done without touching the current slot or props, and
should be completely backwards compatible.
parent fe0a1721
...@@ -50,9 +50,9 @@ export default { ...@@ -50,9 +50,9 @@ export default {
<div class="mr-widget-extension d-flex align-items-center pl-3"> <div class="mr-widget-extension d-flex align-items-center pl-3">
<div v-if="hasError" class="ci-widget media"> <div v-if="hasError" class="ci-widget media">
<div class="media-body"> <div class="media-body">
<span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">{{ <span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">
title {{ title }}
}}</span> </span>
</div> </div>
</div> </div>
...@@ -67,16 +67,27 @@ export default { ...@@ -67,16 +67,27 @@ export default {
<gl-loading-icon v-if="isLoading" /> <gl-loading-icon v-if="isLoading" />
<gl-icon v-else :name="arrowIconName" class="js-icon" /> <gl-icon v-else :name="arrowIconName" class="js-icon" />
</button> </button>
<template v-if="isCollapsed">
<slot name="header"></slot>
<gl-button
variant="link"
data-testid="mr-collapsible-title"
:disabled="isLoading"
:class="{ 'border-0': isLoading }"
@click="toggleCollapsed"
>
{{ title }}
</gl-button>
</template>
<gl-button <gl-button
v-else
variant="link" variant="link"
class="js-title" data-testid="mr-collapsible-title"
:disabled="isLoading" :disabled="isLoading"
:class="{ 'border-0': isLoading }" :class="{ 'border-0': isLoading }"
@click="toggleCollapsed" @click="toggleCollapsed"
>{{ __('Collapse') }}</gl-button
> >
<template v-if="isCollapsed">{{ title }}</template>
<template v-else>{{ __('Collapse') }}</template>
</gl-button>
</template> </template>
</div> </div>
......
<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 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 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';
...@@ -19,6 +22,8 @@ export default { ...@@ -19,6 +22,8 @@ export default {
components: { components: {
ArtifactsApp, ArtifactsApp,
Deployment: () => import('./deployment/deployment.vue'), Deployment: () => import('./deployment/deployment.vue'),
GlSprintf,
MrCollapsibleExtension,
MrWidgetContainer, MrWidgetContainer,
MrWidgetPipeline, MrWidgetPipeline,
MergeTrainPositionIndicator: () => MergeTrainPositionIndicator: () =>
...@@ -69,6 +74,16 @@ export default { ...@@ -69,6 +74,16 @@ export default {
showMergeTrainPositionIndicator() { showMergeTrainPositionIndicator() {
return isNumber(this.mr.mergeTrainIndex); return isNumber(this.mr.mergeTrainIndex);
}, },
showCollapsedDeployments() {
return this.deployments.length > 3;
},
multipleDeploymentsTitle() {
return n__(
'Deployments|%{deployments} environment impacted.',
'Deployments|%{deployments} environments impacted.',
this.deployments.length,
);
},
}, },
}; };
</script> </script>
...@@ -90,17 +105,44 @@ export default { ...@@ -90,17 +105,44 @@ 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>
<div v-if="deployments.length" class="mr-widget-extension"> <template v-if="deployments.length">
<deployment <mr-collapsible-extension
v-for="deployment in deployments" v-if="showCollapsedDeployments"
:key="deployment.id" :title="__('View all environments.')"
:class="deploymentClass" data-testid="mr-collapsed-deployments"
:deployment="deployment" >
:show-metrics="hasDeploymentMetrics" <template #header>
:show-visual-review-app="showVisualReviewAppLink" <div class="gl-mr-3 gl-line-height-normal">
:visual-review-app-meta="visualReviewAppMeta" <gl-sprintf :message="multipleDeploymentsTitle">
/> <template #deployments>
</div> <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"
......
---
title: Collapse deployments in merge request if many
merge_request: 55239
author:
type: changed
...@@ -10223,6 +10223,11 @@ msgstr "" ...@@ -10223,6 +10223,11 @@ msgstr ""
msgid "Deployments" msgid "Deployments"
msgstr "" msgstr ""
msgid "Deployments|%{deployments} environment impacted."
msgid_plural "Deployments|%{deployments} environments impacted."
msgstr[0] ""
msgstr[1] ""
msgid "Deployment|API" msgid "Deployment|API"
msgstr "" msgstr ""
...@@ -32850,6 +32855,9 @@ msgstr "" ...@@ -32850,6 +32855,9 @@ msgstr ""
msgid "View alert details." msgid "View alert details."
msgstr "" msgstr ""
msgid "View all environments."
msgstr ""
msgid "View all issues" msgid "View all issues"
msgstr "" msgstr ""
......
...@@ -48,7 +48,7 @@ describe('Merge Requests Artifacts list app', () => { ...@@ -48,7 +48,7 @@ describe('Merge Requests Artifacts list app', () => {
}; };
const findButtons = () => wrapper.findAll('button'); const findButtons = () => wrapper.findAll('button');
const findTitle = () => wrapper.find('.js-title'); const findTitle = () => wrapper.find('[data-testid="mr-collapsible-title"]');
const findErrorMessage = () => wrapper.find('.js-error-state'); const findErrorMessage = () => wrapper.find('.js-error-state');
const findTableRows = () => wrapper.findAll('tbody tr'); const findTableRows = () => wrapper.findAll('tbody tr');
......
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue'; import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
...@@ -15,12 +15,14 @@ describe('Merge Request Collapsible Extension', () => { ...@@ -15,12 +15,14 @@ describe('Merge Request Collapsible Extension', () => {
}, },
slots: { slots: {
default: '<div class="js-slot">Foo</div>', default: '<div class="js-slot">Foo</div>',
header: '<span data-testid="collapsed-header">hello there</span>',
}, },
}); });
}; };
const findTitle = () => wrapper.find('.js-title'); const findTitle = () => wrapper.find('[data-testid="mr-collapsible-title"]');
const findErrorMessage = () => wrapper.find('.js-error-state'); const findErrorMessage = () => wrapper.find('.js-error-state');
const findIcon = () => wrapper.find(GlIcon);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -35,8 +37,12 @@ describe('Merge Request Collapsible Extension', () => { ...@@ -35,8 +37,12 @@ describe('Merge Request Collapsible Extension', () => {
expect(findTitle().text()).toBe(data.title); expect(findTitle().text()).toBe(data.title);
}); });
it('renders the header slot', () => {
expect(wrapper.find('[data-testid="collapsed-header"]').text()).toBe('hello there');
});
it('renders angle-right icon', () => { it('renders angle-right icon', () => {
expect(wrapper.vm.arrowIconName).toBe('angle-right'); expect(findIcon().props('name')).toBe('angle-right');
}); });
describe('onClick', () => { describe('onClick', () => {
...@@ -54,7 +60,7 @@ describe('Merge Request Collapsible Extension', () => { ...@@ -54,7 +60,7 @@ describe('Merge Request Collapsible Extension', () => {
}); });
it('renders angle-down icon', () => { it('renders angle-down icon', () => {
expect(wrapper.vm.arrowIconName).toBe('angle-down'); expect(findIcon().props('name')).toBe('angle-down');
}); });
}); });
}); });
......
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 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';
...@@ -111,4 +113,50 @@ describe('MrWidgetPipelineContainer', () => { ...@@ -111,4 +113,50 @@ 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('');
});
});
});
}); });
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