Commit 27260307 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'afontaine/collapse-deployments-in-merge-requests' into 'master'

Collapse Deployments in Merge Request if Many

See merge request gitlab-org/gitlab!55239
parents 5c23eaa5 18da847f
...@@ -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,17 +67,28 @@ export default { ...@@ -67,17 +67,28 @@ 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 <gl-button
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"
> >
<template v-if="isCollapsed">{{ title }}</template> {{ title }}
<template v-else>{{ __('Collapse') }}</template>
</gl-button> </gl-button>
</template> </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>
<div v-if="!isCollapsed" class="border-top js-slot-container"> <div v-if="!isCollapsed" class="border-top js-slot-container">
......
<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,7 +105,33 @@ export default { ...@@ -90,7 +105,33 @@ 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">
<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 <deployment
v-for="deployment in deployments" v-for="deployment in deployments"
:key="deployment.id" :key="deployment.id"
...@@ -101,6 +142,7 @@ export default { ...@@ -101,6 +142,7 @@ export default {
:visual-review-app-meta="visualReviewAppMeta" :visual-review-app-meta="visualReviewAppMeta"
/> />
</div> </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
...@@ -10226,6 +10226,11 @@ msgstr "" ...@@ -10226,6 +10226,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 ""
...@@ -32874,6 +32879,9 @@ msgstr "" ...@@ -32874,6 +32879,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