Commit 67857334 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Ezekiel Kigbo

Add delete segment functionality to devops adoption

We add a popover to the table and then a standalone
confirmation modal.
parent b3941ef8
<script>
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID } from '../constants';
import deleteDevopsAdoptionSegmentMutation from '../graphql/mutations/delete_devops_adoption_segment.mutation.graphql';
import { deleteSegmentFromCache } from '../utils/cache_updates';
export default {
name: 'DevopsAdoptionDeleteModal',
components: { GlModal, GlSprintf, GlAlert },
i18n: DEVOPS_ADOPTION_STRINGS.deleteModal,
devopsSegmentDeleteModalId: DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID,
props: {
segment: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
errors: [],
};
},
computed: {
cancelOptions() {
return {
text: this.$options.i18n.cancel,
attributes: [{ disabled: this.loading }],
};
},
primaryOptions() {
return {
text: this.$options.i18n.confirm,
attributes: [
{
variant: 'danger',
loading: this.loading,
},
],
};
},
displayError() {
return this.errors[0];
},
},
methods: {
async deleteSegment() {
try {
const {
segment: { id },
} = this;
this.loading = true;
const {
data: {
deleteDevopsAdoptionSegment: { errors },
},
} = await this.$apollo.mutate({
mutation: deleteDevopsAdoptionSegmentMutation,
variables: {
id,
},
update(store) {
deleteSegmentFromCache(store, id);
},
});
if (errors.length) {
this.errors = errors;
} else {
this.$refs.modal.hide();
}
} catch (error) {
this.errors.push(this.$options.i18n.error);
Sentry.captureException(error);
} finally {
this.loading = false;
}
},
clearErrors() {
this.errors = [];
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="$options.devopsSegmentDeleteModalId"
size="sm"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary.prevent="deleteSegment"
>
<template #modal-title>{{ $options.i18n.title }}</template>
<gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors">
{{ displayError }}
</gl-alert>
<gl-sprintf :message="$options.i18n.confirmationMessage">
<template #name
><strong>{{ segment.name }}</strong></template
>
</gl-sprintf>
</gl-modal>
</template>
<script>
import { GlTable, GlButton } from '@gitlab/ui';
import { GlTable, GlButton, GlPopover, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import DevopsAdoptionTableCellFlag from './devops_adoption_table_cell_flag.vue';
import { DEVOPS_ADOPTION_TABLE_TEST_IDS } from '../constants';
import DevopsAdoptionDeleteModal from './devops_adoption_delete_modal.vue';
import {
DEVOPS_ADOPTION_TABLE_TEST_IDS,
DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID,
} from '../constants';
const fieldOptions = {
thClass: 'gl-bg-white! gl-text-gray-400',
......@@ -11,7 +16,18 @@ const fieldOptions = {
export default {
name: 'DevopsAdoptionTable',
components: { GlTable, DevopsAdoptionTableCellFlag, GlButton },
components: {
GlTable,
DevopsAdoptionTableCellFlag,
GlButton,
GlPopover,
DevopsAdoptionDeleteModal,
},
i18n: DEVOPS_ADOPTION_STRINGS.table,
devopsSegmentDeleteModalId: DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID,
directives: {
GlModal: GlModalDirective,
},
tableHeaderFields: [
{
key: 'name',
......@@ -67,81 +83,116 @@ export default {
required: true,
},
},
data() {
return {
selectedSegment: null,
};
},
methods: {
popoverContainerId(name) {
return `popover_container_id_for_${name}`;
},
popoverId(name) {
return `popover_id_for_${name}`;
},
setSelectedSegment(segment) {
this.selectedSegment = segment;
},
},
};
</script>
<template>
<gl-table
:fields="$options.tableHeaderFields"
:items="segments"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
stacked="sm"
>
<template #cell(name)="{ item }">
<div :data-testid="$options.testids.SEGMENT">
<strong>{{ item.name }}</strong>
</div>
</template>
<div>
<gl-table
:fields="$options.tableHeaderFields"
:items="segments"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
stacked="sm"
>
<template #cell(name)="{ item }">
<div :data-testid="$options.testids.SEGMENT">
<strong>{{ item.name }}</strong>
</div>
</template>
<template #cell(issueOpened)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.ISSUES"
:enabled="item.latestSnapshot.issueOpened"
/>
</template>
<template #cell(issueOpened)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.ISSUES"
:enabled="item.latestSnapshot.issueOpened"
/>
</template>
<template #cell(mergeRequestOpened)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.MRS"
:enabled="item.latestSnapshot.mergeRequestOpened"
/>
</template>
<template #cell(mergeRequestOpened)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.MRS"
:enabled="item.latestSnapshot.mergeRequestOpened"
/>
</template>
<template #cell(mergeRequestApproved)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.APPROVALS"
:enabled="item.latestSnapshot.mergeRequestApproved"
/>
</template>
<template #cell(mergeRequestApproved)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.APPROVALS"
:enabled="item.latestSnapshot.mergeRequestApproved"
/>
</template>
<template #cell(runnerConfigured)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.RUNNERS"
:enabled="item.latestSnapshot.runnerConfigured"
/>
</template>
<template #cell(runnerConfigured)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.RUNNERS"
:enabled="item.latestSnapshot.runnerConfigured"
/>
</template>
<template #cell(pipelineSucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.PIPELINES"
:enabled="item.latestSnapshot.pipelineSucceeded"
/>
</template>
<template #cell(pipelineSucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.PIPELINES"
:enabled="item.latestSnapshot.pipelineSucceeded"
/>
</template>
<template #cell(deploySucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.DEPLOYS"
:enabled="item.latestSnapshot.deploySucceeded"
/>
</template>
<template #cell(deploySucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.DEPLOYS"
:enabled="item.latestSnapshot.deploySucceeded"
/>
</template>
<template #cell(securityScanSucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.SCANNING"
:enabled="item.latestSnapshot.securityScanSucceeded"
/>
</template>
<template #cell(securityScanSucceeded)="{ item }">
<devops-adoption-table-cell-flag
v-if="item.latestSnapshot"
:data-testid="$options.testids.SCANNING"
:enabled="item.latestSnapshot.securityScanSucceeded"
/>
</template>
<template #cell(actions)>
<div :data-testid="$options.testids.ACTIONS">
<gl-button category="tertiary" icon="ellipsis_h" />
</div>
</template>
</gl-table>
<template #cell(actions)="{ item }">
<div :data-testid="$options.testids.ACTIONS">
<gl-button :id="popoverId(item.name)" category="tertiary" icon="ellipsis_h" />
<div :id="popoverContainerId(item.name)">
<gl-popover
:target="popoverId(item.name)"
:container="popoverContainerId(item.name)"
triggers="hover focus"
placement="left"
>
<gl-button
v-gl-modal="$options.devopsSegmentDeleteModalId"
category="tertiary"
variant="danger"
@click="setSelectedSegment(item)"
>{{ $options.i18n.deleteButton }}</gl-button
>
</gl-popover>
</div>
</div>
</template>
</gl-table>
<devops-adoption-delete-modal v-if="selectedSegment" :segment="selectedSegment" />
</div>
</template>
......@@ -4,6 +4,8 @@ export const MAX_REQUEST_COUNT = 10;
export const DEVOPS_ADOPTION_SEGMENT_MODAL_ID = 'devopsSegmentModal';
export const DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID = 'devopsSegmentDeleteModal';
export const DATE_TIME_FORMAT = 'yyyy-mm-dd HH:MM';
export const DEVOPS_ADOPTION_ERROR_KEYS = {
......@@ -43,6 +45,16 @@ export const DEVOPS_ADOPTION_STRINGS = {
selectedGroupsTextPlural: s__('DevopsAdoption|%{selectedCount} groups selected (20 max)'),
error: s__('DevopsAdoption|An error occured while saving the segment. Please try again.'),
},
table: {
deleteButton: s__('DevopsAdoption|Delete segment'),
},
deleteModal: {
title: s__('DevopsAdoption|Confirm delete segment'),
confirmationMessage: s__('DevopsAdoption|Are you sure that you would like to delete %{name}?'),
cancel: __('Cancel'),
confirm: s__('DevopsAdoption|Delete segment'),
error: s__('DevopsAdoption|An error occured while deleting the segment. Please try again.'),
},
};
export const DEVOPS_ADOPTION_TABLE_TEST_IDS = {
......
mutation($id: AnalyticsDevopsAdoptionSegmentID!) {
deleteDevopsAdoptionSegment(input: { id: $id }) {
errors
}
}
query devopsAdoptionSegments {
devopsAdoptionSegments {
nodes {
id
name
latestSnapshot {
issueOpened
......
import { ApolloMutation } from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DevopsAdoptionDeleteModal from 'ee/admin/dev_ops_report/components/devops_adoption_delete_modal.vue';
import { DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID } from 'ee/admin/dev_ops_report/constants';
import * as Sentry from '~/sentry/wrapper';
import {
genericDeleteErrorMessage,
dataErrorMessage,
devopsAdoptionSegmentsData,
} from '../mock_data';
const mockEvent = { preventDefault: jest.fn() };
const mutate = jest.fn().mockResolvedValue({
data: {
deleteDevopsAdoptionSegment: {
errors: [],
},
},
});
const mutateWithDataErrors = jest.fn().mockResolvedValue({
data: {
deleteDevopsAdoptionSegment: {
errors: [dataErrorMessage],
},
},
});
const mutateLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
const mutateWithErrors = jest.fn().mockRejectedValue(genericDeleteErrorMessage);
describe('DevopsAdoptionDeleteModal', () => {
let wrapper;
const createComponent = ({ mutationMock = mutate } = {}) => {
const $apollo = {
mutate: mutationMock,
};
wrapper = shallowMount(DevopsAdoptionDeleteModal, {
propsData: {
segment: devopsAdoptionSegmentsData.nodes[0],
},
stubs: {
GlSprintf,
ApolloMutation,
},
mocks: {
$apollo,
},
});
};
const findModal = () => wrapper.find(GlModal);
const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
const findAlert = () => findModal().find(GlAlert);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('default display', () => {
beforeEach(() => createComponent());
it('contains the corrrect id', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
expect(modal.props('modalId')).toBe(DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID);
});
it('displays the confirmation message', () => {
const text = `Are you sure that you would like to delete ${devopsAdoptionSegmentsData.nodes[0].name}?`;
expect(findModal().text()).toBe(text);
});
it('does not display an error', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('submitting the form', () => {
describe('while waiting for the mutation', () => {
beforeEach(() => createComponent({ mutationMock: mutateLoading }));
it('disables the cancel button', async () => {
expect(cancelButtonDisabledState()).toBe(false);
findModal().vm.$emit('primary', mockEvent);
await wrapper.vm.$nextTick();
expect(cancelButtonDisabledState()).toBe(true);
});
it('sets the action button state to loading', async () => {
expect(actionButtonLoadingState()).toBe(false);
findModal().vm.$emit('primary', mockEvent);
await wrapper.vm.$nextTick();
expect(actionButtonLoadingState()).toBe(true);
});
});
describe('successful submission', () => {
beforeEach(() => {
createComponent();
wrapper.vm.$refs.modal.hide = jest.fn();
findModal().vm.$emit('primary', mockEvent);
});
it('submits the correct request variables', () => {
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: devopsAdoptionSegmentsData.nodes[0].id,
},
}),
);
});
it('closes the modal after a successful mutation', () => {
expect(wrapper.vm.$refs.modal.hide).toHaveBeenCalled();
});
});
describe('error handling', () => {
it.each`
errorType | errorLocation | mutationSpy | message
${'generic'} | ${'top level'} | ${mutateWithErrors} | ${genericDeleteErrorMessage}
${'specific'} | ${'data'} | ${mutateWithDataErrors} | ${dataErrorMessage}
`(
'displays a $errorType error if the mutation has a $errorLocation error',
async ({ mutationSpy, message }) => {
createComponent({ mutationMock: mutationSpy });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.props('variant')).toBe('danger');
expect(alert.text()).toBe(message);
},
);
it('calls sentry on top level error', async () => {
jest.spyOn(Sentry, 'captureException');
createComponent({ mutationMock: mutateWithErrors });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
expect(Sentry.captureException.mock.calls[0][0]).toBe(genericDeleteErrorMessage);
});
});
});
});
......@@ -35,6 +35,7 @@ export const devopsAdoptionSegmentsData = {
nodes: [
{
name: 'Segment 1',
id: 1,
latestSnapshot: {
issueOpened: true,
mergeRequestOpened: true,
......@@ -74,3 +75,6 @@ export const segmentName = 'Foooo';
export const genericErrorMessage = 'An error occured while saving the segment. Please try again.';
export const dataErrorMessage = 'Name already taken.';
export const genericDeleteErrorMessage =
'An error occured while deleting the segment. Please try again.';
......@@ -9619,15 +9619,27 @@ msgstr ""
msgid "DevopsAdoption|Add new segment"
msgstr ""
msgid "DevopsAdoption|An error occured while deleting the segment. Please try again."
msgstr ""
msgid "DevopsAdoption|An error occured while saving the segment. Please try again."
msgstr ""
msgid "DevopsAdoption|Approvals"
msgstr ""
msgid "DevopsAdoption|Are you sure that you would like to delete %{name}?"
msgstr ""
msgid "DevopsAdoption|Confirm delete segment"
msgstr ""
msgid "DevopsAdoption|Create new segment"
msgstr ""
msgid "DevopsAdoption|Delete segment"
msgstr ""
msgid "DevopsAdoption|Deploys"
msgstr ""
......
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