Skip to content

Commit d73e995

Browse files
committed
feat(backup-agent): add backup agent deletion
ref: BKP-503 Signed-off-by: Vincent Bonmarchand <[email protected]>
1 parent 68f1363 commit d73e995

File tree

11 files changed

+225
-3
lines changed

11 files changed

+225
-3
lines changed

packages/manager/modules/backup-agent/public/translations/services/agent/Messages_fr_FR.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,10 @@
88
"product_name": "Nom du produit",
99
"select_os": "Sélectionner un système d'exploitation",
1010
"os": "Système d'exploitation",
11-
"download_agent": "Télécharger l'agent"
11+
"download_agent": "Télécharger l'agent",
12+
"delete_tenant_banner_error": "Une erreur est survenue. Veuillez attendre quelques minutes avant de réessayer.",
13+
"delete_agent_banner_success": "Votre Agent {{agentName}} a correctement été supprimé",
14+
"delete_agent_banner_error": "Une erreur est survenue. Veuillez attendre quelques minutes avant de réessayer.",
15+
"delete_agent_modal_title": "Supprimer l'agent",
16+
"delete_agent_modal_content": "Souhaitez-vous vraiment supprimer l'agent {{agentName}} ?"
1217
}

packages/manager/modules/backup-agent/src/data/api/agents/agents.requests.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export const getEditConfigurationBackupAgentsRoute = (
1111
backupAgentId: string,
1212
) => `/backup/tenant/vspc/${vspcTenantId}/backupAgent/${backupAgentId}`;
1313

14+
export const getDeleteBackupAgentsRoute = (vspcTenantId: string, backupAgentId: string) =>
15+
`/backup/tenant/vspc/${vspcTenantId}/backupAgent/${backupAgentId}`;
16+
1417
export type EditConfigurationBackupAgentsParams = {
1518
vspcTenantId: string;
1619
backupAgentId: string;
@@ -25,3 +28,9 @@ export const editConfigurationBackupAgents = async ({
2528
...payload
2629
}: EditConfigurationBackupAgentsParams): Promise<ApiResponse<string>> =>
2730
v2.put(getEditConfigurationBackupAgentsRoute(vspcTenantId, backupAgentId), payload);
31+
32+
export const deleteBackupAgent = async (
33+
vspcTenantId: string,
34+
agentId: string,
35+
): Promise<ApiResponse<string>> =>
36+
v2.delete(`${getDeleteBackupAgentsRoute(vspcTenantId, agentId)}`);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { UseMutationOptions, useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api';
4+
5+
import { deleteBackupAgent } from '@/data/api/agents/agents.requests';
6+
7+
import { BACKUP_TENANTS_QUERY_KEY } from '../tenants/useBackupTenants';
8+
9+
type DeleteAgentParams = {
10+
vspcTenantId: string;
11+
agentId: string;
12+
};
13+
14+
type UseDeleteTenantAgentParams = Partial<
15+
UseMutationOptions<ApiResponse<string>, ApiError, DeleteAgentParams>
16+
>;
17+
18+
export const useDeleteTenantAgent = (options: UseDeleteTenantAgentParams = {}) => {
19+
const queryClient = useQueryClient();
20+
21+
return useMutation({
22+
mutationFn: ({ vspcTenantId, agentId }) => deleteBackupAgent(vspcTenantId, agentId),
23+
...options,
24+
onSuccess: async (...params) => {
25+
await queryClient.invalidateQueries({
26+
queryKey: BACKUP_TENANTS_QUERY_KEY,
27+
});
28+
options.onSuccess?.(...params);
29+
},
30+
});
31+
};

packages/manager/modules/backup-agent/src/mocks/agents/agents.handler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,14 @@ export const getAgentMocks = ({ isAgentsError = false }: TAgentMockParams = {}):
2828
method: 'get',
2929
status: isAgentsError ? 500 : 200,
3030
},
31+
{
32+
url: '/backup/tenant/vspc/:vspcTenantId/backupAgent/:backupAgentId',
33+
response: (_: unknown, params: PathParams) => {
34+
if (isAgentsError) return null;
35+
return mockAgents.find((agent) => agent.id === params.backupAgentId);
36+
},
37+
api: 'v2',
38+
method: 'delete',
39+
status: isAgentsError ? 500 : 200,
40+
},
3141
];

packages/manager/modules/backup-agent/src/pages/services/dashboard/agent/_components/AgentActionsCell.component.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ export type AgentActionsCellProps = {
1616
tenantId: string;
1717
agentId: string;
1818
};
19-
2019
export const AgentActionsCell = ({ tenantId, agentId }: AgentActionsCellProps) => {
2120
const id = useId();
2221
const { t } = useTranslation([NAMESPACES.ACTIONS, BACKUP_AGENT_NAMESPACES.COMMON]);
2322
const configurationHref = useHref(
2423
urls.editAgentConfiguration.replace(urlParams.tenantId, tenantId).replace(':agentId', agentId),
2524
);
2625

26+
const deleteHref = useHref(
27+
urls.dashboardTenantAgentDelete
28+
.replace(urlParams.tenantId, tenantId)
29+
.replace(':agentId', agentId),
30+
);
31+
2732
const actions: ActionMenuItem[] = [
2833
{
2934
id: 0,
@@ -33,7 +38,7 @@ export const AgentActionsCell = ({ tenantId, agentId }: AgentActionsCellProps) =
3338
{
3439
id: 1,
3540
label: t(`${NAMESPACES.ACTIONS}:delete`),
36-
href: '',
41+
href: deleteHref,
3742
color: ODS_BUTTON_COLOR.critical,
3843
},
3944
];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useNavigate, useParams } from 'react-router-dom';
2+
3+
import { useTranslation } from 'react-i18next';
4+
5+
import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components';
6+
import { 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 { useBackupVSPCTenantAgentDetails } from '@/data/hooks/agents/getAgentDetails';
13+
import { useDeleteTenantAgent } from '@/data/hooks/agents/useDeleteAgent';
14+
15+
export default function DeleteAgentPage() {
16+
const { t } = useTranslation([BACKUP_AGENT_NAMESPACES.AGENT, NAMESPACES.ACTIONS]);
17+
const navigate = useNavigate();
18+
const closeModal = () => navigate('..');
19+
const { addSuccess, addError } = useNotifications();
20+
21+
const { tenantId, agentId } = useParams<{ tenantId: string; agentId: string }>();
22+
const { data } = useBackupVSPCTenantAgentDetails({ tenantId, agentId });
23+
24+
const { mutate: deleteAgent, isPending } = useDeleteTenantAgent({
25+
onSuccess: () =>
26+
addSuccess(t('delete_agent_banner_success', { agentName: data?.currentState.name })),
27+
onError: () => addError(t('delete_agent_banner_error')),
28+
onSettled: () => closeModal(),
29+
});
30+
31+
return (
32+
<Modal
33+
isOpen
34+
heading={t('delete_agent_modal_title')}
35+
primaryLabel={t(`${NAMESPACES.ACTIONS}:confirm`)}
36+
onPrimaryButtonClick={() => agentId && deleteAgent({ vspcTenantId: tenantId!, agentId })}
37+
isPrimaryButtonLoading={isPending}
38+
isPrimaryButtonDisabled={isPending}
39+
secondaryLabel={t(`${NAMESPACES.ACTIONS}:cancel`)}
40+
onSecondaryButtonClick={closeModal}
41+
onDismiss={closeModal}
42+
type={ODS_MODAL_COLOR.critical}
43+
>
44+
<OdsText>{t('delete_agent_modal_content', { agentName: data?.currentState.name })}</OdsText>
45+
</Modal>
46+
);
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
3+
import { screen, waitFor } from '@testing-library/react';
4+
import { vi } from 'vitest';
5+
6+
import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components';
7+
8+
import { mockAgents } from '@/mocks/agents/agents';
9+
import { TENANTS_MOCKS } from '@/mocks/tenant/tenants.mock';
10+
import { renderTest } from '@/test-utils/Test.utils';
11+
import { labels } from '@/test-utils/i18ntest.utils';
12+
13+
describe('[INTEGRATION] - Delete Agent page', () => {
14+
it('display delete agent modal', async () => {
15+
const agent = mockAgents[0]!;
16+
const { container } = await renderTest({
17+
initialRoute: `/services/dashboard/${TENANTS_MOCKS[0]!.id}/agents/delete/${agent.id}`,
18+
});
19+
20+
await waitFor(
21+
() => {
22+
expect(container.querySelector(`ods-text[preset="paragraph"]`)).toHaveTextContent(
23+
agent.currentState.name,
24+
);
25+
},
26+
{ timeout: 10_000 },
27+
);
28+
29+
const modal = screen.getByTestId('modal');
30+
expect(modal).toBeVisible();
31+
expect(modal).toHaveAttribute('color', ODS_MODAL_COLOR.critical);
32+
33+
const modalButtons = [
34+
{ testId: 'primary-button', label: labels.actions.confirm },
35+
{ testId: 'secondary-button', label: labels.actions.cancel },
36+
];
37+
38+
modalButtons.forEach(({ testId, label }) => {
39+
expect(screen.getByTestId(testId)).toHaveAttribute('label', label);
40+
});
41+
});
42+
});

packages/manager/modules/backup-agent/src/routes/Routes.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const urls = {
3131
dashboardTenants: `/${subRoutes.services}/${subRoutes.dashboard}/${urlParams.tenantId}`,
3232
dashboardTenantAgents: `/${subRoutes.services}/${subRoutes.dashboard}/${urlParams.tenantId}/${subRoutes.agents}`,
3333
addAgentConfiguration: `/${subRoutes.services}/${subRoutes.dashboard}/${urlParams.tenantId}/${subRoutes.agents}/${subRoutes.add}`,
34+
dashboardTenantAgentDelete: `/${subRoutes.services}/${subRoutes.dashboard}/${urlParams.tenantId}/${subRoutes.agents}/${subRoutes.delete}/${urlParams.agentId}`,
3435
editAgentConfiguration: `/${subRoutes.services}/${subRoutes.dashboard}/${urlParams.tenantId}/${subRoutes.agents}/${subRoutes.configure}/${urlParams.agentId}`,
3536
listingTenantDelete: `/${subRoutes.services}/${subRoutes.delete}`,
3637
dashboardTenantDelete: `/${subRoutes.services}/${subRoutes.dashboard}/${subRoutes.delete}`,

packages/manager/modules/backup-agent/src/routes/routes.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const VaultBucketsPage = React.lazy(
4343
() => import('../pages/vaults/dashboard/buckets/VaultBuckets.page'),
4444
);
4545

46+
const AgentDeletePage = React.lazy(
47+
() => import('@/pages/services/dashboard/agent/delete/DeleteAgent.page'),
48+
);
49+
4650
export default (
4751
<>
4852
<Route path="" Component={MainLayout}>
@@ -96,6 +100,11 @@ export default (
96100
path={`${subRoutes.configure}/${urlParams.agentId}`}
97101
Component={AgentEditConfigurationPage}
98102
/>
103+
<Route
104+
path={`${subRoutes.delete}/${urlParams.agentId}`}
105+
Component={AgentDeletePage}
106+
handle={{ tracking: { pageName: 'delete-agent', pageType: PageType.popup } }}
107+
/>
99108
</Route>
100109
</Route>
101110
</Route>

packages/manager/modules/backup-agent/src/utils/__tests__/buildSearchQuery.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { describe, expect, it } from 'vitest';
33
import { urls } from '@/routes/Routes.constants';
44

55
import {
6+
TBuildDeleteAgentUrlParams,
67
TBuildDeleteTenantUrlParams,
8+
buildDeleteAgentUrl,
79
buildDeleteTenantUrl,
810
buildSearchQuery,
911
} from '../buildSearchQuery.utils';
@@ -73,3 +75,45 @@ describe('buildDeleteTenantUrl test suite', () => {
7375
expect(buildDeleteTenantUrl({ tenantId, origin })).toEqual(expectedUrl);
7476
});
7577
});
78+
79+
describe('buildDeleteAgentUrl test suite', () => {
80+
const testCases: Array<
81+
TBuildDeleteAgentUrlParams & {
82+
desc: string;
83+
expectedUrl: string;
84+
}
85+
> = [
86+
{
87+
desc: 'fallback url if tenantId is empty',
88+
tenantId: '',
89+
agentId: 'A1',
90+
origin: 'dashboard',
91+
expectedUrl: urls.dashboardTenantAgents,
92+
},
93+
{
94+
desc: 'fallback url if tenantId is undefined',
95+
tenantId: undefined as unknown as string,
96+
agentId: 'A1',
97+
origin: 'dashboard',
98+
expectedUrl: urls.dashboardTenantAgents,
99+
},
100+
{
101+
desc: 'dashboard deleteUrl if tenantId & agentId are valid',
102+
tenantId: '42',
103+
agentId: '007',
104+
origin: 'dashboard',
105+
expectedUrl: `${urls.dashboardTenantAgentDelete}?tenantId=42&agentId=007`,
106+
},
107+
{
108+
desc: 'configuration origin still uses same base and fallback',
109+
tenantId: '42',
110+
agentId: 'abc',
111+
origin: 'configuration',
112+
expectedUrl: `${urls.dashboardTenantAgentDelete}?tenantId=42&agentId=abc`,
113+
},
114+
];
115+
116+
it.each(testCases)('returns the $desc', ({ tenantId, agentId, origin, expectedUrl }) => {
117+
expect(buildDeleteAgentUrl({ tenantId, agentId, origin })).toEqual(expectedUrl);
118+
});
119+
});

0 commit comments

Comments
 (0)