Commit dab7d081 authored by Jiaan Louw's avatar Jiaan Louw Committed by Nicolò Maria Mezzopera

Add delete button to compliance framework list

This adds a delete button to each compliance framework list
item and adds a delete modal to confirm the delete action.
parent 8de5fa44
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __, s__, sprintf } from '~/locale';
import deleteComplianceFrameworkMutation from '../graphql/mutations/delete_compliance_framework.mutation.graphql';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
export default {
components: {
GlModal,
GlSprintf,
},
props: {
name: {
type: String,
required: false,
default: null,
},
id: {
type: String,
required: false,
default: null,
},
groupPath: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(this.$options.i18n.title, { framework: this.name });
},
},
methods: {
async deleteFramework() {
this.reportDeleting();
try {
const {
data: { destroyComplianceFramework },
} = await this.$apollo.mutate({
mutation: deleteComplianceFrameworkMutation,
variables: {
input: {
id: this.id,
},
},
awaitRefetchQueries: true,
refetchQueries: [
{
query: getComplianceFrameworkQuery,
variables: {
fullPath: this.groupPath,
},
},
],
});
const [error] = destroyComplianceFramework.errors;
if (error) {
this.reportError(new Error(error));
} else {
this.reportSuccess();
}
} catch (error) {
this.reportError(error);
}
},
reportDeleting() {
this.$emit('deleting');
},
reportError(error) {
Sentry.captureException(error);
this.$emit('error');
},
reportSuccess() {
this.$emit('delete');
},
show() {
this.$refs.modal.show();
},
},
i18n: {
title: s__('ComplianceFrameworks|Delete compliance framework %{framework}'),
message: s__(
'ComplianceFrameworks|You are about to permanently delete the compliance framework %{framework} from all projects which currently have it applied, which may remove other functionality. This cannot be undone.',
),
},
buttonProps: {
primary: {
text: s__('ComplianceFrameworks|Delete framework'),
attributes: [{ category: 'primary' }, { variant: 'danger' }],
},
cancel: {
text: __('Cancel'),
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:title="title"
modal-id="delete-framework-modal"
:action-primary="$options.buttonProps.primary"
:action-cancel="$options.buttonProps.cancel"
@primary="deleteFramework"
>
<gl-sprintf :message="$options.i18n.message">
<template #framework>
<strong>{{ name }}</strong>
</template>
</gl-sprintf>
</gl-modal>
</template>
...@@ -5,12 +5,15 @@ import * as Sentry from '@sentry/browser'; ...@@ -5,12 +5,15 @@ import * as Sentry from '@sentry/browser';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { DANGER, INFO } from '../constants';
import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql'; import getComplianceFrameworkQuery from '../graphql/queries/get_compliance_framework.query.graphql';
import DeleteModal from './delete_modal.vue';
import EmptyState from './list_empty_state.vue'; import EmptyState from './list_empty_state.vue';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
export default { export default {
components: { components: {
DeleteModal,
EmptyState, EmptyState,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
...@@ -30,8 +33,11 @@ export default { ...@@ -30,8 +33,11 @@ export default {
}, },
data() { data() {
return { return {
markedForDeletion: {},
deletingFramework: null,
complianceFrameworks: [], complianceFrameworks: [],
error: '', error: '',
message: '',
}; };
}, },
apollo: { apollo: {
...@@ -44,7 +50,6 @@ export default { ...@@ -44,7 +50,6 @@ export default {
}, },
update(data) { update(data) {
const nodes = data.namespace?.complianceFrameworks?.nodes; const nodes = data.namespace?.complianceFrameworks?.nodes;
return ( return (
nodes?.map((framework) => ({ nodes?.map((framework) => ({
...framework, ...framework,
...@@ -60,7 +65,7 @@ export default { ...@@ -60,7 +65,7 @@ export default {
}, },
computed: { computed: {
isLoading() { isLoading() {
return this.$apollo.loading; return this.$apollo.loading && !this.deletingFramework;
}, },
hasLoaded() { hasLoaded() {
return !this.isLoading && !this.error; return !this.isLoading && !this.error;
...@@ -77,8 +82,40 @@ export default { ...@@ -77,8 +82,40 @@ export default {
regulatedCount() { regulatedCount() {
return 0; return 0;
}, },
alertDismissible() {
return !this.error;
},
alertVariant() {
return this.error ? DANGER : INFO;
},
alertMessage() {
return this.error || this.message;
},
},
methods: {
markForDeletion(framework) {
this.markedForDeletion = framework;
this.$refs.modal.show();
},
onError() {
this.error = this.$options.i18n.deleteError;
},
onDelete() {
this.message = this.$options.i18n.deleteMessage;
this.deletingFramework = null;
},
onDeleting() {
this.deletingFramework = this.markedForDeletion;
},
isDeleting(framework) {
return this.deletingFramework === framework;
},
}, },
i18n: { i18n: {
deleteMessage: s__('ComplianceFrameworks|Compliance framework deleted successfully'),
deleteError: s__(
'ComplianceFrameworks|Error deleting the compliance framework. Please try again',
),
fetchError: s__( fetchError: s__(
'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page', 'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page',
), ),
...@@ -89,8 +126,13 @@ export default { ...@@ -89,8 +126,13 @@ export default {
</script> </script>
<template> <template>
<div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"> <div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100">
<gl-alert v-if="error" class="gl-mt-5" variant="danger" :dismissible="false"> <gl-alert
{{ error }} v-if="alertMessage"
class="gl-mt-5"
:variant="alertVariant"
:dismissible="alertDismissible"
>
{{ alertMessage }}
</gl-alert> </gl-alert>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<empty-state v-if="isEmpty" :image-path="emptyStateSvgPath" /> <empty-state v-if="isEmpty" :image-path="emptyStateSvgPath" />
...@@ -101,9 +143,21 @@ export default { ...@@ -101,9 +143,21 @@ export default {
v-for="framework in complianceFrameworks" v-for="framework in complianceFrameworks"
:key="framework.parsedId" :key="framework.parsedId"
:framework="framework" :framework="framework"
:loading="isDeleting(framework)"
@delete="markForDeletion"
/> />
</gl-tab> </gl-tab>
<gl-tab disabled :title="$options.i18n.regulatedTab" /> <gl-tab disabled :title="$options.i18n.regulatedTab" />
</gl-tabs> </gl-tabs>
<delete-modal
v-if="hasFrameworks"
:id="markedForDeletion.id"
ref="modal"
:name="markedForDeletion.name"
:group-path="groupPath"
@deleting="onDeleting"
@delete="onDelete"
@error="onError"
/>
</div> </div>
</template> </template>
<script> <script>
import { GlLabel } from '@gitlab/ui'; import { GlLabel, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
export default { export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: { components: {
GlButton,
GlLabel, GlLabel,
}, },
props: { props: {
...@@ -10,12 +15,19 @@ export default { ...@@ -10,12 +15,19 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
loading: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
isScoped() { isScoped() {
return this.framework.name.includes('::'); return this.framework.name.includes('::');
}, },
}, },
i18n: {
deleteFramework: s__('ComplianceFrameworks|Delete framework'),
},
}; };
</script> </script>
<template> <template>
...@@ -28,10 +40,23 @@ export default { ...@@ -28,10 +40,23 @@ export default {
:background-color="framework.color" :background-color="framework.color"
:title="framework.name" :title="framework.name"
:scoped="isScoped" :scoped="isScoped"
:disabled="loading"
/> />
</div> </div>
<p class="gl-w-full gl-m-0!" data-testid="compliance-framework-description"> <p class="gl-w-full gl-m-0!" data-testid="compliance-framework-description">
{{ framework.description }} {{ framework.description }}
</p> </p>
<div>
<gl-button
v-gl-tooltip="$options.i18n.deleteFramework"
:loading="loading"
:disabled="loading"
:aria-label="$options.i18n.deleteFramework"
data-testid="compliance-framework-delete-button"
icon="remove"
category="tertiary"
@click="$emit('delete', framework)"
/>
</div>
</div> </div>
</template> </template>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const DANGER = 'danger';
export const INFO = 'info';
export const FETCH_ERROR = s__( export const FETCH_ERROR = s__(
'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page', 'ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page',
); );
......
mutation destroyComplianceFramework($input: DestroyComplianceFrameworkInput!) {
destroyComplianceFramework(input: $input) {
errors
}
}
...@@ -7,7 +7,7 @@ import Form from './components/list.vue'; ...@@ -7,7 +7,7 @@ import Form from './components/list.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}); });
const createComplianceFrameworksListApp = (el) => { const createComplianceFrameworksListApp = (el) => {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DeleteModal component layout matches the snapshot 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
dismisslabel="Close"
modalclass=""
modalid="delete-framework-modal"
size="md"
title="Delete compliance framework GDPR"
titletag="h4"
>
You are about to permanently delete the compliance framework
<strong>
GDPR
</strong>
from all projects which currently have it applied, which may remove other functionality. This cannot be undone.
</gl-modal-stub>
`;
import { GlSprintf, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import DeleteModal from 'ee/groups/settings/compliance_frameworks/components/delete_modal.vue';
import deleteComplianceFrameworkMutation from 'ee/groups/settings/compliance_frameworks/graphql/mutations/delete_compliance_framework.mutation.graphql';
import getComplianceFrameworkQuery from 'ee/groups/settings/compliance_frameworks/graphql/queries/get_compliance_framework.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
validFetchResponse,
validDeleteResponse,
errorDeleteResponse,
frameworkFoundResponse,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('DeleteModal', () => {
let wrapper;
const fetchSuccess = jest.fn().mockResolvedValue(validFetchResponse);
const networkError = new Error('network error');
const deleteSuccess = jest.fn().mockResolvedValue(validDeleteResponse);
const deleteError = jest.fn().mockResolvedValue(errorDeleteResponse);
const deleteNetworkError = jest.fn().mockRejectedValue(networkError);
const findModal = () => wrapper.findComponent(GlModal);
const clickDeleteFramework = () => findModal().vm.$emit('primary');
function createMockApolloProvider(resolverMock) {
localVue.use(VueApollo);
const requestHandlers = [
[getComplianceFrameworkQuery, fetchSuccess],
[deleteComplianceFrameworkMutation, resolverMock],
];
return createMockApollo(requestHandlers);
}
const createComponent = (resolverMock) => {
wrapper = shallowMount(DeleteModal, {
localVue,
apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
name: frameworkFoundResponse.name,
id: frameworkFoundResponse.id,
groupPath: 'group-1',
},
stubs: {
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('component layout', () => {
beforeEach(() => {
createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('sets the modal id', () => {
expect(findModal().props('modalId')).toBe('delete-framework-modal');
});
it('sets the modal primary button attributes', () => {
const actionPrimary = findModal().props('actionPrimary');
expect(actionPrimary.text).toBe('Delete framework');
expect(actionPrimary.attributes[1].variant).toBe('danger');
});
it('sets the modal cancel button attributes', () => {
expect(findModal().props('actionCancel').text).toBe('Cancel');
});
});
describe('clickDeleteFramework', () => {
it('emits "deleting" event when busy deleting', () => {
createComponent();
clickDeleteFramework();
expect(wrapper.emitted('deleting')).toHaveLength(1);
});
it('calls the delete mutation with the framework ID', async () => {
createComponent(deleteSuccess);
clickDeleteFramework();
await waitForPromises();
expect(deleteSuccess).toHaveBeenCalledWith({ input: { id: frameworkFoundResponse.id } });
});
it('calls the fetch query with the groupPath', async () => {
createComponent(deleteSuccess);
clickDeleteFramework();
await waitForPromises();
expect(fetchSuccess).toHaveBeenCalledWith({ fullPath: 'group-1' });
});
it('emits "delete" event when the framework is successfully deleted', async () => {
createComponent(deleteSuccess);
clickDeleteFramework();
await waitForPromises();
expect(wrapper.emitted('delete')).toHaveLength(1);
});
it('emits "error" event and reports to Sentry when there is a network error', async () => {
jest.spyOn(Sentry, 'captureException');
createComponent(deleteNetworkError);
clickDeleteFramework();
await waitForPromises();
expect(wrapper.emitted('error')).toHaveLength(1);
expect(Sentry.captureException.mock.calls[0][0].networkError).toStrictEqual(networkError);
});
it('emits "error" event and reports to Sentry when there is a graphql error', async () => {
jest.spyOn(Sentry, 'captureException');
createComponent(deleteError);
clickDeleteFramework();
await waitForPromises();
expect(wrapper.emitted('error')).toHaveLength(1);
expect(Sentry.captureException.mock.calls[0][0]).toStrictEqual(new Error('graphql error'));
});
});
});
...@@ -2,6 +2,7 @@ import { GlLabel } from '@gitlab/ui'; ...@@ -2,6 +2,7 @@ import { GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue'; import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('ListItem', () => { describe('ListItem', () => {
let wrapper; let wrapper;
...@@ -9,13 +10,18 @@ describe('ListItem', () => { ...@@ -9,13 +10,18 @@ describe('ListItem', () => {
const framework = { name: 'framework', description: 'a framework', color: '#112233' }; const framework = { name: 'framework', description: 'a framework', color: '#112233' };
const findLabel = () => wrapper.find(GlLabel); const findLabel = () => wrapper.find(GlLabel);
const findDescription = () => wrapper.find('[data-testid="compliance-framework-description"]'); const findDescription = () => wrapper.find('[data-testid="compliance-framework-description"]');
const findDeleteButton = () => wrapper.find('[data-testid="compliance-framework-delete-button"]');
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(ListItem, { wrapper = shallowMount(ListItem, {
propsData: { propsData: {
framework, framework,
loading: false,
...props, ...props,
}, },
directives: {
GlTooltip: createMockDirective(),
},
}); });
}; };
...@@ -41,5 +47,42 @@ describe('ListItem', () => { ...@@ -41,5 +47,42 @@ describe('ListItem', () => {
expect(findLabel().props('title')).toBe('scoped::framework'); expect(findLabel().props('title')).toBe('scoped::framework');
expect(findLabel().props('scoped')).toBe(true); expect(findLabel().props('scoped')).toBe(true);
expect(findLabel().props('disabled')).toBe(false);
});
it('displays a delete button', () => {
createComponent();
const button = findDeleteButton();
const tooltip = getBinding(button.element, 'gl-tooltip');
expect(button.props('icon')).toBe('remove');
expect(button.props('disabled')).toBe(false);
expect(button.props('loading')).toBe(false);
expect(button.attributes('aria-label')).toBe('Delete framework');
expect(tooltip.value).toBe('Delete framework');
});
it('emits "delete" event when the delete button is clicked', async () => {
createComponent();
findDeleteButton().vm.$emit('click');
expect(wrapper.emitted('delete')[0]).toStrictEqual([framework]);
});
describe('when loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('disables the label', () => {
expect(findLabel().props('disabled')).toBe(true);
});
it('disables the delete button and shows loading', () => {
expect(findDeleteButton().props('disabled')).toBe(true);
expect(findDeleteButton().props('loading')).toBe(true);
});
}); });
}); });
...@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser'; ...@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import DeleteModal from 'ee/groups/settings/compliance_frameworks/components/delete_modal.vue';
import List from 'ee/groups/settings/compliance_frameworks/components/list.vue'; import List from 'ee/groups/settings/compliance_frameworks/components/list.vue';
import EmptyState from 'ee/groups/settings/compliance_frameworks/components/list_empty_state.vue'; import EmptyState from 'ee/groups/settings/compliance_frameworks/components/list_empty_state.vue';
import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue'; import ListItem from 'ee/groups/settings/compliance_frameworks/components/list_item.vue';
...@@ -26,6 +27,7 @@ describe('List', () => { ...@@ -26,6 +27,7 @@ describe('List', () => {
const fetchWithErrors = jest.fn().mockRejectedValue(sentryError); const fetchWithErrors = jest.fn().mockRejectedValue(sentryError);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const findDeleteModal = () => wrapper.findComponent(DeleteModal);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(EmptyState); const findEmptyState = () => wrapper.find(EmptyState);
const findTabs = () => wrapper.findAll(GlTab); const findTabs = () => wrapper.findAll(GlTab);
...@@ -82,6 +84,8 @@ describe('List', () => { ...@@ -82,6 +84,8 @@ describe('List', () => {
}); });
it('shows the alert', () => { it('shows the alert', () => {
expect(findAlert().props('dismissible')).toBe(false);
expect(findAlert().props('variant')).toBe('danger');
expect(findAlert().text()).toBe( expect(findAlert().text()).toBe(
'Error fetching compliance frameworks data. Please refresh the page', 'Error fetching compliance frameworks data. Please refresh the page',
); );
...@@ -120,6 +124,7 @@ describe('List', () => { ...@@ -120,6 +124,7 @@ describe('List', () => {
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
expect(findTabsContainer().exists()).toBe(false); expect(findTabsContainer().exists()).toBe(false);
expect(findDeleteModal().exists()).toBe(false);
}); });
}); });
...@@ -166,9 +171,74 @@ describe('List', () => { ...@@ -166,9 +171,74 @@ describe('List', () => {
), ),
color: expect.stringMatching(/^#([0-9A-F]{3}){1,2}$/i), color: expect.stringMatching(/^#([0-9A-F]{3}){1,2}$/i),
}, },
loading: false,
}), }),
), ),
); );
}); });
it('renders the delete modal', () => {
expect(findDeleteModal().exists()).toBe(true);
});
});
describe('delete framework', () => {
describe('when an item is marked for deletion', () => {
let framework;
const findListItem = () => findListItems().at(0);
beforeEach(async () => {
wrapper = createComponentWithApollo(fetch);
await waitForPromises();
framework = findListItem().props('framework');
findDeleteModal().vm.show = jest.fn();
findListItem().vm.$emit('delete', framework);
});
it('shows the modal when there is a "delete" event from a list item', () => {
expect(findDeleteModal().props('id')).toBe(framework.id);
expect(findDeleteModal().props('name')).toBe(framework.name);
expect(findDeleteModal().vm.show).toHaveBeenCalled();
});
describe('and the item is being deleted', () => {
beforeEach(() => {
findDeleteModal().vm.$emit('deleting');
});
it('sets "loading" to true on the marked list item', () => {
expect(findListItem().props('loading')).toBe(true);
});
describe('and an error occurred', () => {
beforeEach(() => {
findDeleteModal().vm.$emit('error');
});
it('shows the alert for the error', () => {
expect(findAlert().props('dismissible')).toBe(false);
expect(findAlert().props('variant')).toBe('danger');
expect(findAlert().text()).toBe(
'Error deleting the compliance framework. Please try again',
);
});
});
describe('and the item was successfully deleted', () => {
beforeEach(async () => {
findDeleteModal().vm.$emit('delete');
await waitForPromises();
});
it('shows the alert for the success message', () => {
expect(findAlert().props('dismissible')).toBe(true);
expect(findAlert().props('variant')).toBe('info');
expect(findAlert().text()).toBe('Compliance framework deleted successfully');
});
});
});
});
}); });
}); });
...@@ -127,3 +127,23 @@ export const errorUpdateResponse = { ...@@ -127,3 +127,23 @@ export const errorUpdateResponse = {
}, },
}, },
}; };
export const validDeleteResponse = {
data: {
destroyComplianceFramework: {
clientMutationId: null,
errors: [],
__typename: 'DestroyComplianceFrameworkPayload',
},
},
};
export const errorDeleteResponse = {
data: {
destroyComplianceFramework: {
clientMutationId: null,
errors: ['graphql error'],
__typename: 'DestroyComplianceFrameworkPayload',
},
},
};
...@@ -7596,12 +7596,24 @@ msgstr "" ...@@ -7596,12 +7596,24 @@ msgstr ""
msgid "ComplianceFrameworks|Combines with the CI configuration at runtime." msgid "ComplianceFrameworks|Combines with the CI configuration at runtime."
msgstr "" msgstr ""
msgid "ComplianceFrameworks|Compliance framework deleted successfully"
msgstr ""
msgid "ComplianceFrameworks|Compliance pipeline configuration location (optional)" msgid "ComplianceFrameworks|Compliance pipeline configuration location (optional)"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|Could not find this configuration location, please try a different location" msgid "ComplianceFrameworks|Could not find this configuration location, please try a different location"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|Delete compliance framework %{framework}"
msgstr ""
msgid "ComplianceFrameworks|Delete framework"
msgstr ""
msgid "ComplianceFrameworks|Error deleting the compliance framework. Please try again"
msgstr ""
msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page" msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page"
msgstr "" msgstr ""
...@@ -7623,6 +7635,9 @@ msgstr "" ...@@ -7623,6 +7635,9 @@ msgstr ""
msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})" msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|You are about to permanently delete the compliance framework %{framework} from all projects which currently have it applied, which may remove other functionality. This cannot be undone."
msgstr ""
msgid "ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name" msgid "ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name"
msgstr "" 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