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 {
<div class="mr-widget-extension d-flex align-items-center pl-3">
<div v-if="hasError" class="ci-widget media">
<div class="media-body">
<span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">{{
title
}}</span>
<span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">
{{ title }}
</span>
</div>
</div>
......@@ -67,17 +67,28 @@ export default {
<gl-loading-icon v-if="isLoading" />
<gl-icon v-else :name="arrowIconName" class="js-icon" />
</button>
<template v-if="isCollapsed">
<slot name="header"></slot>
<gl-button
variant="link"
class="js-title"
data-testid="mr-collapsible-title"
:disabled="isLoading"
:class="{ 'border-0': isLoading }"
@click="toggleCollapsed"
>
<template v-if="isCollapsed">{{ title }}</template>
<template v-else>{{ __('Collapse') }}</template>
{{ title }}
</gl-button>
</template>
<gl-button
v-else
variant="link"
data-testid="mr-collapsible-title"
:disabled="isLoading"
:class="{ 'border-0': isLoading }"
@click="toggleCollapsed"
>{{ __('Collapse') }}</gl-button
>
</template>
</div>
<div v-if="!isCollapsed" class="border-top js-slot-container">
......
<script>
import { GlSprintf } from '@gitlab/ui';
import { isNumber } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ArtifactsApp from './artifacts_list_app.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
......@@ -19,6 +22,8 @@ export default {
components: {
ArtifactsApp,
Deployment: () => import('./deployment/deployment.vue'),
GlSprintf,
MrCollapsibleExtension,
MrWidgetContainer,
MrWidgetPipeline,
MergeTrainPositionIndicator: () =>
......@@ -69,6 +74,16 @@ export default {
showMergeTrainPositionIndicator() {
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>
......@@ -90,7 +105,33 @@ export default {
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
<artifacts-app :endpoint="mr.exposedArtifactsPath" />
</div>
<div v-if="deployments.length" class="mr-widget-extension">
<template v-if="deployments.length">
<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"
......@@ -101,6 +142,7 @@ export default {
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
<merge-train-position-indicator
v-if="showMergeTrainPositionIndicator"
class="mr-widget-extension"
......
---
title: Collapse deployments in merge request if many
merge_request: 55239
author:
type: changed
......@@ -10223,6 +10223,11 @@ msgstr ""
msgid "Deployments"
msgstr ""
msgid "Deployments|%{deployments} environment impacted."
msgid_plural "Deployments|%{deployments} environments impacted."
msgstr[0] ""
msgstr[1] ""
msgid "Deployment|API"
msgstr ""
......@@ -32850,6 +32855,9 @@ msgstr ""
msgid "View alert details."
msgstr ""
msgid "View all environments."
msgstr ""
msgid "View all issues"
msgstr ""
......
......@@ -48,7 +48,7 @@ describe('Merge Requests Artifacts list app', () => {
};
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 findTableRows = () => wrapper.findAll('tbody tr');
......
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
......@@ -15,12 +15,14 @@ describe('Merge Request Collapsible Extension', () => {
},
slots: {
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 findIcon = () => wrapper.find(GlIcon);
afterEach(() => {
wrapper.destroy();
......@@ -35,8 +37,12 @@ describe('Merge Request Collapsible Extension', () => {
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', () => {
expect(wrapper.vm.arrowIconName).toBe('angle-right');
expect(findIcon().props('name')).toBe('angle-right');
});
describe('onClick', () => {
......@@ -54,7 +60,7 @@ describe('Merge Request Collapsible Extension', () => {
});
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 MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
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 MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import { mockStore } from '../mock_data';
......@@ -111,4 +113,50 @@ describe('MrWidgetPipelineContainer', () => {
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