Skip to content
Snippets Groups Projects
Unverified Commit fb0d5a5e authored by Nataliia Radina's avatar Nataliia Radina Committed by GitLab
Browse files

Handle delete framework edge cases

Disable deleting for default frameworks and frameworks with policies linked

Changelog: changed

EE: true
parent bb87dc27
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -40,8 +40,11 @@ export const i18n = {
submitButtonText: s__('ComplianceFrameworks|Create framework'),
 
deleteButtonText: s__('ComplianceFrameworks|Delete framework'),
deleteButtonDisabledTooltip: s__(
`ComplianceFrameworks|Compliance frameworks that are linked to an active policy can't be deleted`,
deleteButtonLinkedPoliciesDisabledTooltip: s__(
"ComplianceFrameworks|Compliance frameworks that are linked to an active policy can't be deleted",
),
deleteButtonDefaultFrameworkDisabledTooltip: s__(
"ComplianceFrameworks|The default framework can't be deleted",
),
deleteModalTitle: s__('ComplianceFrameworks|Delete compliance framework %{framework}'),
deleteModalMessage: s__(
Loading
Loading
Loading
Loading
@@ -33,9 +33,7 @@ export default {
BasicInformationSection,
PoliciesSection,
ProjectsSection,
DeleteModal,
GlAlert,
GlButton,
GlForm,
Loading
Loading
@@ -105,6 +103,10 @@ export default {
return !this.$route.params.id;
},
 
isDefaultFramework() {
return this.formData.default;
},
hasLinkedPolicies() {
return Boolean(
this.formData.scanResultPolicies?.pageInfo.startCursor ||
Loading
Loading
@@ -114,11 +116,13 @@ export default {
},
 
deleteBtnDisabled() {
return this.hasLinkedPolicies;
return this.hasLinkedPolicies || this.isDefaultFramework;
},
 
deleteBtnDisabledTooltip() {
return i18n.deleteButtonDisabledTooltip;
return this.isDefaultFramework
? i18n.deleteButtonDefaultFrameworkDisabledTooltip
: i18n.deleteButtonLinkedPoliciesDisabledTooltip;
},
 
refetchConfig() {
Loading
Loading
Loading
Loading
@@ -6,9 +6,9 @@ import {
GlTable,
GlToast,
GlLink,
GlButton,
GlAlert,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
Loading
Loading
@@ -24,16 +24,16 @@ Vue.use(GlToast);
export default {
name: 'FrameworksTable',
components: {
DeleteModal,
FrameworkInfoDrawer,
FrameworkBadge,
GlLoadingIcon,
GlSearchBoxByClick,
GlTable,
GlLink,
GlAlert,
FrameworkInfoDrawer,
FrameworkBadge,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
DeleteModal,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
Loading
Loading
@@ -124,6 +124,17 @@ export default {
filterProjects(projects) {
return projects.filter((p) => p.fullPath.startsWith(this.groupPath));
},
shouldDisableDeleteAction(framework) {
return framework.default || Boolean(this.getPoliciesList(framework).length);
},
getDeleteActionTooltipTitle(framework) {
if (this.shouldDisableDeleteAction(framework)) {
return framework.default
? this.$options.i18n.deleteButtonDefaultFrameworkDisabledTooltip
: this.$options.i18n.deleteButtonLinkedPoliciesDisabledTooltip;
}
return '';
},
},
fields: [
{
Loading
Loading
@@ -170,6 +181,12 @@ export default {
actionEdit: __('Edit'),
actionDelete: __('Delete'),
toggleText: __('Actions for'),
deleteButtonLinkedPoliciesDisabledTooltip: s__(
"ComplianceFrameworks|Compliance frameworks that are linked to an active policy can't be deleted",
),
deleteButtonDefaultFrameworkDisabledTooltip: s__(
"ComplianceFrameworks|The default framework can't be deleted",
),
},
CREATE_FRAMEWORKS_DOCS_URL,
};
Loading
Loading
@@ -243,33 +260,48 @@ export default {
no-caret
>
<template v-if="isTopLevelGroup">
<gl-disclosure-dropdown-item
data-testid="edit-action"
@action="editFramework({ id: item.id })"
>
<template #list-item>
<div class="gl-mx-2">
<gl-button
data-testid="action-edit"
class="!gl-justify-start"
category="tertiary"
:block="true"
@click="editFramework({ id: item.id })"
>
{{ $options.i18n.actionEdit }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
data-testid="delete-action"
@action="showDeleteModal(item)"
</gl-button>
</div>
<div
v-gl-tooltip.left.viewport
class="gl-mx-2"
data-testid="delete-tooltip"
:title="getDeleteActionTooltipTitle(item)"
>
<template #list-item>
<gl-button
data-testid="action-delete"
class="!gl-justify-start"
category="tertiary"
:block="true"
:disabled="shouldDisableDeleteAction(item)"
@click="showDeleteModal(item)"
>
{{ $options.i18n.actionDelete }}
</template>
</gl-disclosure-dropdown-item>
</gl-button>
</div>
</template>
<gl-disclosure-dropdown-item
v-gl-tooltip.left.viewport
data-testid="copy-id-action"
:title="$options.i18n.copyIdExplanation"
@action="copyFrameworkId(item.id)"
>
<template #list-item>
<div class="gl-mx-2">
<gl-button
v-gl-tooltip.left.viewport
data-testid="action-copy-id"
:title="$options.i18n.copyIdExplanation"
class="!gl-justify-start"
category="tertiary"
:block="true"
@click="copyFrameworkId(item.id)"
>
{{ $options.i18n.actionCopyId }}: {{ getIdFromGraphQLId(item.id) }}
</template>
</gl-disclosure-dropdown-item>
</gl-button>
</div>
</gl-disclosure-dropdown>
</template>
<template #empty>
Loading
Loading
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import getComplianceFrameworkQuery from 'ee/compliance_dashboard/components/frameworks_report/edit_framework/graphql/get_compliance_framework.query.graphql';
import * as Utils from 'ee/groups/settings/compliance_frameworks/utils';
import EditFramework from 'ee/compliance_dashboard/components/frameworks_report/edit_framework/edit_framework.vue';
Loading
Loading
@@ -49,6 +49,7 @@ describe('Edit Framework Form', () => {
const findError = () => wrapper.findComponent(GlAlert);
const findDeleteButton = () => wrapper.findByTestId('delete-btn');
const findDeleteModal = () => wrapper.findComponent(DeleteModal);
const findDeleteButtonTooltip = () => wrapper.findComponent(GlTooltip);
 
const invalidFeedback = (input) =>
input.closest('[role=group]').querySelector('.invalid-feedback')?.textContent ?? '';
Loading
Loading
@@ -249,7 +250,7 @@ describe('Edit Framework Form', () => {
${'scanExecutionPolicies'} | ${'MQ'} | ${true}
${'scanExecutionPolicies'} | ${null} | ${false}
`(
'is $buttonState when $policyType has cursor $policyCursor',
'sets disabled attribute to $buttonState when $policyType has cursor $policyCursor and renders correct tooltip',
async ({ policyType, policyCursor, buttonState }) => {
const response = createComplianceFrameworksReportResponse();
response.data.namespace.complianceFrameworks.nodes[0][policyType].pageInfo.startCursor =
Loading
Loading
@@ -262,9 +263,38 @@ describe('Edit Framework Form', () => {
await waitForPromises();
 
expect(findDeleteButton().props('disabled')).toBe(buttonState);
const tooltip = findDeleteButtonTooltip();
if (buttonState) {
expect(tooltip.exists()).toBe(true);
expect(tooltip.attributes('title')).toBe(
"Compliance frameworks that are linked to an active policy can't be deleted",
);
} else {
expect(tooltip.exists()).toBe(false);
}
},
);
 
it('disables the delete button and shows correct tooltip when framework is default', async () => {
const response = createComplianceFrameworksReportResponse();
response.data.namespace.complianceFrameworks.nodes[0].default = true;
wrapper = createComponent(shallowMountExtended, {
requestHandlers: [[getComplianceFrameworkQuery, () => response]],
});
await waitForPromises();
const deleteButton = findDeleteButton();
expect(deleteButton.props('disabled')).toBe(true);
const tooltip = findDeleteButtonTooltip();
expect(tooltip.exists()).toBe(true);
expect(tooltip.attributes('title')).toBe("The default framework can't be deleted");
});
it('renders delete button if editing existing framework', async () => {
wrapper = createComponent();
await waitForPromises();
Loading
Loading
Loading
Loading
@@ -6,7 +6,6 @@ import {
GlModal,
GlAlert,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
Loading
Loading
@@ -14,7 +13,10 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import { createComplianceFrameworksReportResponse } from 'ee_jest/compliance_dashboard/mock_data';
import {
createComplianceFrameworksReportResponse,
createFramework,
} from 'ee_jest/compliance_dashboard/mock_data';
import FrameworksTable from 'ee/compliance_dashboard/components/frameworks_report/frameworks_table.vue';
import FrameworkInfoDrawer from 'ee/compliance_dashboard/components/frameworks_report/framework_info_drawer.vue';
import { ROUTE_EDIT_FRAMEWORK } from 'ee/compliance_dashboard/constants';
Loading
Loading
@@ -55,11 +57,11 @@ describe('FrameworksTable component', () => {
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findNoFrameworksAlert = () => wrapper.findComponent(GlAlert);
const findActionsDropdowns = () => wrapper.findAllComponents(GlDisclosureDropdown);
const findActionsDropdownItems = () =>
findActionsDropdowns().at(0).findAllComponents(GlDisclosureDropdownItem);
const findEditAction = () => wrapper.findByTestId('edit-action');
const findDeleteAction = () => wrapper.findByTestId('delete-action');
const findCopyIdAction = () => wrapper.findByTestId('copy-id-action');
const findActionsDropdownItems = () => findActionsDropdowns().at(0).findAll('.gl-mx-2');
const findEditAction = () => wrapper.findByTestId('action-edit');
const findDeleteAction = () => wrapper.findByTestId('action-delete');
const findCopyIdAction = () => wrapper.findByTestId('action-copy-id');
const findDeleteActionTooltip = () => wrapper.findByTestId('delete-tooltip');
const findDeleteModal = () => wrapper.findComponent(DeleteModal);
 
const toggleSidebar = async () => {
Loading
Loading
@@ -321,7 +323,7 @@ describe('FrameworksTable component', () => {
});
 
it('redirects to edit framework page on action', () => {
findEditAction().vm.$emit('action');
findEditAction().vm.$emit('click');
expect($router.push).toHaveBeenCalledWith({
name: ROUTE_EDIT_FRAMEWORK,
params: { id: getIdFromGraphQLId(frameworks[rowCheckIndex].id) },
Loading
Loading
@@ -335,11 +337,61 @@ describe('FrameworksTable component', () => {
});
 
it('shows delete modal on action', () => {
findDeleteAction().vm.$emit('action');
findDeleteAction().vm.$emit('click');
expect(modalStub.show).toHaveBeenCalled();
findDeleteModal().vm.$emit('delete');
expect(wrapper.emitted('delete-framework')).toEqual([[frameworks[rowCheckIndex].id]]);
});
it('disables delete action and shows correct tooltip when framework is default', async () => {
const defaultFramework = createFramework({ default: true });
wrapper = createComponent({
frameworks: [defaultFramework],
isLoading: false,
});
await nextTick();
expect(findDeleteAction().props('disabled')).toBe(true);
const tooltip = getBinding(findDeleteActionTooltip().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(findDeleteActionTooltip().attributes('title')).toBe(
"Compliance frameworks that are linked to an active policy can't be deleted",
);
});
it('disables delete action and shows correct tooltip when framework has linked policies', async () => {
const frameworkWithPolicies = createFramework();
wrapper = createComponent({
frameworks: [frameworkWithPolicies],
isLoading: false,
});
await nextTick();
expect(findDeleteAction().props('disabled')).toBe(true);
const tooltip = getBinding(findDeleteActionTooltip().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(findDeleteActionTooltip().attributes('title')).toBe(
"Compliance frameworks that are linked to an active policy can't be deleted",
);
});
it('enables delete action when framework is not default and has no linked policies', async () => {
const frameworkWithoutPolicies = createFramework({
options: { scanResultPolicies: { nodes: [] } },
});
wrapper = createComponent({
frameworks: [frameworkWithoutPolicies],
isLoading: false,
});
await nextTick();
expect(findDeleteAction().props('disabled')).toBe(false);
expect(findDeleteActionTooltip().attributes('title')).toBe('');
});
});
 
describe('copy id action', () => {
Loading
Loading
@@ -357,7 +409,7 @@ describe('FrameworksTable component', () => {
 
it('copies id to clipboard on action', () => {
jest.spyOn(navigator.clipboard, 'writeText');
findCopyIdAction().vm.$emit('action');
findCopyIdAction().vm.$emit('click');
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(1);
expect($toast.show).toHaveBeenCalledWith('Framework ID copied to clipboard.');
});
Loading
Loading
Loading
Loading
@@ -271,6 +271,7 @@ export const createFramework = ({
isDefault = false,
projects = 0,
groupPath = 'foo',
options,
} = {}) => ({
id: `gid://gitlab/ComplianceManagement::Framework/${id}`,
name: `Some framework ${id}`,
Loading
Loading
@@ -323,6 +324,7 @@ export const createFramework = ({
},
pipelineConfigurationFullPath: null,
__typename: 'ComplianceFramework',
...options,
});
 
export const createComplianceFrameworksTokenResponse = () => {
Loading
Loading
Loading
Loading
@@ -14293,6 +14293,9 @@ msgstr ""
msgid "ComplianceFrameworks|The compliance framework must be edited in top-level group %{linkStart}namespace%{linkEnd}"
msgstr ""
 
msgid "ComplianceFrameworks|The default framework can't be deleted"
msgstr ""
msgid "ComplianceFrameworks|There can be only one default framework."
msgstr ""
 
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment