From 84b8d2e8aefe19447cfc39b31e1894e7fc5bdcd2 Mon Sep 17 00:00:00 2001 From: Florian Renaut Date: Thu, 31 Oct 2024 17:44:09 +0100 Subject: [PATCH] fix(pci-private-network): add vrack creation (#13627) ref: DTCORE-2764 Signed-off-by: Florian Renaut --- .../translations/vrack/Messages_de_DE.json | 12 + .../translations/vrack/Messages_en_GB.json | 12 + .../translations/vrack/Messages_es_ES.json | 12 + .../translations/vrack/Messages_fr_CA.json | 12 + .../translations/vrack/Messages_fr_FR.json | 12 + .../translations/vrack/Messages_it_IT.json | 12 + .../translations/vrack/Messages_pl_PL.json | 12 + .../translations/vrack/Messages_pt_PT.json | 12 + .../apps/pci-private-network/setupTests.ts | 12 - .../pci-private-network/src/api/data/vrack.ts | 77 ++ .../src/api/hooks/useVrack.ts | 129 ++ .../delete/DeleteModal.component.spec.tsx | 4 - .../global-regions/DataGridNoresults.spec.tsx | 12 +- .../global-regions/DatagridBodyRow.spec.tsx | 28 +- .../global-regions/DatagridHeader.spec.tsx | 14 +- .../GlobalRegionsDatagrid.spec.tsx | 6 +- .../DeleteAction.component.spec.tsx | 4 - .../pages/onboarding/Onboarding.page.spec.tsx | 10 +- .../src/pages/onboarding/Onboarding.page.tsx | 147 ++- .../Onboarding.page.spec.tsx.snap | 1119 +++++++++++++++++ .../new/VrackCreation.page.spec.tsx | 51 + .../onboarding/new/VrackCreation.page.tsx | 220 ++++ .../VrackCreation.page.spec.tsx.snap | 56 + .../src/pages/onboarding/store.ts | 11 + .../apps/pci-private-network/src/routes.tsx | 8 + .../pci-private-network/src/setupTests.ts | 37 + .../apps/pci-private-network/vitest.config.js | 17 +- .../manager-pci-common/src/api/data/index.ts | 1 + .../src/api/data/operation.ts | 36 + .../manager-pci-common/src/api/hook/index.ts | 1 + .../src/api/hook/useOperation.ts | 68 + 31 files changed, 2074 insertions(+), 90 deletions(-) create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_de_DE.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_en_GB.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_es_ES.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_CA.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_FR.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_it_IT.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pl_PL.json create mode 100644 packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pt_PT.json delete mode 100644 packages/manager/apps/pci-private-network/setupTests.ts create mode 100644 packages/manager/apps/pci-private-network/src/api/data/vrack.ts create mode 100644 packages/manager/apps/pci-private-network/src/api/hooks/useVrack.ts create mode 100644 packages/manager/apps/pci-private-network/src/pages/onboarding/__snapshots__/Onboarding.page.spec.tsx.snap create mode 100644 packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.spec.tsx create mode 100644 packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.tsx create mode 100644 packages/manager/apps/pci-private-network/src/pages/onboarding/new/__snapshots__/VrackCreation.page.spec.tsx.snap create mode 100644 packages/manager/apps/pci-private-network/src/pages/onboarding/store.ts create mode 100644 packages/manager/modules/manager-pci-common/src/api/data/operation.ts create mode 100644 packages/manager/modules/manager-pci-common/src/api/hook/useOperation.ts diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_de_DE.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_de_DE.json new file mode 100644 index 000000000000..4a59cd4a1d60 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_de_DE.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "vRack erstellen", + "pci_projects_project_network_private_vrack_create_new": "Neues vRack", + "pci_projects_project_network_private_vrack_create_existing": "Vorhandenes vRack", + "pci_projects_project_network_private_vrack_create_description": "Klicken Sie auf „Erstellen“, um Ihr neues vRack zu aktivieren", + "pci_projects_project_network_private_vrack_create_choose": "Vorhandenes vRack auswählen", + "pci_projects_project_network_private_vrack_create_action": "Erstellen", + "pci_projects_project_network_private_vrack_create_cancel": "Abbrechen", + "pci_projects_project_network_private_vrack_create_init_error": "Beim Laden der Informationen ist ein Fehler aufgetreten: {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "Bei der Aktivierung des vRack ist ein Fehler aufgetreten: {{message}}", + "pci_projects_project_network_private_vrack_pending": "vRack wird aktiviert. Dieser Vorgang kann einige Minuten dauern" +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_en_GB.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_en_GB.json new file mode 100644 index 000000000000..cdff15f6ac95 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_en_GB.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Create a vRack", + "pci_projects_project_network_private_vrack_create_new": "New vRack", + "pci_projects_project_network_private_vrack_create_existing": "Existing vRack", + "pci_projects_project_network_private_vrack_create_description": "Click “Create” to enable your new vRack", + "pci_projects_project_network_private_vrack_create_choose": "Select an existing vRack", + "pci_projects_project_network_private_vrack_create_action": "Create", + "pci_projects_project_network_private_vrack_create_cancel": "Cancel", + "pci_projects_project_network_private_vrack_create_init_error": "An error has occurred loading the information {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "An error has occurred enabling the vRack {{ message }}", + "pci_projects_project_network_private_vrack_pending": "Your vRack is being enabled. This operation may take several minutes." +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_es_ES.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_es_ES.json new file mode 100644 index 000000000000..2b6f4adab424 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_es_ES.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Crear un vRack", + "pci_projects_project_network_private_vrack_create_new": "Nuevo vRack", + "pci_projects_project_network_private_vrack_create_existing": "vRack existente", + "pci_projects_project_network_private_vrack_create_description": "Haga clic en «Crear» para activar el nuevo vRack", + "pci_projects_project_network_private_vrack_create_choose": "Seleccionar un vRack existente", + "pci_projects_project_network_private_vrack_create_action": "Crear", + "pci_projects_project_network_private_vrack_create_cancel": "Cancelar", + "pci_projects_project_network_private_vrack_create_init_error": "Se ha producido un error al cargar la información: {{ message }}.", + "pci_projects_project_network_private_vrack_create_error": "Se ha producido un error al activar el vRack: {{message}}.", + "pci_projects_project_network_private_vrack_pending": "Activando el vRack... Esta operación puede tardar unos minutos." +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_CA.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_CA.json new file mode 100644 index 000000000000..1e25edf632b9 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_CA.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Créez un vRack", + "pci_projects_project_network_private_vrack_create_new": "Nouveau vRack", + "pci_projects_project_network_private_vrack_create_existing": "vRack existant", + "pci_projects_project_network_private_vrack_create_description": "Cliquez sur \"Créer\" pour activer votre nouveau vRack", + "pci_projects_project_network_private_vrack_create_choose": "Choisir un vRack existant", + "pci_projects_project_network_private_vrack_create_action": "Créer", + "pci_projects_project_network_private_vrack_create_cancel": "Annuler", + "pci_projects_project_network_private_vrack_create_init_error": "Une erreur est survenue lors du chargement des informations {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "Une erreur est survenue lors de l'activation du vRack {{ message }}", + "pci_projects_project_network_private_vrack_pending": "Votre vRack est en cours d'activation. L'opération peut prendre plusieurs minutes" +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_FR.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_FR.json new file mode 100644 index 000000000000..1e25edf632b9 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_fr_FR.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Créez un vRack", + "pci_projects_project_network_private_vrack_create_new": "Nouveau vRack", + "pci_projects_project_network_private_vrack_create_existing": "vRack existant", + "pci_projects_project_network_private_vrack_create_description": "Cliquez sur \"Créer\" pour activer votre nouveau vRack", + "pci_projects_project_network_private_vrack_create_choose": "Choisir un vRack existant", + "pci_projects_project_network_private_vrack_create_action": "Créer", + "pci_projects_project_network_private_vrack_create_cancel": "Annuler", + "pci_projects_project_network_private_vrack_create_init_error": "Une erreur est survenue lors du chargement des informations {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "Une erreur est survenue lors de l'activation du vRack {{ message }}", + "pci_projects_project_network_private_vrack_pending": "Votre vRack est en cours d'activation. L'opération peut prendre plusieurs minutes" +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_it_IT.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_it_IT.json new file mode 100644 index 000000000000..e9b8789f1d24 --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_it_IT.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Crea una vRack", + "pci_projects_project_network_private_vrack_create_new": "Nuova vRack", + "pci_projects_project_network_private_vrack_create_existing": "vRack esistente", + "pci_projects_project_network_private_vrack_create_description": "Per attivare la tua nuova vRack, clicca su “Crea”.", + "pci_projects_project_network_private_vrack_create_choose": "Seleziona una vRack esistente", + "pci_projects_project_network_private_vrack_create_action": "Crea", + "pci_projects_project_network_private_vrack_create_cancel": "Annulla", + "pci_projects_project_network_private_vrack_create_init_error": "Si è verificato un errore durante il caricamento delle informazioni: {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "Si è verificato un errore durante l’attivazione della vRack: {{ message }}", + "pci_projects_project_network_private_vrack_pending": "La tua vRack è in corso di attivazione. L’operazione potrebbe richiedere qualche minuto." +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pl_PL.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pl_PL.json new file mode 100644 index 000000000000..56d592810cfb --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pl_PL.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Utwórz vRack", + "pci_projects_project_network_private_vrack_create_new": "Nowy vRack", + "pci_projects_project_network_private_vrack_create_existing": "Istniejący vRack", + "pci_projects_project_network_private_vrack_create_description": "Kliknij „Utwórz”, aby aktywować Twój nowy vRack.", + "pci_projects_project_network_private_vrack_create_choose": "Wybierz istniejący vRack", + "pci_projects_project_network_private_vrack_create_action": "Utwórz", + "pci_projects_project_network_private_vrack_create_cancel": "Anuluj", + "pci_projects_project_network_private_vrack_create_init_error": "Wystąpił błąd podczas pobierania informacji {{ message }}.", + "pci_projects_project_network_private_vrack_create_error": "Wystąpił błąd podczas tworzenia sieci vRack: {{message}}.", + "pci_projects_project_network_private_vrack_pending": "Trwa tworzenie Twojej sieci vRack. Może to potrwać kilka minut." +} diff --git a/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pt_PT.json b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pt_PT.json new file mode 100644 index 000000000000..09ff96946c7f --- /dev/null +++ b/packages/manager/apps/pci-private-network/public/translations/vrack/Messages_pt_PT.json @@ -0,0 +1,12 @@ +{ + "pci_projects_project_network_private_vrack_create_heading": "Criar um vRack", + "pci_projects_project_network_private_vrack_create_new": "Novo vRack", + "pci_projects_project_network_private_vrack_create_existing": "vRack existente", + "pci_projects_project_network_private_vrack_create_description": "Clique em “Criar” para ativar o seu novo vRack", + "pci_projects_project_network_private_vrack_create_choose": "Selecionar um vRack existente", + "pci_projects_project_network_private_vrack_create_action": "Criar", + "pci_projects_project_network_private_vrack_create_cancel": "Anular", + "pci_projects_project_network_private_vrack_create_init_error": "Ocorreu um erro ao carregar as informações {{ message }}", + "pci_projects_project_network_private_vrack_create_error": "Ocorreu um erro ao ativar o vRack {{ message }}", + "pci_projects_project_network_private_vrack_pending": "A ativação do seu vRack está em curso. A operação pode demorar alguns minutos" +} diff --git a/packages/manager/apps/pci-private-network/setupTests.ts b/packages/manager/apps/pci-private-network/setupTests.ts deleted file mode 100644 index 39651aefe5e5..000000000000 --- a/packages/manager/apps/pci-private-network/setupTests.ts +++ /dev/null @@ -1,12 +0,0 @@ -import '@testing-library/jest-dom'; -import 'element-internals-polyfill'; -import { vi } from 'vitest'; - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (translationKey: string) => translationKey, - i18n: { - changeLanguage: () => new Promise(() => {}), - }, - }), -})); diff --git a/packages/manager/apps/pci-private-network/src/api/data/vrack.ts b/packages/manager/apps/pci-private-network/src/api/data/vrack.ts new file mode 100644 index 000000000000..34a500e20e57 --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/api/data/vrack.ts @@ -0,0 +1,77 @@ +import { fetchIcebergV6, v6 } from '@ovh-ux/manager-core-api'; + +export enum VrackTaskStatus { + Cancelled = 'cancelled', + Doing = 'doing', + Done = 'done', + Init = 'init', + Todo = 'todo', +} + +export type TVrackTask = { + function: string; + id: number; + lastUpdate: string | null; + orderId: number | null; + serviceName: string | null; + status: VrackTaskStatus; + targetDomain: string | null; + todoDate: string | null; +}; + +export type TVrack = { + name: string; + description: string; + iam?: { + id: string; + displayName: string; + tags: Record; + urn: string; + }; +}; + +export const getVrackTask = async (vrack: string, taskId: string) => { + try { + const { data } = await v6.get(`/vrack/${vrack}/task/${taskId}`); + return data; + } catch (err) { + if (err?.response?.status === 404) return null; + throw err; + } +}; + +export const getProjectVrack = async (projectId: string) => { + const { data } = await v6.get<{ + id: string; + name: string; + }>(`/cloud/project/${projectId}/vrack`); + return data; +}; + +export const getVracks = async (): Promise => { + const { data } = await fetchIcebergV6({ + route: `/vrack`, + disableCache: true, + }); + return data; +}; + +export const createVrack = async (projectId: string) => { + const { data } = await v6.post<{ id: string }>( + `/cloud/project/${projectId}/vrack`, + ); + return data; +}; + +export const associateVrack = async ( + serviceName: string, + projectId: string, +) => { + const { data } = await v6.post<{ id: number }>( + `/vrack/${serviceName}/cloudProject`, + { + project: projectId, + }, + ); + return data; +}; diff --git a/packages/manager/apps/pci-private-network/src/api/hooks/useVrack.ts b/packages/manager/apps/pci-private-network/src/api/hooks/useVrack.ts new file mode 100644 index 000000000000..e8a21150a598 --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/api/hooks/useVrack.ts @@ -0,0 +1,129 @@ +import { useEffect, useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + associateVrack, + createVrack, + getProjectVrack, + getVracks, + getVrackTask, + TVrackTask, + VrackTaskStatus, +} from '@/api/data/vrack'; + +export const VRACK_QUERY_KEY = 'vrack'; +export const PROJECT_VRACK_QUERY_KEY = 'project-vrack'; + +export const useProjectVrack = (projectId: string) => + useQuery({ + queryKey: [PROJECT_VRACK_QUERY_KEY], + queryFn: async () => getProjectVrack(projectId), + retry: false, + throwOnError: false, + }); + +export const useVracks = () => + useQuery({ + queryKey: [VRACK_QUERY_KEY], + queryFn: () => getVracks(), + select: (data) => + data?.map((vrack) => { + const vrackId = (vrack.iam?.urn?.match(/vrack:(.*)$/) || [])[1]; + return { + vrackId, + displayName: vrack.name || vrack.iam?.displayName || vrackId, + ...vrack, + }; + }), + }); + +export const useCreateVrack = ({ + onError, + onSuccess, +}: { + onError: (cause: Error) => void; + onSuccess: (operationId: string) => void; +}) => { + const mutation = useMutation({ + mutationFn: createVrack, + onError, + onSuccess: async ({ id }) => { + onSuccess(id); + }, + }); + return { + createVrack: (projectId: string) => mutation.mutate(projectId), + ...mutation, + }; +}; + +export const useAssociateVrack = ({ + projectId, + vrackId, + onError, + onSuccess, +}: { + projectId: string; + vrackId: string; + onError: (cause: Error) => void; + onSuccess: () => void; +}) => { + const queryClient = useQueryClient(); + const [taskId, setTaskId] = useState(null); + const [isPending, setIsPending] = useState(false); + + const mutation = useMutation({ + mutationFn: async () => { + setIsPending(true); + const { id } = await associateVrack(vrackId, projectId); + return id; + }, + onError, + onSuccess: setTaskId, + }); + + // polling of task association + const { data: task, error } = useQuery({ + queryKey: ['pci-vrack-task', vrackId, taskId], + queryFn: () => getVrackTask(vrackId, taskId), + enabled: (query) => + !!projectId && + !!taskId && + ![VrackTaskStatus.Cancelled, VrackTaskStatus.Done].includes( + query.state.data?.status, + ), + refetchInterval: 3000, + retry: 5, + }); + + // report an error if failling task polling after retries + useEffect(() => { + if (error && isPending) { + setIsPending(false); + onError(error); + } + }, [error, isPending]); + + // report after polling + useEffect(() => { + if (!isPending) return; + // if task is missing (404) it is assumed to be done + if (task === null || task?.status === VrackTaskStatus.Done) { + queryClient.invalidateQueries({ + queryKey: [VRACK_QUERY_KEY], + }); + queryClient.invalidateQueries({ + queryKey: [PROJECT_VRACK_QUERY_KEY], + }); + setIsPending(false); + onSuccess(); + } else if (task?.status === VrackTaskStatus.Cancelled) { + setIsPending(false); + onError(new Error(task.status)); + } + }, [task, isPending]); + + return { + associateVrack: mutation.mutate, + isPending, + }; +}; diff --git a/packages/manager/apps/pci-private-network/src/components/delete/DeleteModal.component.spec.tsx b/packages/manager/apps/pci-private-network/src/components/delete/DeleteModal.component.spec.tsx index acf73b1d6ab7..9af2fc248f5e 100644 --- a/packages/manager/apps/pci-private-network/src/components/delete/DeleteModal.component.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/delete/DeleteModal.component.spec.tsx @@ -3,10 +3,6 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import DeleteModal from './DeleteModal.component'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key) => key }), -})); - describe('DeleteModal', () => { const mockOnClose = vi.fn(); const mockOnConfirm = vi.fn(); diff --git a/packages/manager/apps/pci-private-network/src/components/global-regions/DataGridNoresults.spec.tsx b/packages/manager/apps/pci-private-network/src/components/global-regions/DataGridNoresults.spec.tsx index 8ebb726b7d92..e712451908e4 100644 --- a/packages/manager/apps/pci-private-network/src/components/global-regions/DataGridNoresults.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/global-regions/DataGridNoresults.spec.tsx @@ -2,13 +2,15 @@ import { describe, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import DataGridNoResults from '@/components/global-regions/DatagridNoResults'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key: string) => key }), -})); - describe('DataGridNoResults', () => { it('should render no results message', () => { - render(); + render( + + + + +
, + ); expect( screen.getByText('common_pagination_no_results'), ).toBeInTheDocument(); diff --git a/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridBodyRow.spec.tsx b/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridBodyRow.spec.tsx index b73de3452ac4..42a8799211c2 100644 --- a/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridBodyRow.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridBodyRow.spec.tsx @@ -4,10 +4,6 @@ import { useHref } from 'react-router-dom'; import DataGridBodyRow from '@/components/global-regions/DatagridBodyRow'; import { TAggregatedNetwork } from '@/api/data/network'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key: string) => key }), -})); - vi.mock('react-router-dom', () => ({ useHref: vi.fn(), })); @@ -21,7 +17,11 @@ describe('DataGridBodyRow', () => { } as unknown) as TAggregatedNetwork; render( - , + + + + +
, ); expect(screen.getByText('mocked_vlanId')).toBeInTheDocument(); expect(screen.getByText('mocked_name')).toBeInTheDocument(); @@ -38,7 +38,11 @@ describe('DataGridBodyRow', () => { } as unknown) as TAggregatedNetwork; render( - , + + + + +
, ); expect(screen.getAllByText('mocked_region')).toHaveLength(1); expect(screen.getAllByText('mocked_cidr')).toHaveLength(1); @@ -61,7 +65,11 @@ describe('DataGridBodyRow', () => { } as unknown) as TAggregatedNetwork; const { getByTestId } = render( - , + + + + +
, ); const deleteButton = getByTestId('dataGridBodyRow-delete_button'); expect(deleteButton).toBeInTheDocument(); @@ -82,7 +90,11 @@ describe('DataGridBodyRow', () => { } as unknown) as TAggregatedNetwork; render( - , + + + + +
, ); const tooltip = screen.getByText( 'pci_projects_project_network_private_delete', diff --git a/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridHeader.spec.tsx b/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridHeader.spec.tsx index 044276c07947..e9a23487e99f 100644 --- a/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridHeader.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/global-regions/DatagridHeader.spec.tsx @@ -1,14 +1,16 @@ -import { describe, vi } from 'vitest'; +import { describe } from 'vitest'; import { render, screen } from '@testing-library/react'; import DatagridHeader from '@/components/global-regions/DatagridHeader'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key) => key }), -})); - describe('DatagridHeader', () => { it('should render all headers correctly', () => { - render(); + render( + + + + +
, + ); expect( screen.getByText('pci_projects_project_network_private_vlan_id'), ).toBeInTheDocument(); diff --git a/packages/manager/apps/pci-private-network/src/components/global-regions/GlobalRegionsDatagrid.spec.tsx b/packages/manager/apps/pci-private-network/src/components/global-regions/GlobalRegionsDatagrid.spec.tsx index 5df196172724..16aef713f0fa 100644 --- a/packages/manager/apps/pci-private-network/src/components/global-regions/GlobalRegionsDatagrid.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/global-regions/GlobalRegionsDatagrid.spec.tsx @@ -1,12 +1,8 @@ -import { describe, vi } from 'vitest'; +import { describe } from 'vitest'; import { render, screen } from '@testing-library/react'; import GlobalRegionsDatagrid from '@/components/global-regions/GlobalRegionsDatagrid'; import { TAggregatedNetwork } from '@/api/data/network'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key) => key }), -})); - describe('GlobalRegionsDatagrid', () => { it('should render DataGridNoResults when no networks', () => { render( diff --git a/packages/manager/apps/pci-private-network/src/components/local-zones/DeleteAction.component.spec.tsx b/packages/manager/apps/pci-private-network/src/components/local-zones/DeleteAction.component.spec.tsx index 20a5d1e432e5..d0596dc506e9 100644 --- a/packages/manager/apps/pci-private-network/src/components/local-zones/DeleteAction.component.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/components/local-zones/DeleteAction.component.spec.tsx @@ -3,10 +3,6 @@ import { render, screen } from '@testing-library/react'; import { useHref } from 'react-router-dom'; import DeleteAction from '@/components/local-zones/DeleteAction.component'; -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ t: (key) => key }), -})); - vi.mock('react-router-dom', () => ({ useHref: vi.fn(), })); diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.spec.tsx b/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.spec.tsx index 6052d45827b1..4c6b58e7ca16 100644 --- a/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.spec.tsx +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.spec.tsx @@ -1,7 +1,7 @@ import 'element-internals-polyfill'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { describe, expect, vi } from 'vitest'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import { ShellContext, @@ -12,6 +12,10 @@ import OnBoardingPage from './Onboarding.page'; vi.mock('react-router-dom'); +vi.mock('./OnBoardingGuard', () => ({ + default: ({ children }) => <>{children}, +})); + const shellContext = { environment: { getUser: vi.fn(), @@ -43,6 +47,8 @@ describe('OnBoardingPage', () => { shell.navigation.getURL.mockResolvedValue('https://www.ovh.com'); vi.mocked(useParams).mockReturnValue({ projectId: '123' }); const { container } = render(, { wrapper }); - expect(container).toBeDefined(); + await waitFor(() => { + expect(container).toBeDefined(); + }); }); }); diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.tsx b/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.tsx index 75431a21fce9..9266f077c881 100644 --- a/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.tsx +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/Onboarding.page.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components'; import { @@ -8,8 +9,12 @@ import { ODS_TEXT_LEVEL, OdsBreadcrumbAttributeItem, } from '@ovhcloud/ods-components'; -import { OsdsBreadcrumb, OsdsText } from '@ovhcloud/ods-components/react'; -import { TProject } from '@ovh-ux/manager-pci-common'; +import { + OsdsBreadcrumb, + OsdsProgressBar, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { TProject, useOperationProgress } from '@ovh-ux/manager-pci-common'; import { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -20,11 +25,19 @@ import { } from 'react-router-dom'; import { GUIDES } from './onboarding.constants'; import OnBoardingGuard from './OnboardingGuard'; +import { + PROJECT_VRACK_QUERY_KEY, + useProjectVrack, + VRACK_QUERY_KEY, +} from '@/api/hooks/useVrack'; +import { useVrackCreationOperation } from './store'; export default function OnBoardingPage() { const { t } = useTranslation('listing'); const { t: tOnboarding } = useTranslation('onboarding'); + const { t: tVrack } = useTranslation('vrack'); + const queryClient = useQueryClient(); const { projectId } = useParams(); const navigate = useNavigate(); const context = useContext(ShellContext); @@ -32,6 +45,24 @@ export default function OnBoardingPage() { const { ovhSubsidiary } = context.environment.getUser(); const project = useRouteLoaderData('private-networks') as TProject; const [urlProject, setUrlProject] = useState(''); + const { data: vrack, isPending } = useProjectVrack(projectId); + const { operationId, setOperationId } = useVrackCreationOperation(); + const vrackCreation = useOperationProgress(projectId, operationId, () => { + // add a small delay in order to let the progress bar completed + // visible for a short period of time + setTimeout(() => { + queryClient.invalidateQueries({ + queryKey: [VRACK_QUERY_KEY], + }); + queryClient.invalidateQueries({ + queryKey: [PROJECT_VRACK_QUERY_KEY], + }); + setOperationId(null); + }, 2000); + }); + + const isMissingVrack = !isPending && !vrack?.id; + const isVrackCreationPending = !!operationId && !!vrackCreation; useEffect(() => { navigation @@ -60,49 +91,79 @@ export default function OnBoardingPage() { title={tOnboarding('pci_projects_project_network_private')} description={ <> - - {tOnboarding( - 'pci_projects_project_network_private_vrack_empty', - )} - - - {tOnboarding( - 'pci_projects_project_network_private_vrack_deploy', - )} - - - {tOnboarding( - 'pci_projects_project_network_private_vrack_explanation_1', - )} - - - {tOnboarding( - 'pci_projects_project_network_private_vrack_explanation_2', - )} - + {isVrackCreationPending && ( + <> + + {tVrack( + 'pci_projects_project_network_private_vrack_pending', + )} + + + + )} + {!isVrackCreationPending && ( + <> + + {tOnboarding( + 'pci_projects_project_network_private_vrack_empty', + )} + + + {tOnboarding( + 'pci_projects_project_network_private_vrack_deploy', + )} + + + {tOnboarding( + 'pci_projects_project_network_private_vrack_explanation_1', + )} + + + {tOnboarding( + 'pci_projects_project_network_private_vrack_explanation_2', + )} + + + )} } - orderButtonLabel={t('pci_projects_project_network_private_create')} - onOrderButtonClick={() => navigate('../new')} + {...(!isVrackCreationPending && + !isPending && { + orderButtonLabel: isMissingVrack + ? tVrack( + 'pci_projects_project_network_private_vrack_create_heading', + ) + : t('pci_projects_project_network_private_create'), + })} + onOrderButtonClick={() => + isMissingVrack ? navigate('./new') : navigate('../new') + } > {GUIDES.map((guide) => { const card = { diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/__snapshots__/Onboarding.page.spec.tsx.snap b/packages/manager/apps/pci-private-network/src/pages/onboarding/__snapshots__/Onboarding.page.spec.tsx.snap new file mode 100644 index 000000000000..d40c5ea7d99b --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/__snapshots__/Onboarding.page.spec.tsx.snap @@ -0,0 +1,1119 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`OnBoardingPage > displays private network creation button if vrack exists on project 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+ + + pci_projects_project_network_private + + + + pci_projects_project_network_private_vrack_empty + + + pci_projects_project_network_private_vrack_deploy + + + pci_projects_project_network_private_vrack_explanation_1 + + + pci_projects_project_network_private_vrack_explanation_2 + + +
+ + pci_projects_project_network_private_create + +
+
+ +
+
+ , + "container":
+
+
+ + + pci_projects_project_network_private + + + + pci_projects_project_network_private_vrack_empty + + + pci_projects_project_network_private_vrack_deploy + + + pci_projects_project_network_private_vrack_explanation_1 + + + pci_projects_project_network_private_vrack_explanation_2 + + +
+ + pci_projects_project_network_private_create + +
+
+ +
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`OnBoardingPage > displays private vrack creation button if missing vrack on project 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+
+
+ + + pci_projects_project_network_private + + + + pci_projects_project_network_private_vrack_empty + + + pci_projects_project_network_private_vrack_deploy + + + pci_projects_project_network_private_vrack_explanation_1 + + + pci_projects_project_network_private_vrack_explanation_2 + + +
+ + pci_projects_project_network_private_vrack_create_heading + +
+
+ +
+
+ , + "container":
+
+
+ + + pci_projects_project_network_private + + + + pci_projects_project_network_private_vrack_empty + + + pci_projects_project_network_private_vrack_deploy + + + pci_projects_project_network_private_vrack_explanation_1 + + + pci_projects_project_network_private_vrack_explanation_2 + + +
+ + pci_projects_project_network_private_vrack_create_heading + +
+
+ +
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.spec.tsx b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.spec.tsx new file mode 100644 index 000000000000..5173563235ce --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.spec.tsx @@ -0,0 +1,51 @@ +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import { + QueryClient, + QueryClientProvider, + UseQueryResult, +} from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import * as useVrackModule from '@/api/hooks/useVrack'; +import VrackCreation from './VrackCreation.page'; +import { TVrack } from '@/api/data/vrack'; + +const shellContext = { + environment: { + getUser: vi.fn(), + }, + shell: { + navigation: { + getURL: vi.fn(), + }, + }, +}; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + + {children} + + +); + +describe('VrackCreationPage', () => { + it('renders correctly', () => { + vi.spyOn(useVrackModule, 'useVracks').mockReturnValue({ + data: [], + isPending: false, + } as UseQueryResult<(TVrack & { vrackId: string; displayName: string })[]>); + vi.spyOn(useVrackModule, 'useProjectVrack').mockReturnValue({ + data: null, + isPending: false, + } as UseQueryResult<{ id: string; name: string }>); + const { container } = render(, { wrapper }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.tsx b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.tsx new file mode 100644 index 000000000000..e1a8843ed864 --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/VrackCreation.page.tsx @@ -0,0 +1,220 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { + ODS_BUTTON_VARIANT, + ODS_SELECT_SIZE, + ODS_SPINNER_SIZE, + ODS_TEXT_LEVEL, +} from '@ovhcloud/ods-components'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { + OsdsButton, + OsdsModal, + OsdsSelect, + OsdsSelectOption, + OsdsSpinner, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { Translation, useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import { + Notifications, + useNotifications, +} from '@ovh-ux/manager-react-components'; +import { ApiError } from '@ovh-ux/manager-core-api'; +import { + useAssociateVrack, + useCreateVrack, + useProjectVrack, + useVracks, +} from '@/api/hooks/useVrack'; +import { useVrackCreationOperation } from '../store'; + +export default function VrackCreation() { + const { projectId } = useParams(); + const { t } = useTranslation('vrack'); + const { clearNotifications } = useNotifications(); + const navigate = useNavigate(); + const [isCreation, setIsCreation] = useState(true); + const [existingVrackCheck, setExistingVrackCheck] = useState(true); + const [vrackIndex, setVrackIndex] = useState('-'); + const { addError } = useNotifications(); + const { data: vracks, isPending: isVracksPending } = useVracks(); + const selectedVrack = vracks?.[vrackIndex]; + const { setOperationId } = useVrackCreationOperation(); + const { + data: projectVrack, + isPending: isProjectVrackPending, + } = useProjectVrack(projectId); + + const { createVrack, isPending: isCreationPending } = useCreateVrack({ + onSuccess: (operationId) => { + setOperationId(operationId); + navigate('..'); + }, + onError: (error: ApiError) => { + addError( + + {(_t) => + _t('pci_projects_project_network_private_vrack_create_init_error', { + message: error?.response?.data?.message || error?.message || null, + }) + } + , + true, + ); + }, + }); + + const { associateVrack, isPending: isAssociationPending } = useAssociateVrack( + { + projectId, + vrackId: selectedVrack?.vrackId, + onSuccess: () => { + navigate('..'); + }, + onError: (error: ApiError) => { + addError( + + {(_t) => + _t( + 'pci_projects_project_network_private_vrack_create_init_error', + { + message: + error?.response?.data?.message || error?.message || null, + }, + ) + } + , + true, + ); + }, + }, + ); + + useEffect(() => { + if (projectVrack && existingVrackCheck) { + navigate('..'); + } + }, [projectVrack, existingVrackCheck]); + + const isPending = + isVracksPending || + isCreationPending || + isAssociationPending || + isProjectVrackPending; + + return ( + navigate('..')} + headline={t('pci_projects_project_network_private_vrack_create_heading')} + > + + {isPending ? ( + + ) : ( +
+ setIsCreation(true)} + inline + > + {t('pci_projects_project_network_private_vrack_create_new')} + + setIsCreation(false)} + inline + > + {t('pci_projects_project_network_private_vrack_create_existing')} + +

+ {isCreation && ( + + {t( + 'pci_projects_project_network_private_vrack_create_description', + )} + + )} + {!isCreation && ( + { + if (typeof detail.value === 'string') { + setVrackIndex(detail.value); + } + }} + inline + > + + {t( + 'pci_projects_project_network_private_vrack_create_choose', + )} + + {vracks.map((vrack, index) => ( + + {vrack.displayName} + + ))} + + )} +

+
+ )} +
+ + navigate('..')} + disabled={isPending || undefined} + > + {t('pci_projects_project_network_private_vrack_create_cancel')} + + { + clearNotifications(); + setExistingVrackCheck(false); + if (isCreation) { + createVrack(projectId); + } else { + associateVrack(selectedVrack?.vrackId); + } + }} + disabled={isPending || (!isCreation && !selectedVrack) || undefined} + > + {t('pci_projects_project_network_private_vrack_create_action')} + +
+ ); +} diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/new/__snapshots__/VrackCreation.page.spec.tsx.snap b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/__snapshots__/VrackCreation.page.spec.tsx.snap new file mode 100644 index 000000000000..7b6dde73170d --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/new/__snapshots__/VrackCreation.page.spec.tsx.snap @@ -0,0 +1,56 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`VrackCreationPage > renders correctly 1`] = ` +
+ + +
+ + pci_projects_project_network_private_vrack_create_new + + + pci_projects_project_network_private_vrack_create_existing + +

+ + pci_projects_project_network_private_vrack_create_description + +

+
+
+ + pci_projects_project_network_private_vrack_create_cancel + + + pci_projects_project_network_private_vrack_create_action + +
+
+`; diff --git a/packages/manager/apps/pci-private-network/src/pages/onboarding/store.ts b/packages/manager/apps/pci-private-network/src/pages/onboarding/store.ts new file mode 100644 index 000000000000..16ecfd2d7011 --- /dev/null +++ b/packages/manager/apps/pci-private-network/src/pages/onboarding/store.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand'; + +interface VrackCreationState { + operationId: string; + setOperationId: (id: string) => void; +} + +export const useVrackCreationOperation = create((set) => ({ + operationId: null, + setOperationId: (id: string) => set({ operationId: id }), +})); diff --git a/packages/manager/apps/pci-private-network/src/routes.tsx b/packages/manager/apps/pci-private-network/src/routes.tsx index b94a71698554..d304112d6278 100644 --- a/packages/manager/apps/pci-private-network/src/routes.tsx +++ b/packages/manager/apps/pci-private-network/src/routes.tsx @@ -76,6 +76,14 @@ export default [ { path: ROUTE_PATHS.onboarding, ...lazyRouteConfig(() => import('@/pages/onboarding/Onboarding.page')), + children: [ + { + path: 'new', + ...lazyRouteConfig(() => + import('@/pages/onboarding/new/VrackCreation.page'), + ), + }, + ], }, { path: ROUTE_PATHS.new, diff --git a/packages/manager/apps/pci-private-network/src/setupTests.ts b/packages/manager/apps/pci-private-network/src/setupTests.ts index 39651aefe5e5..4f143628a03c 100644 --- a/packages/manager/apps/pci-private-network/src/setupTests.ts +++ b/packages/manager/apps/pci-private-network/src/setupTests.ts @@ -2,6 +2,29 @@ import '@testing-library/jest-dom'; import 'element-internals-polyfill'; import { vi } from 'vitest'; +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useSearchParams: () => [new URLSearchParams({})], + useParams: () => ({ projectId: 'project-id', kubeId: 'kube-id' }), + useHref: vi.fn(), + useLocation: vi.fn(), + useNavigate: vi.fn(), + Navigate: () => null, + Outlet: vi.fn(() => 'Outlet'), + }; +}); + +vi.mock('@ovh-ux/manager-react-components', async () => { + const mod = await vi.importActual('@ovh-ux/manager-react-components'); + return { + ...mod, + useProjectUrl: vi.fn().mockReturnValue('mockProjectUrl'), + Notifications: vi.fn(), + }; +}); + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (translationKey: string) => translationKey, @@ -10,3 +33,17 @@ vi.mock('react-i18next', () => ({ }, }), })); + +vi.mock('@ovh-ux/manager-core-api', async () => { + const mod = await vi.importActual('@ovh-ux/manager-core-api'); + return { + ...mod, + fetchIcebergV6: vi.fn(), + v6: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }, + }; +}); diff --git a/packages/manager/apps/pci-private-network/vitest.config.js b/packages/manager/apps/pci-private-network/vitest.config.js index 156daa6627e5..130783c024e2 100644 --- a/packages/manager/apps/pci-private-network/vitest.config.js +++ b/packages/manager/apps/pci-private-network/vitest.config.js @@ -2,9 +2,24 @@ import path from 'path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +function relativeImgPathImport() { + return { + name: 'relative-img-path-import', + transform(_code, id) { + if (/(jpg|jpeg|png|webp|gif|svg)$/.test(id)) { + const imgSrc = path.relative(process.cwd(), id); + return { + code: `export default '${imgSrc}'`, + }; + } + return undefined; + }, + }; +} + // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), relativeImgPathImport()], test: { globals: true, environment: 'jsdom', diff --git a/packages/manager/modules/manager-pci-common/src/api/data/index.ts b/packages/manager/modules/manager-pci-common/src/api/data/index.ts index fd2e50ec5688..057ecb0dfc2d 100644 --- a/packages/manager/modules/manager-pci-common/src/api/data/index.ts +++ b/packages/manager/modules/manager-pci-common/src/api/data/index.ts @@ -2,5 +2,6 @@ export * from './availability'; export * from './catalog'; export * from './flavors'; export * from './instance'; +export * from './operation'; export * from './project'; export * from './regions'; diff --git a/packages/manager/modules/manager-pci-common/src/api/data/operation.ts b/packages/manager/modules/manager-pci-common/src/api/data/operation.ts new file mode 100644 index 000000000000..43190f46735e --- /dev/null +++ b/packages/manager/modules/manager-pci-common/src/api/data/operation.ts @@ -0,0 +1,36 @@ +import { v6 } from '@ovh-ux/manager-core-api'; + +export enum OperationStatus { + Completed = 'completed', + Created = 'created', + InError = 'in-error', + InProgress = 'in-progress', + Unknown = 'unknown', +} + +export type TOperation = { + action: string; + completedAt: string | null; + createdAt: string; + id: string; + progress: number; + regions: string[]; + resourceId: string | null; + startedAt: string | null; + status: OperationStatus; +}; + +export const getOperation = async ( + projectId: string, + operationId: string, +): Promise => { + try { + const { data } = await v6.get( + `/cloud/project/${projectId}/operation/${operationId}`, + ); + return data; + } catch (err) { + if (err?.response?.status === 404) return null; + throw err; + } +}; diff --git a/packages/manager/modules/manager-pci-common/src/api/hook/index.ts b/packages/manager/modules/manager-pci-common/src/api/hook/index.ts index a45ab8372b0a..c85b51e59033 100644 --- a/packages/manager/modules/manager-pci-common/src/api/hook/index.ts +++ b/packages/manager/modules/manager-pci-common/src/api/hook/index.ts @@ -1,6 +1,7 @@ export * from './useAvailability'; export * from './useCatalog'; export * from './useFlavors'; +export * from './useOperation'; export * from './useProject'; export * from './useRegions'; export * from './useInstance'; diff --git a/packages/manager/modules/manager-pci-common/src/api/hook/useOperation.ts b/packages/manager/modules/manager-pci-common/src/api/hook/useOperation.ts new file mode 100644 index 000000000000..6387473c204d --- /dev/null +++ b/packages/manager/modules/manager-pci-common/src/api/hook/useOperation.ts @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getOperation, OperationStatus, TOperation } from '../data/operation'; + +export const useOperationProgress = ( + projectId: string, + operationId: string, + onCompleted: (status: OperationStatus) => void, +) => { + const [progress, setProgress] = useState({ + percentage: 0, + status: OperationStatus.Unknown, + }); + + const onCompletedRef = useRef(onCompleted); + const notifyCompletion = (status: OperationStatus) => { + if (onCompletedRef.current) { + onCompletedRef.current(status); + onCompletedRef.current = null; + } + }; + + const { data: operation, error } = useQuery({ + queryKey: ['pci-operation-progress', projectId, operationId], + queryFn: () => getOperation(projectId, operationId), + enabled: + !!projectId && + !!operationId && + progress.percentage < 100 && + ![OperationStatus.Completed, OperationStatus.InError].includes( + progress.status, + ), + refetchInterval: 3000, + retry: 5, + }); + + useEffect(() => { + // if operation doesn't exist (404) we assume it is done and successfull + if (operation === null) { + setProgress({ + percentage: 100, + status: OperationStatus.Completed, + }); + notifyCompletion(OperationStatus.Completed); + } else if (operation) { + const status = operation.status || OperationStatus.Unknown; + setProgress({ + percentage: operation.progress, + status, + }); + if (operation.progress === 100) { + notifyCompletion(status); + } + } + }, [operation]); + + useEffect(() => { + if (error) { + setProgress({ + percentage: 100, + status: OperationStatus.InError, + }); + notifyCompletion(OperationStatus.InError); + } + }, [error]); + + return progress; +};