Skip to content
Snippets Groups Projects
Unverified Commit 11c9c68f authored by Mireya Gen Andres's avatar Mireya Gen Andres Committed by GitLab
Browse files

Merge branch '483060-delete-framework-edge-cases' into 'master'

parents fc5314d2 fb0d5a5e
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
@@ -14311,6 +14311,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