Skip to content

Commit 924d567

Browse files
committed
feat(backup-agent): add delete vault to module
ref: #BKP-489 Signed-off-by: Paul Dickerson <paul.dickerson.ext@ovhcloud.com>
1 parent 5978108 commit 924d567

12 files changed

Lines changed: 263 additions & 11 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"delete_vault_modal_title": "Supprimer le Vault",
3+
"delete_vault_modal_content": "Souhaitez-vous vraiment supprimer le Vault {{vaultName}} ?",
4+
"delete_vault_modal_warning": "Attention, cette action entraînera la suppression de tous les services associés.",
5+
"delete_vault_banner_success": "Votre Vault {{vaultName}} a correctement été supprimé.",
6+
"delete_vault_banner_error": "Une erreur est survenue. Veuillez attendre quelques minutes avant de réessayer.",
7+
"delete_vault_disabled_tooltip": "Vous ne pouvez pas supprimer un Vault lié à des Tenants VSPC."
8+
}

packages/manager/modules/backup-agent/src/BackupAgent.translations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export const BACKUP_AGENT_NAMESPACES = {
66
COMMON: `${NAMESPACE_PREFIX}/common`,
77
VAULT_DASHBOARD: `${VAULTS_NAMESPACE_PREFIX}/dashboard`,
88
VAULT_LISTING: `${VAULTS_NAMESPACE_PREFIX}/listing`,
9+
VAULT_DELETE: `${VAULTS_NAMESPACE_PREFIX}/delete`,
910
SERVICE_LISTING: `${SERVICES_NAMESPACE_PREFIX}/listing`,
1011
};
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import apiClient from '@ovh-ux/manager-core-api';
1+
import { v2 } from '@ovh-ux/manager-core-api';
22

33
import { VaultResource } from '@/types/Vault.type';
44

55
const getVaultRoute = (locationName: string) => `/location/${locationName}`;
66

77
export const getVaultDetails = async (locationName: string) =>
8-
(await apiClient.v2.get<VaultResource>(getVaultRoute(locationName))).data;
8+
(await v2.get<VaultResource>(getVaultRoute(locationName))).data;
9+
10+
export const deleteVault = async (vaultId: string) =>
11+
v2.delete<string>(`/backup/tenant/vault/${vaultId}`);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { UseMutationOptions, useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api';
4+
5+
import { deleteVault } from '@/data/api/vaults/vault.requests';
6+
7+
import { BACKUP_VAULTS_LIST_QUERY_KEY } from './getVault';
8+
9+
type UseDeleteVaultParams = Partial<UseMutationOptions<ApiResponse<string>, ApiError, string>>;
10+
11+
export const useDeleteVault = (options: UseDeleteVaultParams = {}) => {
12+
const queryClient = useQueryClient();
13+
14+
return useMutation({
15+
mutationFn: (vaultId: string) => deleteVault(vaultId),
16+
...options,
17+
onSuccess: async (data, variables, context) => {
18+
await queryClient.invalidateQueries({
19+
queryKey: BACKUP_VAULTS_LIST_QUERY_KEY,
20+
});
21+
options.onSuccess?.(data, variables, context);
22+
},
23+
});
24+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useNavigate, useSearchParams } from 'react-router-dom';
2+
3+
import { useTranslation } from 'react-i18next';
4+
5+
import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components';
6+
import { OdsMessage, OdsText } from '@ovhcloud/ods-components/react';
7+
8+
import { NAMESPACES } from '@ovh-ux/manager-common-translations';
9+
import { Modal, useNotifications } from '@ovh-ux/manager-react-components';
10+
11+
import { BACKUP_AGENT_NAMESPACES } from '@/BackupAgent.translations';
12+
import { useBackupVaultsList } from '@/data/hooks/vaults/getVault';
13+
import { useDeleteVault } from '@/data/hooks/vaults/useDeleteVault';
14+
15+
export default function DeleteVaultPage() {
16+
const { t } = useTranslation([BACKUP_AGENT_NAMESPACES.VAULT_DELETE, NAMESPACES.ACTIONS]);
17+
const navigate = useNavigate();
18+
const closeModal = () => navigate('..');
19+
const { addSuccess, addError } = useNotifications();
20+
const [searchParams] = useSearchParams();
21+
const { flattenData: vaults } = useBackupVaultsList();
22+
23+
const vaultId = searchParams.get('vaultId');
24+
const vaultName = vaults?.find(({ id }) => id === vaultId)?.currentState.resourceName;
25+
26+
const { mutate: deleteVault, isPending } = useDeleteVault({
27+
onSuccess: () => addSuccess(t('delete_vault_banner_success', { vaultName })),
28+
onError: () => addError(t('delete_vault_banner_error')),
29+
onSettled: () => closeModal(),
30+
});
31+
32+
return (
33+
<Modal
34+
isOpen
35+
heading={t('delete_vault_modal_title')}
36+
primaryLabel={t(`${NAMESPACES.ACTIONS}:confirm`)}
37+
onPrimaryButtonClick={() => vaultId && deleteVault(vaultId)}
38+
isPrimaryButtonLoading={isPending}
39+
isPrimaryButtonDisabled={isPending}
40+
secondaryLabel={t(`${NAMESPACES.ACTIONS}:cancel`)}
41+
onSecondaryButtonClick={closeModal}
42+
onDismiss={closeModal}
43+
type={ODS_MODAL_COLOR.critical}
44+
>
45+
<div className="flex flex-col gap-2">
46+
<OdsText>{t('delete_vault_modal_content', { vaultName })}</OdsText>
47+
<OdsMessage color="warning" isDismissible={false}>
48+
{t('delete_vault_modal_warning')}
49+
</OdsMessage>
50+
</div>
51+
</Modal>
52+
);
53+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { screen, waitFor } from '@testing-library/react';
2+
import { expect } from 'vitest';
3+
4+
import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components';
5+
6+
import { mockVaults } from '@/mocks/vaults/vaults';
7+
import { renderTest } from '@/test-utils/Test.utils';
8+
import { labels } from '@/test-utils/i18ntest.utils';
9+
import { VaultResource } from '@/types/Vault.type';
10+
import { buildSearchQuery } from '@/utils/buildSearchQuery.utils';
11+
12+
const config = {
13+
vault: mockVaults.at(0) as VaultResource,
14+
waitForPageLoad: async () =>
15+
waitFor(
16+
() => expect(screen.getByText(labels.vaultDelete.delete_vault_modal_title)).toBeVisible(),
17+
{ timeout: 10_000 },
18+
),
19+
};
20+
21+
describe('[INTEGRATION] - Delete Vault page', () => {
22+
it('display delete vault modal', async () => {
23+
const initialRoute = `/vaults/delete${buildSearchQuery({ vaultId: config.vault?.id })}`;
24+
await renderTest({ initialRoute });
25+
await config.waitForPageLoad();
26+
27+
// check modal
28+
const modal = screen.getByTestId('modal');
29+
expect(modal).toBeVisible();
30+
expect(modal).toHaveAttribute('color', ODS_MODAL_COLOR.critical);
31+
32+
// and modal content
33+
const modalElements = [
34+
labels.vaultDelete.delete_vault_modal_title,
35+
labels.vaultDelete.delete_vault_modal_warning,
36+
labels.vaultDelete.delete_vault_modal_content.replace(
37+
'{{vaultName}}',
38+
config.vault?.currentState.resourceName,
39+
),
40+
];
41+
modalElements.forEach((el) => expect(screen.getByText(el)).toBeVisible());
42+
43+
// and modal buttons
44+
const modalButtons = [
45+
{ testId: 'primary-button', label: labels.actions.confirm },
46+
{ testId: 'secondary-button', label: labels.actions.cancel },
47+
];
48+
modalButtons.forEach(({ testId, label }) =>
49+
expect(screen.getByTestId(testId)).toHaveAttribute('label', label),
50+
);
51+
});
52+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useId } from 'react';
2+
3+
import { useNavigate } from 'react-router-dom';
4+
5+
import { useTranslation } from 'react-i18next';
6+
7+
import { OdsButton, OdsText, OdsTooltip } from '@ovhcloud/ods-components/react';
8+
9+
import { DataGridTextCell } from '@ovh-ux/manager-react-components';
10+
11+
import { BACKUP_AGENT_NAMESPACES } from '@/BackupAgent.translations';
12+
import { subRoutes } from '@/routes/Routes.constants';
13+
import { VaultResource } from '@/types/Vault.type';
14+
import { buildSearchQuery } from '@/utils/buildSearchQuery.utils';
15+
16+
export const VaultActionCell = (vaultResource: VaultResource) => {
17+
const { t } = useTranslation(BACKUP_AGENT_NAMESPACES.VAULT_DELETE);
18+
const navigate = useNavigate();
19+
const buttonId = useId();
20+
21+
const queryParams = buildSearchQuery({ vaultId: vaultResource.id });
22+
const isDeleteDisable = !!vaultResource.currentState.vspc.length;
23+
24+
return (
25+
<DataGridTextCell>
26+
{isDeleteDisable && (
27+
<OdsTooltip triggerId={buttonId}>
28+
<OdsText>{t('delete_vault_disabled_tooltip')}</OdsText>
29+
</OdsTooltip>
30+
)}
31+
<OdsButton
32+
id={buttonId}
33+
label=""
34+
icon="trash"
35+
variant="ghost"
36+
isDisabled={isDeleteDisable}
37+
data-testid="delete-vault-button"
38+
onClick={() => {
39+
if (isDeleteDisable) return;
40+
navigate(`${subRoutes.delete}${queryParams}`);
41+
}}
42+
/>
43+
</DataGridTextCell>
44+
);
45+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { ODS_BUTTON_VARIANT, ODS_ICON_NAME } from '@ovhcloud/ods-components';
5+
6+
import { mockVaults } from '@/mocks/vaults/vaults';
7+
import { VaultResource } from '@/types/Vault.type';
8+
9+
import { VaultActionCell } from '../VaultActionCell.component';
10+
11+
vi.mock('react-router-dom', () => ({
12+
useNavigate: () => vi.fn(),
13+
}));
14+
15+
describe('VaultActionCell test suite', () => {
16+
const vault = mockVaults[0]!;
17+
const withVspc: VaultResource = {
18+
...vault,
19+
currentState: { ...vault.currentState, vspc: ['vspc1'] },
20+
};
21+
const withoutVspc: VaultResource = {
22+
...vault,
23+
currentState: { ...vault.currentState, vspc: [] },
24+
};
25+
26+
const testCases: Array<{ desc: string; vault: VaultResource; isDisabled: boolean }> = [
27+
{
28+
desc: 'renders an enabled button if vault has no VSPC',
29+
vault: withoutVspc,
30+
isDisabled: false,
31+
},
32+
{
33+
desc: 'renders a disabled button if vault has VSPC',
34+
vault: withVspc,
35+
isDisabled: true,
36+
},
37+
];
38+
39+
it.each(testCases)('$desc', ({ vault, isDisabled }) => {
40+
render(<VaultActionCell {...vault} />);
41+
42+
const button = screen.getByTestId('delete-vault-button');
43+
expect(button).toHaveAttribute('is-disabled', `${isDisabled}`);
44+
expect(button).toHaveAttribute('icon', ODS_ICON_NAME.trash);
45+
expect(button).toHaveAttribute('variant', ODS_BUTTON_VARIANT.ghost);
46+
});
47+
});

packages/manager/modules/backup-agent/src/pages/vaults/listing/_hooks/__tests__/useColumns.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ vi.mock('react-i18next', () => ({
99
}),
1010
}));
1111

12+
const ACTION_COLUMN_LABEL = '';
1213
const COLUMNS_EXPECTED = [
1314
ID_LABEL,
1415
'resource_name_label',
1516
'location_label',
1617
'region_label',
1718
'buckets_label',
1819
'status_label',
20+
ACTION_COLUMN_LABEL,
1921
];
2022

2123
describe('useColumns on Listing Page', () => {

packages/manager/modules/backup-agent/src/pages/vaults/listing/_hooks/useColumns.hooks.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { useTranslation } from 'react-i18next';
22

3-
import {
4-
VaultBucketsCell,
5-
VaultIdCell,
6-
VaultReferenceCell,
7-
} from '../_components';
8-
import {BACKUP_AGENT_NAMESPACES} from "@/BackupAgent.translations";
9-
import {ResourceLocationCell} from "@/components/CommonCells/ResourceLocationCell/ResourceLocationCell.components";
10-
import {ResourceRegionCell} from "@/components/CommonCells/ResourceRegionCell/ResourceRegionCell.components";
11-
import {ResourceStatusCell} from "@/components/CommonCells/ResourceStatusCell/ResourceStatusCell.components";
3+
import { BACKUP_AGENT_NAMESPACES } from '@/BackupAgent.translations';
4+
import { ResourceLocationCell } from '@/components/CommonCells/ResourceLocationCell/ResourceLocationCell.components';
5+
import { ResourceRegionCell } from '@/components/CommonCells/ResourceRegionCell/ResourceRegionCell.components';
6+
import { ResourceStatusCell } from '@/components/CommonCells/ResourceStatusCell/ResourceStatusCell.components';
7+
8+
import { VaultBucketsCell, VaultIdCell, VaultReferenceCell } from '../_components';
9+
import { VaultActionCell } from '../_components/VaultActionCell.component';
1210

1311
export const ID_LABEL = 'ID';
1412

@@ -46,5 +44,10 @@ export const useColumns = () => {
4644
cell: ResourceStatusCell,
4745
label: t('status_label'),
4846
},
47+
{
48+
id: 'vaultActions',
49+
cell: VaultActionCell,
50+
label: '',
51+
},
4952
];
5053
};

0 commit comments

Comments
 (0)