Skip to content

Commit 31aff7d

Browse files
committed
feat(pci-block-storage): rework get snapshot api and add delete snapshots
ref: #TAPC-4569 Signed-off-by: Adrien Turmo <[email protected]>
1 parent f8a0f8c commit 31aff7d

File tree

10 files changed

+160
-70
lines changed

10 files changed

+160
-70
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { v6 } from '@ovh-ux/manager-core-api';
2+
import { describe, it, vi } from 'vitest';
3+
import { getSnapshotsByRegion } from '@/api/data/snapshot';
4+
5+
vi.mock('@ovh-ux/manager-core-api');
6+
7+
describe('getSnapshotsByRegion', () => {
8+
it('should fetche volume snapshots for given projectId and region', async () => {
9+
const mockVolumeSnapshots = [
10+
{ id: '1', name: 'Snapshot 1' },
11+
{ id: '2', name: 'Snapshot 2' },
12+
];
13+
vi.mocked(v6.get).mockResolvedValue({ data: mockVolumeSnapshots });
14+
15+
const volumeSnapshots = await getSnapshotsByRegion('123', 'paris-a');
16+
17+
expect(v6.get).toHaveBeenCalledWith('/cloud/project/123/volume/snapshot', {
18+
params: {
19+
region: 'paris-a',
20+
},
21+
});
22+
expect(volumeSnapshots).toEqual(mockVolumeSnapshots);
23+
});
24+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { v6 } from '@ovh-ux/manager-core-api';
2+
3+
export type TVolumeSnapshot = {
4+
id: string;
5+
creationDate: string;
6+
description: string;
7+
name: string;
8+
planCode: string;
9+
region: string;
10+
size: number;
11+
status: string;
12+
volumeId: string;
13+
};
14+
15+
export const getSnapshotsByRegion = async (
16+
projectId: string,
17+
region: string,
18+
): Promise<TVolumeSnapshot[]> => {
19+
const { data } = await v6.get(`/cloud/project/${projectId}/volume/snapshot`, {
20+
params: { region },
21+
});
22+
return data;
23+
};
24+
25+
export const deleteSnapshots = async (
26+
projectId: string,
27+
volumeId: string,
28+
snapshotsIds: string[],
29+
) => {
30+
const { data } = await v6.post(
31+
`/cloud/project/${projectId}/volume/${volumeId}/bulkDeleteSnapshots`,
32+
{
33+
snapshotsIds,
34+
},
35+
);
36+
return data;
37+
};

packages/manager/apps/pci-block-storage/src/api/data/volume.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
detachVolume,
77
getAllVolumes,
88
getVolume,
9-
getVolumeSnapshot,
109
} from './volume';
1110

1211
vi.mock('@ovh-ux/manager-core-api');
@@ -23,21 +22,6 @@ describe('getVolume', () => {
2322
});
2423
});
2524

26-
describe('getVolumeSnapshot', () => {
27-
it('fetches volume snapshots for given projectId', async () => {
28-
const mockVolumeSnapshots = [
29-
{ id: '1', name: 'Snapshot 1' },
30-
{ id: '2', name: 'Snapshot 2' },
31-
];
32-
vi.mocked(v6.get).mockResolvedValue({ data: mockVolumeSnapshots });
33-
34-
const volumeSnapshots = await getVolumeSnapshot('123');
35-
36-
expect(v6.get).toHaveBeenCalledWith('/cloud/project/123/volume/snapshot');
37-
expect(volumeSnapshots).toEqual(mockVolumeSnapshots);
38-
});
39-
});
40-
4125
describe('getAllVolumes', () => {
4226
it('fetches all volumes for given projectId', async () => {
4327
const mockVolumes = [

packages/manager/apps/pci-block-storage/src/api/data/volume.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,6 @@ export type TAPIVolume = {
1515
availabilityZone: 'any' | string;
1616
};
1717

18-
export type TVolumeSnapshot = {
19-
id: string;
20-
creationDate: string;
21-
description: string;
22-
name: string;
23-
planCode: string;
24-
region: string;
25-
size: number;
26-
status: string;
27-
volumeId: string;
28-
};
29-
3018
export const getVolume = async (
3119
projectId: string,
3220
volumeId: string,
@@ -37,15 +25,6 @@ export const getVolume = async (
3725
return data;
3826
};
3927

40-
export const getVolumeSnapshot = async (
41-
projectId: string,
42-
): Promise<TVolumeSnapshot[]> => {
43-
const { data } = await v6.get<TVolumeSnapshot[]>(
44-
`/cloud/project/${projectId}/volume/snapshot`,
45-
);
46-
return data;
47-
};
48-
4928
export const getAllVolumes = async (
5029
projectId: string,
5130
): Promise<TAPIVolume[]> => {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useMutation, useQuery } from '@tanstack/react-query';
2+
import { useMemo } from 'react';
3+
import { useVolume } from '@/api/hooks/useVolume';
4+
import { deleteSnapshots, getSnapshotsByRegion } from '@/api/data/snapshot';
5+
import { selectSnapshotForVolume } from '@/api/select/snapshot';
6+
import queryClient from '@/queryClient';
7+
8+
export const getVolumeSnapshotsQueryKey = (
9+
projectId: string,
10+
volumeId: string,
11+
) => ['volume-snapshot', projectId, volumeId];
12+
13+
export const useVolumeSnapshots = (projectId: string, volumeId: string) => {
14+
const { data: volume } = useVolume(projectId, volumeId);
15+
16+
const select = useMemo(
17+
() => (volume ? selectSnapshotForVolume(volumeId) : undefined),
18+
[volume],
19+
);
20+
21+
return useQuery({
22+
queryKey: getVolumeSnapshotsQueryKey(projectId, volume?.region),
23+
queryFn: () => getSnapshotsByRegion(projectId, volume?.region),
24+
enabled: !!volume,
25+
select,
26+
});
27+
};
28+
29+
interface DeleteVolumeSnapshotsProps {
30+
projectId: string;
31+
volumeId: string;
32+
snapshotsIds: string[];
33+
}
34+
35+
export const useDeleteVolumeSnapshots = ({
36+
projectId,
37+
volumeId,
38+
snapshotsIds,
39+
}: DeleteVolumeSnapshotsProps) => {
40+
const mutation = useMutation({
41+
mutationFn: async () => deleteSnapshots(projectId, volumeId, snapshotsIds),
42+
onSettled: async () => {
43+
await queryClient.invalidateQueries({
44+
queryKey: getVolumeSnapshotsQueryKey(projectId, volumeId),
45+
});
46+
},
47+
});
48+
return {
49+
deleteVolumeSnapshots: () => mutation.mutate(),
50+
...mutation,
51+
};
52+
};

packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ import {
1616
detachVolume,
1717
getAllVolumes,
1818
getVolume,
19-
getVolumeSnapshot,
2019
TAPIVolume,
21-
TVolumeSnapshot,
2220
updateVolume,
2321
} from '@/api/data/volume';
2422
import { UCENTS_FACTOR } from '@/hooks/currency-constants';
@@ -176,17 +174,6 @@ export const useVolume = (
176174
};
177175
};
178176

179-
export const getVolumeSnapshotQueryKey = (projectId: string) => [
180-
'volume-snapshot',
181-
projectId,
182-
];
183-
184-
export const useVolumeSnapshot = (projectId: string) =>
185-
useQuery({
186-
queryKey: ['volume-snapshot', projectId],
187-
queryFn: (): Promise<TVolumeSnapshot[]> => getVolumeSnapshot(projectId),
188-
});
189-
190177
interface DeleteVolumeProps {
191178
projectId: string;
192179
volumeId: string;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TVolumeSnapshot } from '@/api/data/snapshot';
2+
3+
export const selectSnapshotForVolume = (volumeId: string) => (
4+
snapshots: TVolumeSnapshot[],
5+
) => snapshots.filter((snapshot) => snapshot.volumeId === volumeId);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { selectSnapshotForVolume } from '@/api/select/snapshot';
2+
import { TVolumeSnapshot } from '@/api/data/snapshot';
3+
4+
describe('snapshots', () => {
5+
describe('selectSnapshotForVolume', () => {
6+
it('should keep only shnapshot which have the same volumeId as the input', () => {
7+
const selectedVolumeId = '1';
8+
const snapshots = [
9+
{ id: '1', volumeId: 'wrong' },
10+
{ id: '2', volumeId: 'wrong' },
11+
{ id: '3', volumeId: selectedVolumeId },
12+
{ id: '4', volumeId: selectedVolumeId },
13+
{ id: '5', volumeId: 'wrong' },
14+
{ id: '6', volumeId: selectedVolumeId },
15+
{ id: '7', volumeId: 'wrong' },
16+
] as TVolumeSnapshot[];
17+
18+
const result = selectSnapshotForVolume(selectedVolumeId)(snapshots);
19+
20+
expect(result).toEqual([
21+
{ id: '3', volumeId: selectedVolumeId },
22+
{ id: '4', volumeId: selectedVolumeId },
23+
{ id: '6', volumeId: selectedVolumeId },
24+
]);
25+
});
26+
});
27+
});

packages/manager/apps/pci-block-storage/src/pages/delete/DeleteStorage.page.spec.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import { act, fireEvent } from '@testing-library/react';
22
import { describe, it, vi } from 'vitest';
33
import { useParams } from 'react-router-dom';
44
import DeleteStorage from './DeleteStorage.page';
5-
import {
6-
useDeleteVolume,
7-
useVolume,
8-
useVolumeSnapshot,
9-
} from '@/api/hooks/useVolume';
5+
import { useDeleteVolume, useVolume } from '@/api/hooks/useVolume';
6+
import { useVolumeSnapshots } from '@/api/hooks/useSnapshot';
107
import { renderWithMockedWrappers } from '@/__tests__/renderWithMockedWrappers';
118

129
const deleteVolume = vi.fn();
@@ -15,6 +12,7 @@ vi.mock('react-router-dom');
1512
vi.mocked(useParams).mockReturnValue({ volumeId: 'testVolume' });
1613

1714
vi.mock('@/api/hooks/useVolume');
15+
vi.mock('@/api/hooks/useSnapshot');
1816

1917
const mockedVolume = {
2018
data: {
@@ -37,33 +35,33 @@ vi.mocked(useDeleteVolume).mockReturnValue({
3735
const mockedSnapshots = {
3836
data: [{ id: 'snapshot1', volumeId: 'testVolume' }],
3937
isPending: false,
40-
} as ReturnType<typeof useVolumeSnapshot>;
38+
} as ReturnType<typeof useVolumeSnapshots>;
4139

4240
const mockedSnapshotsEmpty = {
4341
data: [],
4442
isPending: false,
45-
} as ReturnType<typeof useVolumeSnapshot>;
43+
} as ReturnType<typeof useVolumeSnapshots>;
4644

4745
describe('DeleteStorage', () => {
4846
it('renders without crashing', () => {
4947
vi.mocked(useVolume).mockReturnValue(mockedVolume);
50-
vi.mocked(useVolumeSnapshot).mockReturnValue(mockedSnapshots);
48+
vi.mocked(useVolumeSnapshots).mockReturnValue(mockedSnapshots);
5149

5250
const { container } = renderWithMockedWrappers(<DeleteStorage />);
5351
expect(container).toBeTruthy();
5452
});
5553

5654
it('renders spinner when isPending is true', () => {
5755
vi.mocked(useVolume).mockReturnValue(mockedVolumePending);
58-
vi.mocked(useVolumeSnapshot).mockReturnValue(mockedSnapshots);
56+
vi.mocked(useVolumeSnapshots).mockReturnValue(mockedSnapshots);
5957

6058
const { getByTestId } = renderWithMockedWrappers(<DeleteStorage />);
6159
expect(getByTestId('deleteStorage-spinner')).toBeInTheDocument();
6260
});
6361

6462
it('renders DeleteConstraintWarningMessage when hasSnapshot or isAttached is true', () => {
6563
vi.mocked(useVolume).mockReturnValue(mockedVolume);
66-
vi.mocked(useVolumeSnapshot).mockReturnValue(mockedSnapshots);
64+
vi.mocked(useVolumeSnapshots).mockReturnValue(mockedSnapshots);
6765

6866
const { getByTestId } = renderWithMockedWrappers(<DeleteStorage />);
6967
expect(
@@ -73,7 +71,7 @@ describe('DeleteStorage', () => {
7371

7472
it('renders DeleteWarningMessage when canDelete is true', () => {
7573
vi.mocked(useVolume).mockReturnValue(mockedVolume);
76-
vi.mocked(useVolumeSnapshot).mockReturnValue(mockedSnapshotsEmpty);
74+
vi.mocked(useVolumeSnapshots).mockReturnValue(mockedSnapshotsEmpty);
7775

7876
const { getByText } = renderWithMockedWrappers(<DeleteStorage />);
7977
expect(
@@ -85,7 +83,7 @@ describe('DeleteStorage', () => {
8583
vi.mocked(useVolume).mockReturnValue(
8684
mockedVolume as ReturnType<typeof useVolume>,
8785
);
88-
vi.mocked(useVolumeSnapshot).mockReturnValue(mockedSnapshotsEmpty);
86+
vi.mocked(useVolumeSnapshots).mockReturnValue(mockedSnapshotsEmpty);
8987

9088
const { getByText } = renderWithMockedWrappers(<DeleteStorage />);
9189
act(() => {

packages/manager/apps/pci-block-storage/src/pages/delete/DeleteStorage.page.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
44
import { Translation, useTranslation } from 'react-i18next';
55
import { useNavigate, useParams } from 'react-router-dom';
66
import { useNotifications } from '@ovh-ux/manager-react-components';
7-
import {
8-
useDeleteVolume,
9-
useVolume,
10-
useVolumeSnapshot,
11-
} from '@/api/hooks/useVolume';
7+
import { useDeleteVolume, useVolume } from '@/api/hooks/useVolume';
128
import DeleteWarningMessage from './DeleteWarningMessage';
139
import DeleteConstraintWarningMessage from './DeleteConstraintWarningMessage';
1410
import { ButtonLink } from '@/components/button-link/ButtonLink';
1511
import { useTrackBanner } from '@/hooks/useTrackBanner';
1612
import { Button } from '@/components/button/Button';
13+
import { useVolumeSnapshots } from '@/api/hooks/useSnapshot';
1714

1815
export default function DeleteStorage() {
1916
const { projectId, volumeId } = useParams();
@@ -26,8 +23,9 @@ export default function DeleteStorage() {
2623
projectId,
2724
volumeId,
2825
);
29-
const { data: snapshots, isPending: isSnapshotPending } = useVolumeSnapshot(
26+
const { data: snapshots, isPending: isSnapshotPending } = useVolumeSnapshots(
3027
projectId,
28+
volumeId,
3129
);
3230

3331
const onTrackingBannerError = useTrackBanner(
@@ -76,8 +74,7 @@ export default function DeleteStorage() {
7674
});
7775

7876
const isPending = isVolumePending || isSnapshotPending || isDeletePending;
79-
const hasSnapshot =
80-
!isPending && snapshots?.some((s) => s.volumeId === volumeId);
77+
const hasSnapshot = !isPending && snapshots?.length > 0;
8178
const isAttached = !isPending && volume?.attachedTo.length > 0;
8279
const canDelete = !isPending && !isAttached && !hasSnapshot;
8380

0 commit comments

Comments
 (0)