From 51aca20e5f18d85b912de4986adbe9923466217a Mon Sep 17 00:00:00 2001 From: Thibault Barske Date: Thu, 25 Jun 2026 16:33:56 +0200 Subject: [PATCH 1/4] feat(dashboard-tile): add VCDA migration tile, routing & data foundation ref: #BKP-1174 Signed-off-by: Thibault Barske --- .../translations/vcda/Messages_de_DE.json | 31 +++ .../translations/vcda/Messages_en_GB.json | 31 +++ .../translations/vcda/Messages_es_ES.json | 31 +++ .../translations/vcda/Messages_fr_CA.json | 31 +++ .../translations/vcda/Messages_fr_FR.json | 31 +++ .../translations/vcda/Messages_it_IT.json | 31 +++ .../translations/vcda/Messages_pl_PL.json | 31 +++ .../translations/vcda/Messages_pt_PT.json | 31 +++ .../MigrationTile.component.tsx | 112 ++++++++++ .../migration-tile/MigrationTile.spec.tsx | 202 ++++++++++++++++++ .../_components/ActiveBody.component.tsx | 72 +++++++ .../ComingSoonButton.component.tsx | 37 ++++ .../vcda/VcdaFeatureGuard.component.tsx | 27 +++ .../src/data/hooks/vcda/useVcdaStatus.hook.ts | 72 +++++++ .../src/data/hooks/vcda/useVcdaStatus.spec.ts | 50 +++++ .../hpc-vmware-public-vcf-aas/src/index.tsx | 2 +- .../OrganizationGeneralInformation.page.tsx | 6 + .../src/test-utils/test-i18n.ts | 2 + .../src/utils/features.constants.ts | 1 + .../src/utils/testIds.constants.ts | 6 + .../manager/modules/vcd-api/src/api/index.ts | 2 + .../vcd-api/src/api/vcda-migration-order.ts | 87 ++++++++ .../modules/vcd-api/src/api/vcda-migration.ts | 24 +++ .../modules/vcd-api/src/types/index.ts | 1 + .../vcd-api/src/types/vcda-migration.type.ts | 54 +++++ 25 files changed, 1004 insertions(+), 1 deletion(-) create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/vcda/VcdaFeatureGuard.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.hook.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.spec.ts create mode 100644 packages/manager/modules/vcd-api/src/api/vcda-migration-order.ts create mode 100644 packages/manager/modules/vcd-api/src/api/vcda-migration.ts create mode 100644 packages/manager/modules/vcd-api/src/types/vcda-migration.type.ts diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json new file mode 100644 index 000000000000..fa586136dc8d --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migration", + "cta": { + "order": "Bestellen", + "orderDisabledTooltip": { + "noPermission": "Sie sind nicht berechtigt, diesen Dienst zu bestellen." + } + }, + "badge": { + "actif": "Aktiv", + "provisioning": "Wird bereitgestellt…", + "deleting": "Wird gelöscht…", + "suspended": "Ausgesetzt", + "error": "Fehler", + "updateInProgress": "Aktualisierung läuft" + }, + "error": { + "fetch": "Status nicht verfügbar.", + "retry": "Erneut versuchen" + }, + "ariaLabel": { + "active": "VCDA-Migration: aktiv", + "inactive": "VCDA-Migration: nicht aktiv. Klicken Sie auf Bestellen, um zu abonnieren.", + "provisioning": "VCDA-Migration: wird bereitgestellt." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json new file mode 100644 index 000000000000..408b70be9e4e --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migration", + "cta": { + "order": "Order", + "orderDisabledTooltip": { + "noPermission": "You do not have permission to order this service." + } + }, + "badge": { + "actif": "Active", + "provisioning": "Provisioning…", + "deleting": "Deleting…", + "suspended": "Suspended", + "error": "Error", + "updateInProgress": "Update in progress" + }, + "error": { + "fetch": "Status unavailable.", + "retry": "Retry" + }, + "ariaLabel": { + "active": "VCDA migration: active", + "inactive": "VCDA migration: not active. Click Order to subscribe.", + "provisioning": "VCDA migration: provisioning." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json new file mode 100644 index 000000000000..a4adc8af9731 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migración", + "cta": { + "order": "Solicitar", + "orderDisabledTooltip": { + "noPermission": "No tiene permiso para solicitar este servicio." + } + }, + "badge": { + "actif": "Activo", + "provisioning": "Aprovisionando…", + "deleting": "Eliminando…", + "suspended": "Suspendido", + "error": "Error", + "updateInProgress": "Actualización en curso" + }, + "error": { + "fetch": "Estado no disponible.", + "retry": "Reintentar" + }, + "ariaLabel": { + "active": "Migración VCDA: activa", + "inactive": "Migración VCDA: no activa. Haga clic en Solicitar para suscribirse.", + "provisioning": "Migración VCDA: aprovisionando." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json new file mode 100644 index 000000000000..7db26b688811 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migration", + "cta": { + "order": "Commander", + "orderDisabledTooltip": { + "noPermission": "Vous n'avez pas l'autorisation de commander ce service." + } + }, + "badge": { + "actif": "Actif", + "provisioning": "Provisionnement…", + "deleting": "Suppression…", + "suspended": "Suspendu", + "error": "Erreur", + "updateInProgress": "Mise à jour en cours" + }, + "error": { + "fetch": "Statut indisponible.", + "retry": "Réessayer" + }, + "ariaLabel": { + "active": "Migration VCDA : active", + "inactive": "Migration VCDA : non active. Cliquez sur Commander pour souscrire.", + "provisioning": "Migration VCDA : provisionnement en cours." + } + }, + "serviceTermination": { + "cta": "Résilier le service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json new file mode 100644 index 000000000000..7db26b688811 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migration", + "cta": { + "order": "Commander", + "orderDisabledTooltip": { + "noPermission": "Vous n'avez pas l'autorisation de commander ce service." + } + }, + "badge": { + "actif": "Actif", + "provisioning": "Provisionnement…", + "deleting": "Suppression…", + "suspended": "Suspendu", + "error": "Erreur", + "updateInProgress": "Mise à jour en cours" + }, + "error": { + "fetch": "Statut indisponible.", + "retry": "Réessayer" + }, + "ariaLabel": { + "active": "Migration VCDA : active", + "inactive": "Migration VCDA : non active. Cliquez sur Commander pour souscrire.", + "provisioning": "Migration VCDA : provisionnement en cours." + } + }, + "serviceTermination": { + "cta": "Résilier le service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json new file mode 100644 index 000000000000..1800a9eaf947 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migrazione", + "cta": { + "order": "Ordina", + "orderDisabledTooltip": { + "noPermission": "Non disponi dell'autorizzazione per ordinare questo servizio." + } + }, + "badge": { + "actif": "Attivo", + "provisioning": "Provisioning in corso…", + "deleting": "Eliminazione in corso…", + "suspended": "Sospeso", + "error": "Errore", + "updateInProgress": "Aggiornamento in corso" + }, + "error": { + "fetch": "Stato non disponibile.", + "retry": "Riprova" + }, + "ariaLabel": { + "active": "Migrazione VCDA: attiva", + "inactive": "Migrazione VCDA: non attiva. Clicca su Ordina per sottoscrivere.", + "provisioning": "Migrazione VCDA: provisioning in corso." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json new file mode 100644 index 000000000000..07776fdcfb56 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migracja", + "cta": { + "order": "Zamów", + "orderDisabledTooltip": { + "noPermission": "Nie masz uprawnień do zamówienia tej usługi." + } + }, + "badge": { + "actif": "Aktywna", + "provisioning": "Przygotowywanie…", + "deleting": "Usuwanie…", + "suspended": "Zawieszona", + "error": "Błąd", + "updateInProgress": "Aktualizacja w toku" + }, + "error": { + "fetch": "Status niedostępny.", + "retry": "Ponów" + }, + "ariaLabel": { + "active": "Migracja VCDA: aktywna", + "inactive": "Migracja VCDA: nieaktywna. Kliknij Zamów, aby wykupić usługę.", + "provisioning": "Migracja VCDA: przygotowywanie." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json new file mode 100644 index 000000000000..cc8ce2f08be6 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json @@ -0,0 +1,31 @@ +{ + "tile": { + "title": "Migração", + "cta": { + "order": "Encomendar", + "orderDisabledTooltip": { + "noPermission": "Não tem permissão para encomendar este serviço." + } + }, + "badge": { + "actif": "Ativo", + "provisioning": "A aprovisionar…", + "deleting": "A eliminar…", + "suspended": "Suspenso", + "error": "Erro", + "updateInProgress": "Atualização em curso" + }, + "error": { + "fetch": "Estado indisponível.", + "retry": "Tentar novamente" + }, + "ariaLabel": { + "active": "Migração VCDA: ativa", + "inactive": "Migração VCDA: não ativa. Clique em Encomendar para subscrever.", + "provisioning": "Migração VCDA: a aprovisionar." + } + }, + "serviceTermination": { + "cta": "Terminate service" + } +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx new file mode 100644 index 000000000000..960fbfb31b78 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { DashboardTile } from '@ovh-ux/manager-react-components'; +import { + OdsBadge, + OdsButton, + OdsMessage, + OdsSkeleton, + OdsText, +} from '@ovhcloud/ods-components/react'; +import { + ODS_BADGE_COLOR, + ODS_BUTTON_COLOR, + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, +} from '@ovhcloud/ods-components'; +import { useVcdaStatus } from '@/data/hooks/vcda/useVcdaStatus.hook'; +import TEST_IDS from '@/utils/testIds.constants'; +import ComingSoonButton from './_components/ComingSoonButton.component'; +import ActiveBody from './_components/ActiveBody.component'; + +const MIGRATION_TILE_ID = 'migration'; + +export default function MigrationTile() { + const { t } = useTranslation('vcda'); + const { id } = useParams(); + const { data: status, isPending, isError, refetch } = useVcdaStatus(id); + + const renderBody = () => { + if (isPending) { + return ; + } + + if (isError || !status) { + return ( + +
+ {t('tile.error.fetch')} + refetch()} + /> +
+
+ ); + } + + switch (status.kind) { + case 'inactive': + return ( + + ); + case 'provisioning': + return ( + + ); + case 'deleting': + return ( +
+ + +
+ ); + case 'active': + default: + return ; + } + }; + + return ( +
+ + {renderBody()} +
+ ), + }, + ]} + /> + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx new file mode 100644 index 000000000000..b02017e828b6 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx @@ -0,0 +1,202 @@ +import { act, render } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach, beforeAll } from 'vitest'; +import React from 'react'; +import { i18n } from 'i18next'; +import { I18nextProvider } from 'react-i18next'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import { useVcdOrganization } from '@ovh-ux/manager-module-vcd-api'; +import { + assertElementLabel, + assertElementVisibility, + getElementByTestId, + initTestI18n, +} from '@ovh-ux/manager-core-test-utils'; +import userEvent from '@testing-library/user-event'; +import MigrationTile from './MigrationTile.component'; +import { useVcdaStatus } from '../../../data/hooks/vcda/useVcdaStatus.hook'; +import { labels, translations } from '../../../test-utils/test-i18n'; +import vcdaMessages from '../../../../public/translations/vcda/Messages_fr_FR.json'; +import TEST_IDS from '../../../utils/testIds.constants'; +import { APP_NAME } from '../../../tracking.constants'; + +const navigateMock = vi.fn(); +const trackClickMock = vi.fn(); +const refetchMock = vi.fn(); + +vi.mock('react-router-dom', async (importOriginal) => { + const original: typeof import('react-router-dom') = await importOriginal(); + return { + ...original, + useNavigate: () => navigateMock, + useParams: () => ({ id: 'org-123' }), + }; +}); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-shell-client') = await importOriginal(); + return { + ...original, + useOvhTracking: () => ({ trackClick: trackClickMock }), + }; +}); + +vi.mock('@ovh-ux/manager-module-vcd-api', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-module-vcd-api') = await importOriginal(); + return { + ...original, + useVcdOrganization: vi.fn(), + }; +}); + +vi.mock('../../../data/hooks/vcda/useVcdaStatus.hook', () => ({ + useVcdaStatus: vi.fn(), +})); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-components') = await importOriginal(); + return { + ...original, + ManagerButton: ({ + label, + onClick, + 'data-testid': dataTestId, + 'aria-label': ariaLabel, + }: { + label: string; + onClick?: () => void; + 'data-testid'?: string; + 'aria-label'?: string; + }) => + React.createElement('ods-button', { + label, + 'data-testid': dataTestId, + 'aria-label': ariaLabel, + onClick, + }), + }; +}); + +let i18nState: i18n; + +const shellContext = { + shell: { + navigation: { getURL: vi.fn().mockResolvedValue('https://www.ovh.com') }, + }, +}; + +const mockStatus = (overrides: Partial>) => { + vi.mocked(useVcdaStatus).mockReturnValue({ + data: undefined, + isPending: false, + isLoading: false, + isError: false, + refetch: refetchMock, + ...overrides, + }); +}; + +const renderComponent = () => { + const queryClient = new QueryClient(); + return render( + + + + + + + , + ); +}; + +describe('MigrationTile', () => { + beforeAll(async () => { + i18nState = await initTestI18n(APP_NAME, translations); + i18nState.addResourceBundle('fr_FR', 'vcda', vcdaMessages, true, true); + }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useVcdOrganization).mockReturnValue({ + data: { data: { iam: { urn: 'urn:org-123' } } }, + } as ReturnType); + }); + + it('renders the loading skeleton while the status is in flight', async () => { + mockStatus({ isPending: true }); + renderComponent(); + const skeleton = await getElementByTestId(TEST_IDS.migrationTileSkeleton); + await assertElementVisibility(skeleton); + }); + + it('renders the inline error with a retry action and refetches on click', async () => { + const user = userEvent.setup(); + mockStatus({ isError: true }); + renderComponent(); + + const error = await getElementByTestId(TEST_IDS.migrationTileError); + await assertElementVisibility(error); + + const retry = await getElementByTestId(TEST_IDS.migrationTileRetryCta); + await act(() => user.click(retry)); + expect(refetchMock).toHaveBeenCalled(); + }); + + + it('renders the provisioning badge for CREATING', async () => { + mockStatus({ data: { kind: 'provisioning' } }); + renderComponent(); + const badge = await getElementByTestId( + TEST_IDS.migrationTileProvisioningBadge, + ); + await assertElementLabel({ + element: badge, + label: labels.vcda.tile.badge.provisioning, + }); + }); + + it('renders the deleting badge for DELETING', async () => { + mockStatus({ data: { kind: 'deleting' } }); + renderComponent(); + const badge = await getElementByTestId(TEST_IDS.migrationTileDeletingBadge); + await assertElementLabel({ + element: badge, + label: labels.vcda.tile.badge.deleting, + }); + }); + + it('renders the Actif badge for READY', async () => { + mockStatus({ data: { kind: 'active', resourceStatus: 'READY' } }); + renderComponent(); + const badge = await getElementByTestId(TEST_IDS.migrationTileStatusBadge); + await assertElementLabel({ + element: badge, + label: labels.vcda.tile.badge.actif, + }); + }); + + it('renders the Suspendu badge for SUSPENDED', async () => { + mockStatus({ data: { kind: 'active', resourceStatus: 'SUSPENDED' } }); + renderComponent(); + const badge = await getElementByTestId(TEST_IDS.migrationTileStatusBadge); + await assertElementLabel({ + element: badge, + label: labels.vcda.tile.badge.suspended, + }); + }); + + it('renders the Erreur badge for ERROR', async () => { + mockStatus({ data: { kind: 'active', resourceStatus: 'ERROR' } }); + renderComponent(); + const badge = await getElementByTestId(TEST_IDS.migrationTileStatusBadge); + await assertElementLabel({ + element: badge, + label: labels.vcda.tile.badge.error, + }); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx new file mode 100644 index 000000000000..bb983023bac8 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from 'react-i18next'; +import { OdsBadge, OdsText } from '@ovhcloud/ods-components/react'; +import { ODS_BADGE_COLOR, ODS_BUTTON_COLOR } from '@ovhcloud/ods-components'; +import { + VcdaResourceStatus, + VcdaTileStatus, +} from '@ovh-ux/manager-module-vcd-api'; +import TEST_IDS from '@/utils/testIds.constants'; +import ComingSoonButton from './ComingSoonButton.component'; + +type BadgeConfig = { + color: ODS_BADGE_COLOR; + labelKey: string; + ariaKey?: string; + captionKey?: string; +}; + +const STATUS_BADGE: Partial> = { + SUSPENDED: { + color: ODS_BADGE_COLOR.warning, + labelKey: 'tile.badge.suspended', + }, + ERROR: { color: ODS_BADGE_COLOR.critical, labelKey: 'tile.badge.error' }, + UPDATING: { + color: ODS_BADGE_COLOR.success, + labelKey: 'tile.badge.actif', + ariaKey: 'tile.ariaLabel.active', + captionKey: 'tile.badge.updateInProgress', + }, +}; + +const DEFAULT_BADGE: BadgeConfig = { + color: ODS_BADGE_COLOR.success, + labelKey: 'tile.badge.actif', + ariaKey: 'tile.ariaLabel.active', +}; + +export default function ActiveBody({ + status, +}: Readonly<{ status: VcdaTileStatus }>) { + const { t } = useTranslation('vcda'); + if (status.kind !== 'active') return null; + + const cfg = STATUS_BADGE[status.resourceStatus] ?? DEFAULT_BADGE; + + const badge = ( + + ); + + return ( +
+ {cfg.captionKey ? ( +
+ {badge} + {t(cfg.captionKey)} +
+ ) : ( + badge + )} + +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx new file mode 100644 index 000000000000..e4ddbab38553 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import { OdsButton, OdsTooltip } from '@ovhcloud/ods-components/react'; +import { + ODS_BUTTON_COLOR, + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, +} from '@ovhcloud/ods-components'; + +// Disabled placeholder CTA. The dashboard tile is complete & previewable, but the +// Order and Terminate actions are wired by the order / service-termination feature +// PRs — until then they are inert and flagged "coming soon" (not production-ready). +export default function ComingSoonButton({ + triggerId, + label, + color, +}: Readonly<{ + triggerId: string; + label: string; + color?: ODS_BUTTON_COLOR; +}>) { + const { t: tDashboard } = useTranslation('dashboard'); + return ( + <> + + + {tDashboard('managed_vcd_dashboard_coming_soon')} + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/vcda/VcdaFeatureGuard.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/vcda/VcdaFeatureGuard.component.tsx new file mode 100644 index 000000000000..0b6035104174 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/vcda/VcdaFeatureGuard.component.tsx @@ -0,0 +1,27 @@ +import { PropsWithChildren } from 'react'; +import { useParams } from 'react-router-dom'; +import { + RedirectionGuard, + useFeatureAvailability, +} from '@ovh-ux/manager-react-components'; +import { FEATURES } from '@/utils/features.constants'; +import { urls } from '@/routes/routes.constant'; + +export default function VcdaFeatureGuard({ + children, +}: Readonly) { + const { id } = useParams(); + const { data: features, isLoading } = useFeatureAvailability([ + FEATURES.HPC_VCFAAS_VCDA, + ]); + + return ( + + {children} + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.hook.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.hook.ts new file mode 100644 index 000000000000..b757e55b7b60 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.hook.ts @@ -0,0 +1,72 @@ +import { useQuery } from '@tanstack/react-query'; +import { + getVcdaMigrationList, + matchesVcdaOrg, + PublicVcda, + VcdaResourceStatus, + VcdaTileStatus, +} from '@ovh-ux/manager-module-vcd-api'; + +export const getVcdaStatusQueryKey = (orgId: string) => [ + 'public-vcf', + orgId, + 'vcda-status', +]; + +const POLLED_STATUSES: VcdaResourceStatus[] = [ + 'CREATING', + 'UPDATING', + 'DELETING', +]; + +export const deriveTileStatus = ( + entity: PublicVcda | undefined, +): VcdaTileStatus => { + if (!entity) { + return { kind: 'inactive' }; + } + switch (entity.resourceStatus) { + case 'CREATING': + return { kind: 'provisioning' }; + case 'DELETING': + return { kind: 'deleting' }; + default: + return { kind: 'active', resourceStatus: entity.resourceStatus }; + } +}; + +export interface UseVcdaStatusResult { + data: VcdaTileStatus | undefined; + isPending: boolean; + isLoading: boolean; + isError: boolean; + refetch: () => void; +} + +export const useVcdaStatus = ( + orgId: string, + enabled = true, +): UseVcdaStatusResult => { + const { data, isPending, isLoading, isError, refetch } = useQuery({ + queryKey: getVcdaStatusQueryKey(orgId), + queryFn: getVcdaMigrationList, + enabled: enabled && !!orgId, + staleTime: 30_000, + select: (migrations: PublicVcda[]) => + deriveTileStatus( + migrations.find((migration) => + matchesVcdaOrg(migration.currentState?.organizationId, orgId), + ), + ), + refetchInterval: (query) => { + const matched = query.state.data?.find((migration) => + matchesVcdaOrg(migration.currentState?.organizationId, orgId), + ); + return matched && POLLED_STATUSES.includes(matched.resourceStatus) + ? 30_000 + : false; + }, + }); + + return { data, isPending, isLoading, isError, refetch }; +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.spec.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.spec.ts new file mode 100644 index 000000000000..13b73ab9b9e0 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaStatus.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { deriveTileStatus, getVcdaStatusQueryKey } from './useVcdaStatus.hook'; +import { PublicVcda, VcdaResourceStatus } from '@ovh-ux/manager-module-vcd-api'; + +const makeEntity = (resourceStatus: VcdaResourceStatus): PublicVcda => ({ + id: 'mig-1', + resourceStatus, + currentState: { organizationId: 'org-123' }, + targetSpec: { ips: [] }, + createdAt: '2026-06-01', + updatedAt: '2026-06-01', +}); + +describe('useVcdaStatus query key', () => { + it('builds a hierarchical key scoped to the org', () => { + expect(getVcdaStatusQueryKey('org-123')).toEqual([ + 'public-vcf', + 'org-123', + 'vcda-status', + ]); + }); +}); + +describe('deriveTileStatus (R6bis / R8 mapping)', () => { + it('maps an absent org to inactive', () => { + expect(deriveTileStatus(undefined)).toEqual({ kind: 'inactive' }); + }); + + it('maps CREATING to provisioning', () => { + expect(deriveTileStatus(makeEntity('CREATING'))).toEqual({ + kind: 'provisioning', + }); + }); + + it('maps DELETING to deleting', () => { + expect(deriveTileStatus(makeEntity('DELETING'))).toEqual({ + kind: 'deleting', + }); + }); + + it.each(['READY', 'UPDATING', 'SUSPENDED', 'ERROR'] as VcdaResourceStatus[])( + 'maps %s to active with its resourceStatus', + (resourceStatus) => { + expect(deriveTileStatus(makeEntity(resourceStatus))).toEqual({ + kind: 'active', + resourceStatus, + }); + }, + ); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/index.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/index.tsx index 68a86e7450d2..0ec05e8ab120 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/index.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/index.tsx @@ -32,7 +32,7 @@ const init = async (appName: string) => { context, reloadOnLocaleChange: true, defaultNS: appName, - ns: ['listing', 'dashboard', 'onboarding'], + ns: ['listing', 'dashboard', 'onboarding', 'vcda'], }); const region = context.environment.getRegion(); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/general-information/OrganizationGeneralInformation.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/general-information/OrganizationGeneralInformation.page.tsx index 4dcbe26420ef..6571ab8f7f14 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/general-information/OrganizationGeneralInformation.page.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/general-information/OrganizationGeneralInformation.page.tsx @@ -3,12 +3,15 @@ import { isStatusTerminated, useVcdOrganization, } from '@ovh-ux/manager-module-vcd-api'; +import { useFeatureAvailability } from '@ovh-ux/manager-react-components'; import Errors from '@/components/error/Error.component'; import Loading from '@/components/loading/Loading.component'; import OrganizationGenerationInformationTile from '@/components/tiles/organization-general-information-tile/OrganizationGeneralInformationTile.component'; import OrganizationOptionsTile from '@/components/tiles/organization-options-tile/OrganizationOptionsTile.component'; import OrganizationDataProtectionTile from '@/components/tiles/organization-data-tile/OrganizationDataProtectionTile.component'; import OrganizationServiceManagementTile from '@/components/tiles/organization-service-tile/OrganizationServiceManagementTile.component'; +import MigrationTile from '@/components/tiles/migration-tile/MigrationTile.component'; +import { FEATURES } from '@/utils/features.constants'; export default function GeneralInformation() { const { id } = useParams(); @@ -22,6 +25,8 @@ export default function GeneralInformation() { id, refetchInterval: 60 * 1000, }); + const { data: features } = useFeatureAvailability([FEATURES.HPC_VCFAAS_VCDA]); + const isVcdaEnabled = !!features?.[FEATURES.HPC_VCFAAS_VCDA]; if (isError || isRefetchError) { return ; @@ -48,6 +53,7 @@ export default function GeneralInformation() { + {isVcdaEnabled && } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts index 4d76bb31b60c..07699dc7f943 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts @@ -9,6 +9,7 @@ import datacentresVrackSegment from '../../public/translations/datacentres/vrack import terminate from '../../public/translations/terminate/Messages_fr_FR.json'; import networkAcl from '../../public/translations/networkAcl/Messages_fr_FR.json'; import zodError from '../../public/translations/zodError/Messages_fr_FR.json'; +import vcda from '../../public/translations/vcda/Messages_fr_FR.json'; const error = { manager_error_page_title: 'Oops …!', @@ -89,4 +90,5 @@ export const labels = { networkAcl, commun, zodError, + vcda, }; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/features.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/features.constants.ts index b0da614a072d..bf2dc51c71c7 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/features.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/features.constants.ts @@ -3,4 +3,5 @@ import { APP_NAME } from '@/tracking.constants'; export const FEATURES = { APP: APP_NAME, COMPUTE_SPECIAL_OFFER_BANNER: `${APP_NAME}:compute-special-offer-banner`, + HPC_VCFAAS_VCDA: `${APP_NAME}:vcda`, }; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts index defa9924cf2e..16829ac1bf61 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts @@ -20,6 +20,12 @@ const TEST_IDS = { networkAclCta: 'network-acl-cta', networkAclAddCurrentIpAction: 'add-current-ip-button', networkAclfromAnywhereIpAction: 'from-anywhere-ip-button', + migrationTileStatusBadge: 'migration-tile-status-badge', + migrationTileProvisioningBadge: 'migration-tile-provisioning-badge', + migrationTileDeletingBadge: 'migration-tile-deleting-badge', + migrationTileSkeleton: 'migration-tile-skeleton', + migrationTileError: 'migration-tile-error', + migrationTileRetryCta: 'migration-tile-retry-cta', } as const; export default TEST_IDS; diff --git a/packages/manager/modules/vcd-api/src/api/index.ts b/packages/manager/modules/vcd-api/src/api/index.ts index 507057f0471d..e94b6544372a 100644 --- a/packages/manager/modules/vcd-api/src/api/index.ts +++ b/packages/manager/modules/vcd-api/src/api/index.ts @@ -4,3 +4,5 @@ export * from './vcd-organization'; export * from './vcd-reset-password'; export * from './veeam-backup'; export * from './vcd-network-acl'; +export * from './vcda-migration'; +export * from './vcda-migration-order'; diff --git a/packages/manager/modules/vcd-api/src/api/vcda-migration-order.ts b/packages/manager/modules/vcd-api/src/api/vcda-migration-order.ts new file mode 100644 index 000000000000..199170c1c083 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/api/vcda-migration-order.ts @@ -0,0 +1,87 @@ +import { + createCart, + postOrderCartCartIdCheckout, +} from '@ovh-ux/manager-module-order'; +import { VCDOrganization } from '../types'; + +export const VCDA_ORDER = { + PLAN_CODE: 'vcda-migration', + PRODUCT_FAMILY: 'vcdaMigration', + PRICING_MODE: 'default', + DURATION: 'P1M', + QUANTITY: 1, +} as const; + +export const VCDA_ORDER_CONFIG_LABEL = { + ORG_ID: 'org-id', + EXTERNAL_IPS: 'external-ips', + DATACENTER_ZONE: 'datacenter-zone', +} as const; + +export interface OrderVcdaConfig { + orgId: string; + externalIp: string; + datacenterZone: string; +} + +export type SubmitVcdaOrderParams = { + ovhSubsidiary: string; + config: OrderVcdaConfig; +}; + +export type VcdaContract = { + name: string; + url: string; + content?: string; +}; + +export type PreparedVcdaOrder = { + cartId: string; + contractList: VcdaContract[]; +}; + +export const getVcdaDatacenterZone = (organization?: VCDOrganization): string => + organization?.currentState?.region?.toLowerCase() ?? ''; + +export const prepareVcdaOrder = async ({ + ovhSubsidiary, + config, +}: SubmitVcdaOrderParams): Promise => { + const { cartId, contractList } = await createCart({ + ovhSubsidiary, + items: [ + { + itemEndpoint: VCDA_ORDER.PRODUCT_FAMILY, + options: { + planCode: VCDA_ORDER.PLAN_CODE, + pricingMode: VCDA_ORDER.PRICING_MODE, + duration: VCDA_ORDER.DURATION, + quantity: VCDA_ORDER.QUANTITY, + }, + configurations: [ + { label: VCDA_ORDER_CONFIG_LABEL.ORG_ID, value: config.orgId }, + { + label: VCDA_ORDER_CONFIG_LABEL.EXTERNAL_IPS, + value: config.externalIp, + }, + { + label: VCDA_ORDER_CONFIG_LABEL.DATACENTER_ZONE, + value: config.datacenterZone, + }, + ], + }, + ], + }); + + return { cartId, contractList: contractList ?? [] }; +}; + +export const checkoutVcdaOrder = async (cartId: string) => { + const { data } = await postOrderCartCartIdCheckout({ + cartId, + autoPayWithPreferredPaymentMethod: true, + waiveRetractationPeriod: true, + }); + + return data; +}; diff --git a/packages/manager/modules/vcd-api/src/api/vcda-migration.ts b/packages/manager/modules/vcd-api/src/api/vcda-migration.ts new file mode 100644 index 000000000000..4f1d82822a23 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/api/vcda-migration.ts @@ -0,0 +1,24 @@ +import { ApiResponse, apiClient } from '@ovh-ux/manager-core-api'; +import { PublicVcda, PublicVcdaTargetSpec } from '../types/vcda-migration.type'; + +export const VCDA_MIGRATION_ROUTE = '/vmwareCloudDirector/migration'; + +export const getVcdaMigrationList = async (): Promise => { + const { data } = await apiClient.v2.get(VCDA_MIGRATION_ROUTE); + return data; +}; + +export const getVcdaMigration = async ( + migrationId: string, +): Promise => { + const { data } = await apiClient.v2.get( + `${VCDA_MIGRATION_ROUTE}/${migrationId}`, + ); + return data; +}; + +export const updateVcdaMigrationWhitelist = ( + migrationId: string, + targetSpec: PublicVcdaTargetSpec, +): Promise> => + apiClient.v2.put(`${VCDA_MIGRATION_ROUTE}/${migrationId}`, { targetSpec }); diff --git a/packages/manager/modules/vcd-api/src/types/index.ts b/packages/manager/modules/vcd-api/src/types/index.ts index dd7bf04b0fd1..250b11da3c27 100644 --- a/packages/manager/modules/vcd-api/src/types/index.ts +++ b/packages/manager/modules/vcd-api/src/types/index.ts @@ -10,3 +10,4 @@ export * from './vcd-storage.type'; export * from './vcd-utility.type'; export * from './vcd-vrack-segment.type'; export * from './vcd-network-acl.type'; +export * from './vcda-migration.type'; diff --git a/packages/manager/modules/vcd-api/src/types/vcda-migration.type.ts b/packages/manager/modules/vcd-api/src/types/vcda-migration.type.ts new file mode 100644 index 000000000000..974ae68450e2 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/types/vcda-migration.type.ts @@ -0,0 +1,54 @@ +export type VcdaResourceStatus = + | 'CREATING' + | 'READY' + | 'UPDATING' + | 'DELETING' + | 'SUSPENDED' + | 'ERROR' + | 'OUT_OF_SYNC' + | 'UNKNOWN'; + +export interface PublicCurrentTask { + id: string; + status: string; + type?: string; +} + +export interface PublicVcdaCurrentState { + organizationId: string; + migrationCloudUrl?: string; + ips?: string[]; + azName?: string; + id?: string; +} + +export interface PublicVcdaTargetSpec { + ips: string[]; +} + +export interface PublicVcda { + id: string; + resourceStatus: VcdaResourceStatus; + currentState: PublicVcdaCurrentState; + currentTasks?: PublicCurrentTask[] | null; + targetSpec: PublicVcdaTargetSpec; + createdAt: string; + updatedAt: string; +} + +export interface VcdaWhitelistEntry { + ip: string; +} + +export type VcdaTileStatus = + | { kind: 'inactive' } + | { kind: 'provisioning' } + | { kind: 'deleting' } + | { kind: 'active'; resourceStatus: VcdaResourceStatus }; + +export const matchesVcdaOrg = ( + organizationId: string | undefined, + routeOrgId: string, +): boolean => + !!organizationId && + (organizationId === routeOrgId || routeOrgId.endsWith(`-${organizationId}`)); From f8ad5c035b34999012e88e91d97b8d0075dbfb0e Mon Sep 17 00:00:00 2001 From: Thibault Barske Date: Thu, 25 Jun 2026 16:33:57 +0200 Subject: [PATCH 2/4] feat(migration-tab): add endpoint & authorized-IPs management tab ref: #BKP-1174 Signed-off-by: Thibault Barske --- .../migration/Messages_de_DE.json | 28 ++++ .../migration/Messages_en_GB.json | 28 ++++ .../migration/Messages_es_ES.json | 28 ++++ .../migration/Messages_fr_CA.json | 28 ++++ .../migration/Messages_fr_FR.json | 28 ++++ .../migration/Messages_it_IT.json | 28 ++++ .../migration/Messages_pl_PL.json | 28 ++++ .../migration/Messages_pt_PT.json | 28 ++++ .../src/components/Fields/index.tsx | 9 +- .../layout/VcdDashboardLayout.component.tsx | 1 + .../hooks/vcda/useUpdateVcdaWhitelist.hook.ts | 38 ++++++ .../vcda/useUpdateVcdaWhitelist.spec.tsx | 73 +++++++++++ .../data/hooks/vcda/useVcdaMigration.hook.ts | 34 +++++ .../src/data/hooks/vcda/vcdaQueryKey.ts | 5 + .../src/mocks/vcda/index.ts | 2 + .../src/mocks/vcda/vcda.handler.ts | 37 ++++++ .../src/mocks/vcda/vcda.mock.ts | 17 +++ .../OrganizationDashboard.page.tsx | 19 +++ .../migration/Migration.context.tsx | 88 +++++++++++++ .../organization/migration/Migration.page.tsx | 10 ++ .../organization/migration/Migration.spec.tsx | 82 ++++++++++++ .../_components/EndpointSection.component.tsx | 43 ++++++ .../MigrationContent.component.tsx | 35 +++++ .../WhitelistSection.component.tsx | 124 ++++++++++++++++++ .../migration/add-ip/AddIp.page.tsx | 115 ++++++++++++++++ .../migration/add-ip/AddIp.spec.tsx | 26 ++++ .../migration/delete-ip/DeleteIp.page.tsx | 74 +++++++++++ .../migration/delete-ip/DeleteIp.spec.tsx | 26 ++++ .../src/routes/routes.constant.ts | 6 + .../src/routes/routes.tsx | 43 ++++++ .../src/schemas/cidr.schema.spec.ts | 70 ++++++++++ .../src/schemas/cidr.schema.ts | 38 ++++++ .../src/test-utils/renderTest.tsx | 3 + .../src/test-utils/test-i18n.ts | 3 + .../src/utils/iam.constants.ts | 2 + .../src/utils/testIds.constants.ts | 7 + 36 files changed, 1253 insertions(+), 1 deletion(-) create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_de_DE.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_en_GB.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_es_ES.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_CA.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_FR.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_it_IT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pl_PL.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pt_PT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.hook.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaMigration.hook.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/vcdaQueryKey.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/index.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.handler.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.mock.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.context.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/EndpointSection.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/MigrationContent.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/WhitelistSection.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/schemas/cidr.schema.spec.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/schemas/cidr.schema.ts diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_de_DE.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_de_DE.json new file mode 100644 index 000000000000..ded609a5882e --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_de_DE.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migration", + "migration.endpoint.section.title": "VCDA-IP für Public VCFaaS", + "migration.endpoint.description": "Die öffentliche IP-Adresse, die Ihr Quell-VCD erreichen muss, um Workloads zu migrieren.", + "migration.endpoint.error.fetch": "Beim Laden der VCDA-IP ist ein Fehler aufgetreten.", + "migration.section.title": "Autorisierte IP-Adressen", + "migration.cta.add": "+ Konfiguration hinzufügen", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "IP {{cidr}} aus den autorisierten IP-Adressen entfernen", + "migration.empty.message": "Es ist noch keine IP-Adresse autorisiert. Fügen Sie eine Konfiguration hinzu, um eine IP für den Zugriff auf diesen VCDA-Dienst zu autorisieren.", + "migration.error.fetch": "Beim Laden der autorisierten IP-Adressen ist ein Fehler aufgetreten.", + "migration.error.retry": "Erneut versuchen", + "migration.addModal.title": "Konfiguration hinzufügen", + "migration.addModal.field.cidr.label": "Öffentliche IP (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Diese IP-Adresse ist bereits autorisiert.", + "migration.addModal.error.generic": "Beim Hinzufügen der IP ist ein Fehler aufgetreten.", + "migration.addModal.button.add": "Hinzufügen", + "migration.addModal.toast.success": "IP-Adresse autorisiert.", + "migration.deleteModal.title": "Autorisierte IP-Adresse entfernen?", + "migration.deleteModal.body": "Die IP {{cidr}} ist nicht mehr berechtigt, den VCDA-Dienst zu erreichen.", + "migration.deleteModal.button.confirm": "Entfernen", + "migration.deleteModal.error.generic": "Beim Entfernen der IP ist ein Fehler aufgetreten.", + "migration.deleteModal.toast.success": "IP-Adresse entfernt.", + "cidr.error.format": "Geben Sie ein gültiges CIDR ein (z. B. 192.168.1.10/32).", + "cidr.error.octetRange": "Jedes Oktett muss zwischen 0 und 255 liegen.", + "cidr.error.maskRange": "Die Maske muss /32 sein (ein einzelner Host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_en_GB.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_en_GB.json new file mode 100644 index 000000000000..b0d47dcaa896 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_en_GB.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migration", + "migration.endpoint.section.title": "VCDA IP for Public VCFaaS", + "migration.endpoint.description": "The public IP your source VCD must reach to migrate workloads.", + "migration.endpoint.error.fetch": "An error occurred while loading the VCDA IP.", + "migration.section.title": "Authorized IP addresses", + "migration.cta.add": "+ Add configuration", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Remove IP {{cidr}} from the authorized IP addresses", + "migration.empty.message": "No IP address is authorized yet. Add a configuration to authorize an IP to reach this VCDA service.", + "migration.error.fetch": "An error occurred while loading the authorized IP addresses.", + "migration.error.retry": "Retry", + "migration.addModal.title": "Add configuration", + "migration.addModal.field.cidr.label": "Public IP (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "This IP address is already authorized.", + "migration.addModal.error.generic": "An error occurred while adding the IP.", + "migration.addModal.button.add": "Add", + "migration.addModal.toast.success": "IP address authorized.", + "migration.deleteModal.title": "Remove authorized IP address?", + "migration.deleteModal.body": "The IP {{cidr}} will no longer be authorized to reach the VCDA service.", + "migration.deleteModal.button.confirm": "Remove", + "migration.deleteModal.error.generic": "An error occurred while removing the IP.", + "migration.deleteModal.toast.success": "IP address removed.", + "cidr.error.format": "Enter a valid CIDR (e.g. 192.168.1.10/32).", + "cidr.error.octetRange": "Each octet must be between 0 and 255.", + "cidr.error.maskRange": "The mask must be /32 (a single host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_es_ES.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_es_ES.json new file mode 100644 index 000000000000..1c5c384ea13c --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_es_ES.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migración", + "migration.endpoint.section.title": "IP VCDA para Public VCFaaS", + "migration.endpoint.description": "La IP pública que su VCD de origen debe alcanzar para migrar las cargas de trabajo.", + "migration.endpoint.error.fetch": "Se ha producido un error al cargar la IP VCDA.", + "migration.section.title": "Direcciones IP autorizadas", + "migration.cta.add": "+ Añadir configuración", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Eliminar la IP {{cidr}} de las direcciones IP autorizadas", + "migration.empty.message": "Todavía no hay ninguna dirección IP autorizada. Añada una configuración para autorizar una IP a alcanzar este servicio VCDA.", + "migration.error.fetch": "Se ha producido un error al cargar las direcciones IP autorizadas.", + "migration.error.retry": "Reintentar", + "migration.addModal.title": "Añadir configuración", + "migration.addModal.field.cidr.label": "IP pública (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Esta dirección IP ya está autorizada.", + "migration.addModal.error.generic": "Se ha producido un error al añadir la IP.", + "migration.addModal.button.add": "Añadir", + "migration.addModal.toast.success": "Dirección IP autorizada.", + "migration.deleteModal.title": "¿Eliminar la dirección IP autorizada?", + "migration.deleteModal.body": "La IP {{cidr}} ya no estará autorizada a alcanzar el servicio VCDA.", + "migration.deleteModal.button.confirm": "Eliminar", + "migration.deleteModal.error.generic": "Se ha producido un error al eliminar la IP.", + "migration.deleteModal.toast.success": "Dirección IP eliminada.", + "cidr.error.format": "Introduzca un CIDR válido (por ejemplo, 192.168.1.10/32).", + "cidr.error.octetRange": "Cada octeto debe estar entre 0 y 255.", + "cidr.error.maskRange": "La máscara debe ser /32 (un único host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_CA.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_CA.json new file mode 100644 index 000000000000..0e6808b8aa25 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_CA.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migration", + "migration.endpoint.section.title": "IP VCDA pour Public VCFaaS", + "migration.endpoint.description": "L'adresse IP publique que votre VCD source doit joindre pour migrer vos charges de travail.", + "migration.endpoint.error.fetch": "Une erreur est survenue lors du chargement de l'IP VCDA.", + "migration.section.title": "Adresses IP autorisées", + "migration.cta.add": "+ Ajouter une configuration", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Retirer l'IP {{cidr}} des adresses IP autorisées", + "migration.empty.message": "Aucune adresse IP n'est encore autorisée. Ajoutez une configuration pour autoriser une IP à joindre ce service VCDA.", + "migration.error.fetch": "Une erreur est survenue lors du chargement des adresses IP autorisées.", + "migration.error.retry": "Réessayer", + "migration.addModal.title": "Ajouter une configuration", + "migration.addModal.field.cidr.label": "IP publique (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Cette adresse IP est déjà autorisée.", + "migration.addModal.error.generic": "Une erreur est survenue lors de l'ajout de l'IP.", + "migration.addModal.button.add": "Ajouter", + "migration.addModal.toast.success": "Adresse IP autorisée.", + "migration.deleteModal.title": "Retirer l'adresse IP autorisée ?", + "migration.deleteModal.body": "L'IP {{cidr}} ne sera plus autorisée à joindre le service VCDA.", + "migration.deleteModal.button.confirm": "Retirer", + "migration.deleteModal.error.generic": "Une erreur est survenue lors du retrait de l'IP.", + "migration.deleteModal.toast.success": "Adresse IP retirée.", + "cidr.error.format": "Saisissez un CIDR valide (par exemple 192.168.1.10/32).", + "cidr.error.octetRange": "Chaque octet doit être compris entre 0 et 255.", + "cidr.error.maskRange": "Le masque doit être /32 (un hôte unique)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_FR.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_FR.json new file mode 100644 index 000000000000..0e6808b8aa25 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_fr_FR.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migration", + "migration.endpoint.section.title": "IP VCDA pour Public VCFaaS", + "migration.endpoint.description": "L'adresse IP publique que votre VCD source doit joindre pour migrer vos charges de travail.", + "migration.endpoint.error.fetch": "Une erreur est survenue lors du chargement de l'IP VCDA.", + "migration.section.title": "Adresses IP autorisées", + "migration.cta.add": "+ Ajouter une configuration", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Retirer l'IP {{cidr}} des adresses IP autorisées", + "migration.empty.message": "Aucune adresse IP n'est encore autorisée. Ajoutez une configuration pour autoriser une IP à joindre ce service VCDA.", + "migration.error.fetch": "Une erreur est survenue lors du chargement des adresses IP autorisées.", + "migration.error.retry": "Réessayer", + "migration.addModal.title": "Ajouter une configuration", + "migration.addModal.field.cidr.label": "IP publique (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Cette adresse IP est déjà autorisée.", + "migration.addModal.error.generic": "Une erreur est survenue lors de l'ajout de l'IP.", + "migration.addModal.button.add": "Ajouter", + "migration.addModal.toast.success": "Adresse IP autorisée.", + "migration.deleteModal.title": "Retirer l'adresse IP autorisée ?", + "migration.deleteModal.body": "L'IP {{cidr}} ne sera plus autorisée à joindre le service VCDA.", + "migration.deleteModal.button.confirm": "Retirer", + "migration.deleteModal.error.generic": "Une erreur est survenue lors du retrait de l'IP.", + "migration.deleteModal.toast.success": "Adresse IP retirée.", + "cidr.error.format": "Saisissez un CIDR valide (par exemple 192.168.1.10/32).", + "cidr.error.octetRange": "Chaque octet doit être compris entre 0 et 255.", + "cidr.error.maskRange": "Le masque doit être /32 (un hôte unique)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_it_IT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_it_IT.json new file mode 100644 index 000000000000..d5d53758455d --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_it_IT.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migrazione", + "migration.endpoint.section.title": "IP VCDA per Public VCFaaS", + "migration.endpoint.description": "L'indirizzo IP pubblico che il tuo VCD di origine deve raggiungere per migrare i carichi di lavoro.", + "migration.endpoint.error.fetch": "Si è verificato un errore durante il caricamento dell'IP VCDA.", + "migration.section.title": "Indirizzi IP autorizzati", + "migration.cta.add": "+ Aggiungi configurazione", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Rimuovi l'IP {{cidr}} dagli indirizzi IP autorizzati", + "migration.empty.message": "Nessun indirizzo IP è ancora autorizzato. Aggiungi una configurazione per autorizzare un IP a raggiungere questo servizio VCDA.", + "migration.error.fetch": "Si è verificato un errore durante il caricamento degli indirizzi IP autorizzati.", + "migration.error.retry": "Riprova", + "migration.addModal.title": "Aggiungi configurazione", + "migration.addModal.field.cidr.label": "IP pubblico (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Questo indirizzo IP è già autorizzato.", + "migration.addModal.error.generic": "Si è verificato un errore durante l'aggiunta dell'IP.", + "migration.addModal.button.add": "Aggiungi", + "migration.addModal.toast.success": "Indirizzo IP autorizzato.", + "migration.deleteModal.title": "Rimuovere l'indirizzo IP autorizzato?", + "migration.deleteModal.body": "L'IP {{cidr}} non sarà più autorizzato a raggiungere il servizio VCDA.", + "migration.deleteModal.button.confirm": "Rimuovi", + "migration.deleteModal.error.generic": "Si è verificato un errore durante la rimozione dell'IP.", + "migration.deleteModal.toast.success": "Indirizzo IP rimosso.", + "cidr.error.format": "Inserisci un CIDR valido (ad esempio 192.168.1.10/32).", + "cidr.error.octetRange": "Ogni ottetto deve essere compreso tra 0 e 255.", + "cidr.error.maskRange": "La maschera deve essere /32 (un singolo host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pl_PL.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pl_PL.json new file mode 100644 index 000000000000..d2497d6ed2a2 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pl_PL.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migracja", + "migration.endpoint.section.title": "IP VCDA dla Public VCFaaS", + "migration.endpoint.description": "Publiczny adres IP, z którym musi się połączyć źródłowy VCD, aby migrować obciążenia.", + "migration.endpoint.error.fetch": "Wystąpił błąd podczas ładowania adresu IP VCDA.", + "migration.section.title": "Autoryzowane adresy IP", + "migration.cta.add": "+ Dodaj konfigurację", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Usuń adres IP {{cidr}} z autoryzowanych adresów IP", + "migration.empty.message": "Żaden adres IP nie jest jeszcze autoryzowany. Dodaj konfigurację, aby autoryzować adres IP do połączenia z tą usługą VCDA.", + "migration.error.fetch": "Wystąpił błąd podczas ładowania autoryzowanych adresów IP.", + "migration.error.retry": "Ponów próbę", + "migration.addModal.title": "Dodaj konfigurację", + "migration.addModal.field.cidr.label": "Publiczny adres IP (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Ten adres IP jest już autoryzowany.", + "migration.addModal.error.generic": "Wystąpił błąd podczas dodawania adresu IP.", + "migration.addModal.button.add": "Dodaj", + "migration.addModal.toast.success": "Adres IP autoryzowany.", + "migration.deleteModal.title": "Usunąć autoryzowany adres IP?", + "migration.deleteModal.body": "Adres IP {{cidr}} nie będzie już autoryzowany do połączenia z usługą VCDA.", + "migration.deleteModal.button.confirm": "Usuń", + "migration.deleteModal.error.generic": "Wystąpił błąd podczas usuwania adresu IP.", + "migration.deleteModal.toast.success": "Adres IP usunięty.", + "cidr.error.format": "Wprowadź prawidłowy CIDR (np. 192.168.1.10/32).", + "cidr.error.octetRange": "Każdy oktet musi mieć wartość od 0 do 255.", + "cidr.error.maskRange": "Maska musi wynosić /32 (pojedynczy host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pt_PT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pt_PT.json new file mode 100644 index 000000000000..f55b75e35686 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/Messages_pt_PT.json @@ -0,0 +1,28 @@ +{ + "migration.tab.title": "Migração", + "migration.endpoint.section.title": "IP VCDA para Public VCFaaS", + "migration.endpoint.description": "O endereço IP público que o seu VCD de origem deve alcançar para migrar as cargas de trabalho.", + "migration.endpoint.error.fetch": "Ocorreu um erro ao carregar o IP VCDA.", + "migration.section.title": "Endereços IP autorizados", + "migration.cta.add": "+ Adicionar configuração", + "migration.column.ip": "IP", + "migration.column.action.delete.ariaLabel": "Remover o IP {{cidr}} dos endereços IP autorizados", + "migration.empty.message": "Ainda não há nenhum endereço IP autorizado. Adicione uma configuração para autorizar um IP a alcançar este serviço VCDA.", + "migration.error.fetch": "Ocorreu um erro ao carregar os endereços IP autorizados.", + "migration.error.retry": "Tentar novamente", + "migration.addModal.title": "Adicionar configuração", + "migration.addModal.field.cidr.label": "IP público (CIDR)", + "migration.addModal.field.cidr.placeholder": "xxx.xxx.xxx.xxx/32", + "migration.addModal.error.duplicate": "Este endereço IP já está autorizado.", + "migration.addModal.error.generic": "Ocorreu um erro ao adicionar o IP.", + "migration.addModal.button.add": "Adicionar", + "migration.addModal.toast.success": "Endereço IP autorizado.", + "migration.deleteModal.title": "Remover o endereço IP autorizado?", + "migration.deleteModal.body": "O IP {{cidr}} deixará de estar autorizado a alcançar o serviço VCDA.", + "migration.deleteModal.button.confirm": "Remover", + "migration.deleteModal.error.generic": "Ocorreu um erro ao remover o IP.", + "migration.deleteModal.toast.success": "Endereço IP removido.", + "cidr.error.format": "Introduza um CIDR válido (por exemplo, 192.168.1.10/32).", + "cidr.error.octetRange": "Cada octeto deve estar entre 0 e 255.", + "cidr.error.maskRange": "A máscara deve ser /32 (um único host)." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/Fields/index.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/Fields/index.tsx index 8dd869006aa6..d949d2e78f2d 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/Fields/index.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/Fields/index.tsx @@ -10,9 +10,12 @@ import { RhfFieldContext, useRhfFieldContext } from './RhfField.context'; import { RhfQuantity } from './RhfQuantity.component'; import { RhfInput } from './RhfInput.component'; +type TranslatorKey = string; + type RhfFieldProps = React.ComponentProps & { controllerParams: UseControllerProps; helperMessage?: string; + errorMessage?: TranslatorKey; isHiddenError?: boolean; control?: Control; }; @@ -21,6 +24,7 @@ export const RhfField = ({ controllerParams, className, helperMessage, + errorMessage: errorMessageOverride, isHiddenError, control, ...rest @@ -38,7 +42,10 @@ export const RhfField = ({ ); const hasError = !isHiddenError && !!controller.fieldState?.error; - const errorMessage = helperMessage || controller.fieldState?.error?.message; + const errorMessage = + errorMessageOverride || + helperMessage || + controller.fieldState?.error?.message; return ( diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/dashboard/layout/VcdDashboardLayout.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/dashboard/layout/VcdDashboardLayout.component.tsx index 433750d8cb55..781ec3d2305d 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/dashboard/layout/VcdDashboardLayout.component.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/dashboard/layout/VcdDashboardLayout.component.tsx @@ -12,6 +12,7 @@ export type DashboardTab = { title: string; to: string; trackingActions?: string[]; + disabled?: boolean; }; export type TDashboardLayoutProps = { diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.hook.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.hook.ts new file mode 100644 index 000000000000..e0bab7c24f89 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.hook.ts @@ -0,0 +1,38 @@ +import { ApiError } from '@ovh-ux/manager-core-api'; +import { + useMutation, + UseMutationOptions, + useQueryClient, +} from '@tanstack/react-query'; +import { updateVcdaMigrationWhitelist } from '@ovh-ux/manager-module-vcd-api'; +import { getVcdaMigrationQueryKey } from './vcdaQueryKey'; +import { getVcdaStatusQueryKey } from './useVcdaStatus.hook'; + +export const useUpdateVcdaWhitelist = ({ + orgId, + migrationId, + onSuccess, + ...options +}: { + orgId: string; + migrationId: string; +} & Partial>) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (ips: string[]) => + updateVcdaMigrationWhitelist(migrationId, { ips }), + onSuccess: (...params) => { + // Both VCDA caches must refresh: the migration page (useVcdaMigration) and + // the tile/tab gate (useVcdaStatus) — a PUT moves the entity to UPDATING. + queryClient.invalidateQueries({ + queryKey: getVcdaMigrationQueryKey(orgId), + }); + queryClient.invalidateQueries({ + queryKey: getVcdaStatusQueryKey(orgId), + }); + onSuccess?.(...params); + }, + ...options, + }); +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.spec.tsx new file mode 100644 index 000000000000..9eb3b5392690 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useUpdateVcdaWhitelist.spec.tsx @@ -0,0 +1,73 @@ +import { ReactNode } from 'react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useUpdateVcdaWhitelist } from './useUpdateVcdaWhitelist.hook'; +import { getVcdaMigrationQueryKey } from './vcdaQueryKey'; +import * as vcdaApi from '@ovh-ux/manager-module-vcd-api'; + +const ORG_ID = 'org-1'; +const MIGRATION_ID = 'migration-1'; + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + return { wrapper, queryClient }; +}; + +describe('useUpdateVcdaWhitelist', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('PUTs the full ips array and invalidates the migration query (R11)', async () => { + const putSpy = vi + .spyOn(vcdaApi, 'updateVcdaMigrationWhitelist') + .mockResolvedValue({ data: {} } as never); + const { wrapper, queryClient } = createWrapper(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + const { result } = renderHook( + () => + useUpdateVcdaWhitelist({ orgId: ORG_ID, migrationId: MIGRATION_ID }), + { wrapper }, + ); + + result.current.mutate(['192.168.1.10', '10.0.0.5']); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(putSpy).toHaveBeenCalledWith(MIGRATION_ID, { + ips: ['192.168.1.10', '10.0.0.5'], + }); + expect(invalidateSpy).toHaveBeenCalledWith({ + queryKey: getVcdaMigrationQueryKey(ORG_ID), + }); + }); + + it('forwards a provided onSuccess callback', async () => { + vi.spyOn(vcdaApi, 'updateVcdaMigrationWhitelist').mockResolvedValue({ + data: {}, + } as never); + const onSuccess = vi.fn(); + const { wrapper } = createWrapper(); + + const { result } = renderHook( + () => + useUpdateVcdaWhitelist({ + orgId: ORG_ID, + migrationId: MIGRATION_ID, + onSuccess, + }), + { wrapper }, + ); + + result.current.mutate(['10.0.0.5']); + + await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaMigration.hook.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaMigration.hook.ts new file mode 100644 index 000000000000..feed8b44dc15 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/useVcdaMigration.hook.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { + getVcdaMigration, + getVcdaMigrationList, + matchesVcdaOrg, + PublicVcda, +} from '@ovh-ux/manager-module-vcd-api'; +import { getVcdaMigrationQueryKey } from './vcdaQueryKey'; + +const POLLING_STATUSES = ['CREATING', 'UPDATING', 'DELETING']; +const POLLING_INTERVAL = 30_000; + +const resolveMigration = async (orgId: string): Promise => { + const list = await getVcdaMigrationList(); + const match = list.find((migration) => + matchesVcdaOrg(migration.currentState?.organizationId, orgId), + ); + if (!match) { + throw new Error(`No VCDA migration found for organization ${orgId}`); + } + return getVcdaMigration(match.id); +}; + +export const useVcdaMigration = (orgId: string) => + useQuery({ + queryKey: getVcdaMigrationQueryKey(orgId), + queryFn: () => resolveMigration(orgId), + enabled: !!orgId, + refetchInterval: (query) => + query.state.data && + POLLING_STATUSES.includes(query.state.data.resourceStatus) + ? POLLING_INTERVAL + : false, + }); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/vcdaQueryKey.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/vcdaQueryKey.ts new file mode 100644 index 000000000000..ec85ac51c7a6 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcda/vcdaQueryKey.ts @@ -0,0 +1,5 @@ +export const getVcdaMigrationQueryKey = (orgId: string) => [ + 'vcda', + 'migration', + orgId, +]; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/index.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/index.ts new file mode 100644 index 000000000000..a1e7f81b40a3 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/index.ts @@ -0,0 +1,2 @@ +export * from './vcda.handler'; +export * from './vcda.mock'; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.handler.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.handler.ts new file mode 100644 index 000000000000..bd2d38e97d69 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.handler.ts @@ -0,0 +1,37 @@ +import { Handler } from '@ovh-ux/manager-core-test-utils'; +import { PublicVcda } from '@ovh-ux/manager-module-vcd-api'; +import { vcdaMigrationMock } from './vcda.mock'; + +export type TVcdaMockParams = { + vcdaMigration?: PublicVcda | null; + isVcdaMigrationKo?: boolean; + isVcdaUpdateKo?: boolean; +}; + +export const getVcdaMocks = ({ + vcdaMigration = vcdaMigrationMock, + isVcdaMigrationKo, + isVcdaUpdateKo, +}: TVcdaMockParams): Handler[] => [ + { + url: '/vmwareCloudDirector/migration', + response: () => (vcdaMigration ? [vcdaMigration] : []), + api: 'v2', + method: 'get', + status: isVcdaMigrationKo ? 500 : 200, + }, + { + url: '/vmwareCloudDirector/migration/:migrationId', + response: () => vcdaMigration ?? {}, + api: 'v2', + method: 'get', + status: isVcdaMigrationKo ? 500 : 200, + }, + { + url: '/vmwareCloudDirector/migration/:migrationId', + response: () => vcdaMigration ?? {}, + api: 'v2', + method: 'put', + status: isVcdaUpdateKo ? 500 : 200, + }, +]; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.mock.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.mock.ts new file mode 100644 index 000000000000..135528c14578 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/mocks/vcda/vcda.mock.ts @@ -0,0 +1,17 @@ +import { organizationList, PublicVcda } from '@ovh-ux/manager-module-vcd-api'; + +export const DEFAULT_ORGANIZATION_ID = organizationList[0].id; + +export const vcdaMigrationMock: PublicVcda = { + id: 'migration-1', + resourceStatus: 'READY', + currentState: { + organizationId: DEFAULT_ORGANIZATION_ID, + migrationCloudUrl: 'https://migration.eu-west-par.vcda.ovh.net', + ips: ['192.168.1.10', '10.0.0.5'], + }, + currentTasks: [], + targetSpec: { ips: ['192.168.1.10', '10.0.0.5'] }, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/OrganizationDashboard.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/OrganizationDashboard.page.tsx index 067dd7507897..29d7188a6349 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/OrganizationDashboard.page.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/OrganizationDashboard.page.tsx @@ -5,7 +5,10 @@ import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { ChangelogButton, HeadersProps, + useFeatureAvailability, } from '@ovh-ux/manager-react-components'; +import { useVcdaStatus } from '@/data/hooks/vcda/useVcdaStatus.hook'; +import { FEATURES } from '@/utils/features.constants'; import VcdDashboardLayout, { DashboardTab, } from '@/components/dashboard/layout/VcdDashboardLayout.component'; @@ -22,8 +25,15 @@ export default function DashboardPage() { const { t } = useTranslation('dashboard'); const { t: tDashboard } = useTranslation(NAMESPACES.DASHBOARD); const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const { t: tMigration } = useTranslation('migration'); const { data: vcdOrganisation } = useVcdOrganization({ id }); const navigate = useNavigate(); + const { data: features } = useFeatureAvailability([FEATURES.HPC_VCFAAS_VCDA]); + const isVcdaEnabled = !!features?.[FEATURES.HPC_VCFAAS_VCDA]; + const { data: vcdaStatus } = useVcdaStatus(id, isVcdaEnabled); + const isMigrationTabMounted = isVcdaEnabled && vcdaStatus?.kind === 'active'; + + const migrationPath = useResolvedPath(subRoutes.migration).pathname; const tabsList: DashboardTab[] = [ { @@ -43,6 +53,15 @@ export default function DashboardPage() { title: t('managed_vcd_dashboard_general_network_acl'), to: useResolvedPath(subRoutes.networkAcl).pathname, }, + ...(isMigrationTabMounted + ? [ + { + name: 'migration', + title: tMigration('migration.tab.title'), + to: migrationPath, + }, + ] + : []), ]; const serviceName = vcdOrganisation?.data?.currentState?.fullName; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.context.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.context.tsx new file mode 100644 index 000000000000..ab3b69ed9119 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.context.tsx @@ -0,0 +1,88 @@ +import { createContext, ReactNode, useContext, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { ApiError } from '@ovh-ux/manager-core-api'; +import { + useVcdOrganization, + PublicVcda, + VcdaResourceStatus, + VcdaWhitelistEntry, +} from '@ovh-ux/manager-module-vcd-api'; +import { useVcdaMigration } from '@/data/hooks/vcda/useVcdaMigration.hook'; + +interface MigrationContextValue { + orgId: string; + migration: PublicVcda | undefined; + migrationId: string; + endpointUrl: string | undefined; + whitelist: VcdaWhitelistEntry[]; + resourceStatus: VcdaResourceStatus | undefined; + isEditable: boolean; + organisationUrn: string | undefined; + isPending: boolean; + isError: boolean; + error: ApiError | null; + refetch: () => void; +} + +const MigrationContext = createContext(null); + +export function MigrationProvider({ children }: { children: ReactNode }) { + const { id } = useParams(); + const orgId = id ?? ''; + const { data: organisation } = useVcdOrganization({ id: orgId }); + const { + data: migration, + isPending, + isError, + error, + refetch, + } = useVcdaMigration(orgId); + + const whitelist = useMemo( + () => (migration?.currentState?.ips ?? []).map((ip) => ({ ip })), + [migration], + ); + + const value = useMemo( + () => ({ + orgId, + migration, + migrationId: migration?.id ?? '', + endpointUrl: migration?.currentState?.migrationCloudUrl, + whitelist, + resourceStatus: migration?.resourceStatus, + isEditable: migration?.resourceStatus === 'READY', + organisationUrn: organisation?.data?.iam?.urn, + isPending, + isError, + error: (error as ApiError) ?? null, + refetch, + }), + [ + orgId, + migration, + whitelist, + organisation, + isPending, + isError, + error, + refetch, + ], + ); + + return ( + + {children} + + ); +} + +export function useMigrationContext() { + const context = useContext(MigrationContext); + if (!context) { + throw new Error( + 'useMigrationContext must be used within MigrationProvider', + ); + } + return context; +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.page.tsx new file mode 100644 index 000000000000..6ad641f7ab01 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.page.tsx @@ -0,0 +1,10 @@ +import VcdaFeatureGuard from '@/components/vcda/VcdaFeatureGuard.component'; +import MigrationContent from './_components/MigrationContent.component'; + +export default function MigrationPage() { + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.spec.tsx new file mode 100644 index 000000000000..57ff5b9e8964 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/Migration.spec.tsx @@ -0,0 +1,82 @@ +import { waitFor } from '@testing-library/react'; +import { + assertElementVisibility, + assertTextVisibility, + getElementByTestId, +} from '@ovh-ux/manager-core-test-utils'; +import { labels, renderTest } from '@/test-utils'; +import TEST_IDS from '@/utils/testIds.constants'; +import { FEATURES } from '@/utils/features.constants'; +import { DEFAULT_ORGANIZATION_ID, vcdaMigrationMock } from '@/mocks/vcda'; + +const migrationRoute = `/${DEFAULT_ORGANIZATION_ID}/migration`; +const flagOn = { [FEATURES.HPC_VCFAAS_VCDA]: true }; + +describe('Migration tab', () => { + it('renders both section headings and the endpoint URL on success', async () => { + await renderTest({ initialRoute: migrationRoute, feature: flagOn }); + + await waitFor( + async () => { + await assertTextVisibility( + labels.migration['migration.endpoint.section.title'], + ); + await assertTextVisibility(labels.migration['migration.section.title']); + }, + { timeout: 10000 }, + ); + }); + + it('renders the whitelisted IPs in /32 notation', async () => { + await renderTest({ initialRoute: migrationRoute, feature: flagOn }); + + await waitFor(() => assertTextVisibility('192.168.1.10/32'), { + timeout: 10000, + }); + }); + + it('enables the add CTA when the resource is READY', async () => { + await renderTest({ initialRoute: migrationRoute, feature: flagOn }); + + const addCta = await getElementByTestId(TEST_IDS.migrationAddCta); + await assertElementVisibility(addCta); + }); + + it('disables edit affordances when the resource is not READY (R10/R17)', async () => { + await renderTest({ + initialRoute: migrationRoute, + feature: flagOn, + vcdaMigration: { ...vcdaMigrationMock, resourceStatus: 'SUSPENDED' }, + }); + + const addCta = await getElementByTestId(TEST_IDS.migrationAddCta); + await waitFor(() => expect(addCta).toBeDisabled(), { timeout: 10000 }); + }); + + it('shows the empty state when no IP is authorized (R13)', async () => { + await renderTest({ + initialRoute: migrationRoute, + feature: flagOn, + vcdaMigration: { + ...vcdaMigrationMock, + currentState: { ...vcdaMigrationMock.currentState, ips: [] }, + }, + }); + + await waitFor( + () => assertTextVisibility(labels.migration['migration.empty.message']), + { timeout: 10000 }, + ); + }); + + it('shows the error state with a retry control on fetch failure (R14)', async () => { + await renderTest({ + initialRoute: migrationRoute, + feature: flagOn, + isVcdaMigrationKo: true, + }); + + const retry = await getElementByTestId(TEST_IDS.migrationRetryCta); + await assertElementVisibility(retry); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/EndpointSection.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/EndpointSection.component.tsx new file mode 100644 index 000000000000..946c589f8969 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/EndpointSection.component.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { Clipboard } from '@ovh-ux/manager-react-components'; +import { + OdsMessage, + OdsSkeleton, + OdsText, +} from '@ovhcloud/ods-components/react'; +import { useMigrationContext } from '../Migration.context'; + +export default function EndpointSection() { + const { t } = useTranslation('migration'); + const { endpointUrl, isPending, isError } = useMigrationContext(); + + return ( +
+ + {t('migration.endpoint.section.title')} + +
+ {isPending && } + {!isPending && isError && ( + + {t('migration.endpoint.error.fetch')} + + )} + {!isPending && !isError && ( + + )} +
+ + {t('migration.endpoint.description')} + +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/MigrationContent.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/MigrationContent.component.tsx new file mode 100644 index 000000000000..750a9fc81d60 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/MigrationContent.component.tsx @@ -0,0 +1,35 @@ +import { Suspense } from 'react'; +import { Navigate, Outlet, useParams } from 'react-router-dom'; +import Loading from '@/components/loading/Loading.component'; +import { useVcdaStatus } from '@/data/hooks/vcda/useVcdaStatus.hook'; +import { urls } from '@/routes/routes.constant'; +import { MigrationProvider } from '../Migration.context'; +import EndpointSection from './EndpointSection.component'; +import WhitelistSection from './WhitelistSection.component'; + +export default function MigrationContent() { + const { id } = useParams(); + const { data: status, isPending, isError } = useVcdaStatus(id ?? ''); + + if (isPending) { + return ; + } + + // R4: the Migration tab exists only for a provisioned migration. A deep-link + // while the org is definitively inactive / provisioning / deleting redirects to + // the Dashboard. On a fetch error we do NOT redirect — the content below renders + // its own error + retry affordance (R14). + if (!isError && (!status || status.kind !== 'active')) { + return ; + } + + return ( + + + + + + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/WhitelistSection.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/WhitelistSection.component.tsx new file mode 100644 index 000000000000..3cedfe7c2cce --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/_components/WhitelistSection.component.tsx @@ -0,0 +1,124 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { + Datagrid, + DataGridTextCell, + ManagerButton, +} from '@ovh-ux/manager-react-components'; +import { OdsButton, OdsMessage, OdsText } from '@ovhcloud/ods-components/react'; +import { + ODS_BUTTON_COLOR, + ODS_BUTTON_VARIANT, + ODS_ICON_NAME, +} from '@ovhcloud/ods-components'; +import { subRoutes } from '@/routes/routes.constant'; +import TEST_IDS from '@/utils/testIds.constants'; +import { iamActions } from '@/utils/iam.constants'; +import { VcdaWhitelistEntry } from '@ovh-ux/manager-module-vcd-api'; +import { useMigrationContext } from '../Migration.context'; + +export default function WhitelistSection() { + const { t } = useTranslation('migration'); + const navigate = useNavigate(); + const { + whitelist, + isPending, + isError, + isEditable, + organisationUrn, + refetch, + } = useMigrationContext(); + + const columns = [ + { + id: 'ip', + label: t('migration.column.ip'), + cell: (row: VcdaWhitelistEntry) => ( + {`${row.ip}/32`} + ), + }, + { + id: 'actions', + label: '', + cell: (row: VcdaWhitelistEntry) => ( +
+ + navigate( + `${subRoutes.migrationDeleteIp}?ip=${encodeURIComponent( + row.ip, + )}`, + ) + } + /> +
+ ), + }, + ]; + + return ( +
+ + {t('migration.section.title')} + + +
+ navigate(subRoutes.migrationAddIp)} + /> +
+ + {isError ? ( +
+ + {t('migration.error.fetch')} + + refetch()} + /> +
+ ) : ( + <> + {!isPending && whitelist.length === 0 ? ( + + {t('migration.empty.message')} + + ) : ( + + )} + + )} +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.page.tsx new file mode 100644 index 000000000000..d61e3557a61d --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.page.tsx @@ -0,0 +1,115 @@ +import { Suspense } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { Modal } from '@ovh-ux/manager-react-components'; +import { + OdsFormField, + OdsInput, + OdsMessage, +} from '@ovhcloud/ods-components/react'; +import { useMessageContext } from '@/context/Message.context'; +import TEST_IDS from '@/utils/testIds.constants'; +import { + AddIpFormSchema, + AddIpFormValues, + isDuplicateHostIp, + toHostIp, +} from '@/schemas/cidr.schema'; +import { useUpdateVcdaWhitelist } from '@/data/hooks/vcda/useUpdateVcdaWhitelist.hook'; +import { useMigrationContext } from '../Migration.context'; + +export default function AddIpPage() { + const { t } = useTranslation('migration'); + const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const navigate = useNavigate(); + const closeModal = () => navigate('..'); + const { addSuccess } = useMessageContext(); + const { orgId, migrationId, whitelist } = useMigrationContext(); + + const { + control, + handleSubmit, + setError, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(AddIpFormSchema), + mode: 'onChange', + defaultValues: { cidr: '' }, + }); + + const { mutate: updateWhitelist, error, isPending } = useUpdateVcdaWhitelist({ + orgId, + migrationId, + onSuccess: () => { + addSuccess({ + content: t('migration.addModal.toast.success'), + includedSubRoutes: [orgId], + duration: 5000, + }); + closeModal(); + }, + }); + + const currentIps = whitelist.map((entry) => entry.ip); + + const onSubmit = handleSubmit(({ cidr }) => { + const hostIp = toHostIp(cidr); + if (isDuplicateHostIp(hostIp, currentIps)) { + setError('cidr', { message: 'migration.addModal.error.duplicate' }); + return; + } + updateWhitelist([...currentIps, hostIp]); + }); + + return ( + + +
+ {error && ( + + {t('migration.addModal.error.generic')} + + )} + ( + + + + + )} + /> +
+
+
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.spec.tsx new file mode 100644 index 000000000000..8eee64fbddac --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/add-ip/AddIp.spec.tsx @@ -0,0 +1,26 @@ +import { waitFor, screen } from '@testing-library/react'; +import { getElementByTestId } from '@ovh-ux/manager-core-test-utils'; +import { renderTest, labels } from '@/test-utils'; +import TEST_IDS from '@/utils/testIds.constants'; +import { FEATURES } from '@/utils/features.constants'; +import { DEFAULT_ORGANIZATION_ID } from '@/mocks/vcda'; + +const addRoute = `/${DEFAULT_ORGANIZATION_ID}/migration/add-ip`; +const flagOn = { [FEATURES.HPC_VCFAAS_VCDA]: true }; + +describe('Migration — Add IP modal', () => { + it('opens the add modal with a CIDR field and submit CTA (R6/R7)', async () => { + await renderTest({ initialRoute: addRoute, feature: flagOn }); + + await waitFor( + () => screen.getByText(labels.migration['migration.addModal.title']), + { timeout: 10000 }, + ); + + expect( + document.getElementById('migration-add-ip-input'), + ).toBeInTheDocument(); + const submit = await getElementByTestId(TEST_IDS.migrationAddSubmitCta); + expect(submit).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.page.tsx new file mode 100644 index 000000000000..cb4ad82d8144 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.page.tsx @@ -0,0 +1,74 @@ +import { Suspense } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components'; +import { Modal } from '@ovh-ux/manager-react-components'; +import { OdsMessage, OdsText } from '@ovhcloud/ods-components/react'; +import { useMessageContext } from '@/context/Message.context'; +import TEST_IDS from '@/utils/testIds.constants'; +import { useUpdateVcdaWhitelist } from '@/data/hooks/vcda/useUpdateVcdaWhitelist.hook'; +import { useMigrationContext } from '../Migration.context'; + +export default function DeleteIpPage() { + const { t } = useTranslation('migration'); + const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const navigate = useNavigate(); + const closeModal = () => navigate('..'); + const { addSuccess } = useMessageContext(); + const { orgId, migrationId, whitelist } = useMigrationContext(); + + const { search } = useLocation(); + const targetIp = new URLSearchParams(search).get('ip') ?? ''; + const cidr = `${targetIp}/32`; + + const { mutate: updateWhitelist, error, isPending } = useUpdateVcdaWhitelist({ + orgId, + migrationId, + onSuccess: () => { + addSuccess({ + content: t('migration.deleteModal.toast.success'), + includedSubRoutes: [orgId], + duration: 5000, + }); + closeModal(); + }, + }); + + const onConfirm = () => + updateWhitelist( + whitelist.map((entry) => entry.ip).filter((ip) => ip !== targetIp), + ); + + return ( + + +
+ {error && ( + + {t('migration.deleteModal.error.generic')} + + )} + + {t('migration.deleteModal.body', { cidr })} + + + {cidr} + +
+
+
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.spec.tsx new file mode 100644 index 000000000000..bad43fd31c84 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/delete-ip/DeleteIp.spec.tsx @@ -0,0 +1,26 @@ +import { waitFor, screen } from '@testing-library/react'; +import { getElementByTestId } from '@ovh-ux/manager-core-test-utils'; +import { renderTest } from '@/test-utils'; +import TEST_IDS from '@/utils/testIds.constants'; +import { FEATURES } from '@/utils/features.constants'; +import { DEFAULT_ORGANIZATION_ID } from '@/mocks/vcda'; + +const deleteRoute = `/${DEFAULT_ORGANIZATION_ID}/migration/delete-ip?ip=192.168.1.10`; +const flagOn = { [FEATURES.HPC_VCFAAS_VCDA]: true }; + +describe('Migration — Delete IP modal', () => { + it('opens the confirmation modal highlighting the targeted IP (R10/R11)', async () => { + await renderTest({ initialRoute: deleteRoute, feature: flagOn }); + + await waitFor( + () => + expect(screen.getAllByText('192.168.1.10/32').length).toBeGreaterThan( + 0, + ), + { timeout: 10000 }, + ); + + const confirm = await getElementByTestId(TEST_IDS.migrationDeleteSubmitCta); + expect(confirm).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts index 3a5c48135534..02e665993bbd 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts @@ -14,6 +14,9 @@ export const subRoutes = { datacentreComputeOrder: 'order-compute', datacentreComputeId: ':computeId', datacentreComputeDelete: 'delete-compute', + migration: 'migration', + migrationAddIp: 'add-ip', + migrationDeleteIp: 'delete-ip', resetPassword: 'reset-password', vrackSegments: 'vrack-segments', vrackSegmentId: ':vrackSegmentId', @@ -36,6 +39,9 @@ export const urls = { onboarding: `/${subRoutes.root}/${subRoutes.onboarding}`, dashboard: `/${subRoutes.root}/${subRoutes.dashboard}`, resetPassword: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.resetPassword}`, + migration: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}`, + migrationAddIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationAddIp}`, + migrationDeleteIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationDeleteIp}`, editName: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.editName}`, editDescription: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.editDescription}`, datacentres: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}`, diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx index 9ded291c7bb7..bf1188526645 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx @@ -35,9 +35,18 @@ const OrganizationResetPasswordPage = React.lazy(() => '@/pages/dashboard/organization/general-information/edit/EditPassword.page' ), ); +const MigrationPage = React.lazy(() => + import('@/pages/dashboard/organization/migration/Migration.page'), +); const DatacentresPage = React.lazy(() => import('@/pages/listing/datacentres/Datacentres.page'), ); +const MigrationAddIpPage = React.lazy(() => + import('@/pages/dashboard/organization/migration/add-ip/AddIp.page'), +); +const MigrationDeleteIpPage = React.lazy(() => + import('@/pages/dashboard/organization/migration/delete-ip/DeleteIp.page'), +); const NetworkAclPage = React.lazy(() => import('@/pages/listing/networkAcl/NetworkAcl.page'), @@ -230,6 +239,40 @@ export default ( }, }} /> + + + + { + it('accepts a valid /32 host CIDR', () => { + expect(Ipv4HostCidrSchema.safeParse('192.168.1.10/32').success).toBe(true); + }); + + it('rejects a malformed value', () => { + const result = Ipv4HostCidrSchema.safeParse('not-an-ip'); + expect(result.success).toBe(false); + expect(result.error?.issues[0].message).toBe('cidr.error.format'); + }); + + it('rejects an out-of-range octet', () => { + const result = Ipv4HostCidrSchema.safeParse('300.1.1.1/32'); + expect(result.success).toBe(false); + expect(result.error?.issues[0].message).toBe('cidr.error.octetRange'); + }); + + it('rejects a mask other than /32', () => { + const result = Ipv4HostCidrSchema.safeParse('10.0.0.0/24'); + expect(result.success).toBe(false); + expect(result.error?.issues[0].message).toBe('cidr.error.maskRange'); + }); + + it('trims surrounding whitespace before validating', () => { + expect(Ipv4HostCidrSchema.safeParse(' 10.0.0.5/32 ').success).toBe(true); + }); +}); + +describe('toHostIp', () => { + it('strips the /32 mask', () => { + expect(toHostIp('10.0.0.5/32')).toBe('10.0.0.5'); + }); + + it('trims whitespace', () => { + expect(toHostIp(' 10.0.0.5/32 ')).toBe('10.0.0.5'); + }); +}); + +describe('isDuplicateHostIp', () => { + it('detects an IP already present in the list (R8)', () => { + expect(isDuplicateHostIp('10.0.0.5', ['192.168.1.10', '10.0.0.5'])).toBe( + true, + ); + }); + + it('returns false for a new IP', () => { + expect(isDuplicateHostIp('10.0.0.6', ['192.168.1.10', '10.0.0.5'])).toBe( + false, + ); + }); +}); + +describe('AddIpFormSchema', () => { + it('validates the cidr field', () => { + expect(AddIpFormSchema.safeParse({ cidr: '192.168.1.10/32' }).success).toBe( + true, + ); + expect(AddIpFormSchema.safeParse({ cidr: '192.168.1.10/24' }).success).toBe( + false, + ); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/schemas/cidr.schema.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/schemas/cidr.schema.ts new file mode 100644 index 000000000000..21d473cff7df --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/schemas/cidr.schema.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; + +const ipv4Octet = /^(0|[1-9]\d?|1\d\d|2[0-4]\d|25[0-5])$/; + +export const Ipv4HostCidrSchema = z + .string() + .trim() + .superRefine((value, ctx) => { + const cidr = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec( + value, + ); + if (!cidr) { + ctx.addIssue({ code: 'custom', message: 'cidr.error.format' }); + return; + } + const [, a, b, c, d, mask] = cidr; + if (![a, b, c, d].every((octet) => ipv4Octet.test(octet))) { + ctx.addIssue({ code: 'custom', message: 'cidr.error.octetRange' }); + return; + } + // The whitelist accepts a single host only (D3/R2): the mask is locked to /32. + if (Number(mask) !== 32) { + ctx.addIssue({ code: 'custom', message: 'cidr.error.maskRange' }); + } + }); + +// The v2 field is `format: ipv4`, so the /32 mask is stripped before the PUT. +export const toHostIp = (cidr: string): string => + cidr.trim().replace(/\/32$/, ''); + +export const isDuplicateHostIp = (hostIp: string, ips: string[]): boolean => + ips.includes(hostIp); + +export const AddIpFormSchema = z.object({ + cidr: Ipv4HostCidrSchema, +}); + +export type AddIpFormValues = z.infer; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/renderTest.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/renderTest.tsx index 3fa4720ef9ea..d896489df1f5 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/renderTest.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/renderTest.tsx @@ -40,6 +40,7 @@ import { getFeatureAvailabilityMocks, TFeatureAvailabilityMockParams, } from '@/mocks/feature-availability'; +import { getVcdaMocks, TVcdaMockParams } from '@/mocks/vcda'; import { VMWARE_CLOUD_DIRECTOR_LABEL } from '@/utils/label.constants'; import { urls } from '@/routes/routes.constant'; @@ -59,6 +60,7 @@ export const renderTest = async ({ GetDatacentreOrderMocksParams & GetVeeamBackupMocksParams & TFeatureAvailabilityMockParams & + TVcdaMockParams & GetVrackSegmentsMocksParams & GetNetworkAclMocksParams & GetServicesMocksParams = {}) => { @@ -74,6 +76,7 @@ export const renderTest = async ({ ...getVrackSegmentsMocks(mockParams), ...getFeatureAvailabilityMocks(mockParams), ...getNetworkAclMock(mockParams), + ...getVcdaMocks(mockParams), ]), ); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts index 07699dc7f943..6d148f342aa4 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/test-utils/test-i18n.ts @@ -8,6 +8,7 @@ import datacentresStorage from '../../public/translations/datacentres/storage/Me import datacentresVrackSegment from '../../public/translations/datacentres/vrack-segment/Messages_fr_FR.json'; import terminate from '../../public/translations/terminate/Messages_fr_FR.json'; import networkAcl from '../../public/translations/networkAcl/Messages_fr_FR.json'; +import migration from '../../public/translations/migration/Messages_fr_FR.json'; import zodError from '../../public/translations/zodError/Messages_fr_FR.json'; import vcda from '../../public/translations/vcda/Messages_fr_FR.json'; @@ -66,6 +67,7 @@ export const translations = { error, terminate, networkAcl, + migration, '@ovh-ux/manager-common-translations/dashboard': commun.dashboard, '@ovh-ux/manager-common-translations/actions': commun.actions, '@ovh-ux/manager-common-translations/status': commun.status, @@ -88,6 +90,7 @@ export const labels = { datacentresVrackSegment, terminate, networkAcl, + migration, commun, zodError, vcda, diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/iam.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/iam.constants.ts index 11675016baf2..a6a6a9b15737 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/iam.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/iam.constants.ts @@ -3,4 +3,6 @@ export const iamActions = { 'vmwareCloudDirector:apiovh:organization/edit', vmwareCloudDirectorApiovhOrganizationVirtualDataCenterEdit: 'vmwareCloudDirector:apiovh:organization/virtualDataCenter/edit', + vmwareCloudDirectorApiovhMigrationEdit: + 'vmwareCloudDirector:apiovh:migration/edit', }; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts index 16829ac1bf61..8a811337bbd4 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/testIds.constants.ts @@ -20,12 +20,19 @@ const TEST_IDS = { networkAclCta: 'network-acl-cta', networkAclAddCurrentIpAction: 'add-current-ip-button', networkAclfromAnywhereIpAction: 'from-anywhere-ip-button', + migrationAddCta: 'migration-add-cta', + migrationCopyCta: 'migration-copy-cta', + migrationDeleteCta: 'migration-delete-cta', + migrationAddSubmitCta: 'migration-add-submit-cta', + migrationDeleteSubmitCta: 'migration-delete-submit-cta', + migrationRetryCta: 'migration-retry-cta', migrationTileStatusBadge: 'migration-tile-status-badge', migrationTileProvisioningBadge: 'migration-tile-provisioning-badge', migrationTileDeletingBadge: 'migration-tile-deleting-badge', migrationTileSkeleton: 'migration-tile-skeleton', migrationTileError: 'migration-tile-error', migrationTileRetryCta: 'migration-tile-retry-cta', + migrationTab: 'migration-tab', } as const; export default TEST_IDS; From c49ad486ae6566c4751ff750ad3451e3f8e0897c Mon Sep 17 00:00:00 2001 From: Thibault Barske Date: Thu, 25 Jun 2026 16:33:58 +0200 Subject: [PATCH 3/4] feat(order): add VCDA Public VCF migration order with contracts step ref: #BKP-1174 Signed-off-by: Thibault Barske --- .../migration/order/Messages_de_DE.json | 24 ++ .../migration/order/Messages_en_GB.json | 24 ++ .../migration/order/Messages_es_ES.json | 24 ++ .../migration/order/Messages_fr_CA.json | 24 ++ .../migration/order/Messages_fr_FR.json | 24 ++ .../migration/order/Messages_it_IT.json | 24 ++ .../migration/order/Messages_pl_PL.json | 24 ++ .../migration/order/Messages_pt_PT.json | 24 ++ .../MigrationTile.component.tsx | 32 ++- .../migration-tile/MigrationTile.spec.tsx | 21 +- .../src/data/api/vcdaOrder/vcdaOrder.spec.ts | 97 +++++++ .../data/hooks/vcdaOrder/useOrderVcda.hook.ts | 88 +++++++ .../useApplicationBreadcrumbItems.tsx | 7 + .../migration/order/MigrationOrder.page.tsx | 10 + .../MigrationOrderContent.component.tsx | 242 ++++++++++++++++++ .../migration/order/order.schema.spec.ts | 42 +++ .../migration/order/order.schema.ts | 15 ++ .../order/terms/MigrationOrderTerms.page.tsx | 41 +++ .../order/terms/MigrationOrderTerms.spec.tsx | 25 ++ .../MigrationOrderTermsModal.component.tsx | 103 ++++++++ .../src/routes/routes.constant.ts | 4 + .../src/routes/routes.tsx | 31 +++ .../src/test-utils/test-i18n.ts | 3 + .../src/tracking.constants.ts | 18 ++ .../src/utils/iam.constants.ts | 2 + .../src/utils/testIds.constants.ts | 9 + 26 files changed, 975 insertions(+), 7 deletions(-) create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_de_DE.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_en_GB.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_es_ES.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_CA.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_FR.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_it_IT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pl_PL.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pt_PT.json create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/api/vcdaOrder/vcdaOrder.spec.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcdaOrder/useOrderVcda.hook.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/MigrationOrder.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/_components/MigrationOrderContent.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.spec.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.ts create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/_components/MigrationOrderTermsModal.component.tsx diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_de_DE.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_de_DE.json new file mode 100644 index 000000000000..c9264ecce9c8 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_de_DE.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migration", + "managed_vcd_migration_order_breadcrumb": "Bestellen", + "managed_vcd_migration_order_title": "Managed VCDA für Public VCF", + "managed_vcd_migration_order_subtitle": "Die Dienste basieren auf VMware Cloud Director Availability und dem VCD-Plugin, die auf einer VMware Cloud Foundation-Infrastruktur bereitgestellt werden. Mit dieser Option können Sie Workloads von Ihrem Quell-VCD zu OVHcloud migrieren oder replizieren.", + "managed_vcd_migration_order_solution_title": "Ihre Lösung", + "managed_vcd_migration_order_solution_body": "Der VCDA-Dienst stellt ein Plugin für VCD bereit, um VMs und vApps aus jedem Virtual Data Center (VDC) der Organisation zu replizieren und zu migrieren. Er steht auf Organisationsebene für jeden Public VCF as-a-Service-Benutzer mit der Rolle des Organisationsadministrators zur Verfügung.", + "managed_vcd_migration_order_infrastructure_title": "Geben Sie Ihre Infrastrukturinformationen ein", + "managed_vcd_migration_order_ip_label": "Ihre öffentliche IP", + "managed_vcd_migration_order_ip_helper": "Geben Sie die öffentliche IP an, über die der VCDA auf Ihrem Public VCFaaS erreicht wird.", + "managed_vcd_migration_order_ip_error_required": "Bitte geben Sie eine öffentliche IP ein.", + "managed_vcd_migration_order_ip_error_invalid": "Bitte geben Sie eine gültige IP in CIDR-Notation ein (z. B. xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Nach der Bestellung des Produkts erhalten Sie eine E-Mail mit der Bestätigung der Bereitstellung und allen Informationen, die zum Verbinden Ihres VCDA mit unserem VCDA erforderlich sind.", + "managed_vcd_migration_order_cancel": "Abbrechen", + "managed_vcd_migration_order_order": "Bestellen", + "managed_vcd_migration_order_disabled_tooltip": "Geben Sie eine gültige IP ein, um fortzufahren.", + "managed_vcd_migration_order_error_submit": "Bei der Aufgabe Ihrer Bestellung ist ein Fehler aufgetreten.", + "managed_vcd_migration_order_success_banner_body": "Ihr VCDA-Dienst wird aktiviert. Sie erhalten in Kürze eine Bestätigungs-E-Mail mit Verbindungsanweisungen.", + "managed_vcd_migration_order_terms_title": "Bestätigen Sie Ihre Bestellung", + "managed_vcd_migration_order_terms_intro": "Lesen und akzeptieren Sie die nachstehenden Verträge, um Ihre Bestellung aufzugeben.", + "managed_vcd_migration_order_terms_accept": "Ich habe die Verträge gelesen und akzeptiere sie und verzichte auf mein Widerrufsrecht.", + "managed_vcd_migration_order_terms_confirm": "Bestätigen und bezahlen", + "managed_vcd_migration_order_terms_cancel": "Abbrechen" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_en_GB.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_en_GB.json new file mode 100644 index 000000000000..b14148a6dec0 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_en_GB.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migration", + "managed_vcd_migration_order_breadcrumb": "Order", + "managed_vcd_migration_order_title": "Managed VCDA for Public VCF", + "managed_vcd_migration_order_subtitle": "Services are based on VMware Cloud Director Availability and the VCD plugin, implemented on VMware Cloud Foundation infrastructure. This option allows you to migrate or replicate workloads from your source VCD to OVHcloud.", + "managed_vcd_migration_order_solution_title": "Your solution", + "managed_vcd_migration_order_solution_body": "The VCDA service provides a plugin for VCD to replicate and migrate VMs and vApps from any Virtual Data Center (VDC) of the organisation. It is available at organisation level for any Public VCF as-a-Service user with organisation administrator role.", + "managed_vcd_migration_order_infrastructure_title": "Enter your infrastructure information", + "managed_vcd_migration_order_ip_label": "Your Public IP", + "managed_vcd_migration_order_ip_helper": "Give the Public IP that will reach the VCDA on your Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Please enter a Public IP.", + "managed_vcd_migration_order_ip_error_invalid": "Please enter a valid IP in CIDR notation (e.g. xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Following the order of the product, you will receive a mail confirming the delivery and all information needed to connect your VCDA to our VCDA.", + "managed_vcd_migration_order_cancel": "Cancel", + "managed_vcd_migration_order_order": "Order", + "managed_vcd_migration_order_disabled_tooltip": "Enter a valid IP to continue.", + "managed_vcd_migration_order_error_submit": "An error occurred while placing your order.", + "managed_vcd_migration_order_success_banner_body": "Your VCDA service is being activated. You will receive a confirmation email with connection instructions shortly.", + "managed_vcd_migration_order_terms_title": "Confirm your order", + "managed_vcd_migration_order_terms_intro": "Review and accept the contracts below to place your order.", + "managed_vcd_migration_order_terms_accept": "I have read and accept the contracts and waive my right of withdrawal.", + "managed_vcd_migration_order_terms_confirm": "Confirm and pay", + "managed_vcd_migration_order_terms_cancel": "Cancel" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_es_ES.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_es_ES.json new file mode 100644 index 000000000000..c3bc950940ff --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_es_ES.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migración", + "managed_vcd_migration_order_breadcrumb": "Solicitar", + "managed_vcd_migration_order_title": "VCDA gestionado para Public VCF", + "managed_vcd_migration_order_subtitle": "Los servicios se basan en VMware Cloud Director Availability y el plugin de VCD, implementados sobre una infraestructura VMware Cloud Foundation. Esta opción le permite migrar o replicar cargas de trabajo desde su VCD de origen a OVHcloud.", + "managed_vcd_migration_order_solution_title": "Su solución", + "managed_vcd_migration_order_solution_body": "El servicio VCDA ofrece un plugin para VCD que permite replicar y migrar máquinas virtuales y vApps desde cualquier Virtual Data Center (VDC) de la organización. Está disponible a nivel de organización para cualquier usuario de Public VCF as-a-Service con el rol de administrador de la organización.", + "managed_vcd_migration_order_infrastructure_title": "Introduzca la información de su infraestructura", + "managed_vcd_migration_order_ip_label": "Su IP pública", + "managed_vcd_migration_order_ip_helper": "Indique la IP pública que permitirá acceder al VCDA en su Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Introduzca una IP pública.", + "managed_vcd_migration_order_ip_error_invalid": "Introduzca una IP válida en notación CIDR (por ejemplo, xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Tras solicitar el producto, recibirá un correo electrónico confirmando la entrega y toda la información necesaria para conectar su VCDA con el nuestro.", + "managed_vcd_migration_order_cancel": "Cancelar", + "managed_vcd_migration_order_order": "Solicitar", + "managed_vcd_migration_order_disabled_tooltip": "Introduzca una IP válida para continuar.", + "managed_vcd_migration_order_error_submit": "Se ha producido un error al realizar su pedido.", + "managed_vcd_migration_order_success_banner_body": "Su servicio VCDA se está activando. En breve recibirá un correo electrónico de confirmación con las instrucciones de conexión.", + "managed_vcd_migration_order_terms_title": "Confirme su pedido", + "managed_vcd_migration_order_terms_intro": "Revise y acepte los contratos siguientes para realizar su pedido.", + "managed_vcd_migration_order_terms_accept": "He leído y acepto los contratos y renuncio a mi derecho de desistimiento.", + "managed_vcd_migration_order_terms_confirm": "Confirmar y pagar", + "managed_vcd_migration_order_terms_cancel": "Cancelar" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_CA.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_CA.json new file mode 100644 index 000000000000..291be67004ff --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_CA.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migration", + "managed_vcd_migration_order_breadcrumb": "Commander", + "managed_vcd_migration_order_title": "VCDA géré pour Public VCF", + "managed_vcd_migration_order_subtitle": "Les services reposent sur VMware Cloud Director Availability et le plugin VCD, déployés sur une infrastructure VMware Cloud Foundation. Cette option vous permet de migrer ou de répliquer vos charges de travail depuis votre VCD source vers OVHcloud.", + "managed_vcd_migration_order_solution_title": "Votre solution", + "managed_vcd_migration_order_solution_body": "Le service VCDA fournit un plugin pour VCD permettant de répliquer et de migrer des VM et des vApps depuis n'importe quel Virtual Data Center (VDC) de l'organisation. Il est disponible au niveau de l'organisation pour tout utilisateur Public VCF as-a-Service disposant du rôle d'administrateur de l'organisation.", + "managed_vcd_migration_order_infrastructure_title": "Saisissez les informations de votre infrastructure", + "managed_vcd_migration_order_ip_label": "Votre IP publique", + "managed_vcd_migration_order_ip_helper": "Indiquez l'IP publique qui permettra d'atteindre le VCDA sur votre Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Veuillez saisir une IP publique.", + "managed_vcd_migration_order_ip_error_invalid": "Veuillez saisir une IP valide en notation CIDR (par exemple xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Après la commande du produit, vous recevrez un courriel confirmant la livraison ainsi que toutes les informations nécessaires pour connecter votre VCDA au nôtre.", + "managed_vcd_migration_order_cancel": "Annuler", + "managed_vcd_migration_order_order": "Commander", + "managed_vcd_migration_order_disabled_tooltip": "Saisissez une IP valide pour continuer.", + "managed_vcd_migration_order_error_submit": "Une erreur est survenue lors de la passation de votre commande.", + "managed_vcd_migration_order_success_banner_body": "Votre service VCDA est en cours d'activation. Vous recevrez prochainement un courriel de confirmation contenant les instructions de connexion.", + "managed_vcd_migration_order_terms_title": "Confirmez votre commande", + "managed_vcd_migration_order_terms_intro": "Consultez et acceptez les contrats ci-dessous pour passer votre commande.", + "managed_vcd_migration_order_terms_accept": "J’ai lu et j’accepte les contrats et je renonce à mon droit de rétractation.", + "managed_vcd_migration_order_terms_confirm": "Confirmer et payer", + "managed_vcd_migration_order_terms_cancel": "Annuler" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_FR.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_FR.json new file mode 100644 index 000000000000..62a677cf12f7 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_fr_FR.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migration", + "managed_vcd_migration_order_breadcrumb": "Commander", + "managed_vcd_migration_order_title": "VCDA managé pour Public VCF", + "managed_vcd_migration_order_subtitle": "Les services reposent sur VMware Cloud Director Availability et le plugin VCD, déployés sur une infrastructure VMware Cloud Foundation. Cette option vous permet de migrer ou de répliquer vos charges de travail depuis votre VCD source vers OVHcloud.", + "managed_vcd_migration_order_solution_title": "Votre solution", + "managed_vcd_migration_order_solution_body": "Le service VCDA fournit un plugin pour VCD permettant de répliquer et de migrer des VM et des vApps depuis n'importe quel Virtual Data Center (VDC) de l'organisation. Il est disponible au niveau de l'organisation pour tout utilisateur Public VCF as-a-Service disposant du rôle d'administrateur de l'organisation.", + "managed_vcd_migration_order_infrastructure_title": "Saisissez les informations de votre infrastructure", + "managed_vcd_migration_order_ip_label": "Votre IP publique", + "managed_vcd_migration_order_ip_helper": "Indiquez l'IP publique qui permettra d'atteindre le VCDA sur votre Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Veuillez saisir une IP publique.", + "managed_vcd_migration_order_ip_error_invalid": "Veuillez saisir une IP valide en notation CIDR (par exemple xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Après la commande du produit, vous recevrez un e-mail confirmant la livraison ainsi que toutes les informations nécessaires pour connecter votre VCDA au nôtre.", + "managed_vcd_migration_order_cancel": "Annuler", + "managed_vcd_migration_order_order": "Commander", + "managed_vcd_migration_order_disabled_tooltip": "Saisissez une IP valide pour continuer.", + "managed_vcd_migration_order_error_submit": "Une erreur est survenue lors de la passation de votre commande.", + "managed_vcd_migration_order_success_banner_body": "Votre service VCDA est en cours d'activation. Vous recevrez prochainement un e-mail de confirmation contenant les instructions de connexion.", + "managed_vcd_migration_order_terms_title": "Confirmez votre commande", + "managed_vcd_migration_order_terms_intro": "Consultez et acceptez les contrats ci-dessous pour passer votre commande.", + "managed_vcd_migration_order_terms_accept": "J’ai lu et j’accepte les contrats et je renonce à mon droit de rétractation.", + "managed_vcd_migration_order_terms_confirm": "Confirmer et payer", + "managed_vcd_migration_order_terms_cancel": "Annuler" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_it_IT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_it_IT.json new file mode 100644 index 000000000000..ad0dd2c4ac87 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_it_IT.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migrazione", + "managed_vcd_migration_order_breadcrumb": "Ordina", + "managed_vcd_migration_order_title": "VCDA gestito per Public VCF", + "managed_vcd_migration_order_subtitle": "I servizi si basano su VMware Cloud Director Availability e sul plugin VCD, implementati su un'infrastruttura VMware Cloud Foundation. Questa opzione ti consente di migrare o replicare i carichi di lavoro dal tuo VCD di origine verso OVHcloud.", + "managed_vcd_migration_order_solution_title": "La tua soluzione", + "managed_vcd_migration_order_solution_body": "Il servizio VCDA fornisce un plugin per VCD che permette di replicare e migrare VM e vApp da qualsiasi Virtual Data Center (VDC) dell'organizzazione. È disponibile a livello di organizzazione per qualsiasi utente Public VCF as-a-Service con ruolo di amministratore dell'organizzazione.", + "managed_vcd_migration_order_infrastructure_title": "Inserisci le informazioni della tua infrastruttura", + "managed_vcd_migration_order_ip_label": "Il tuo IP pubblico", + "managed_vcd_migration_order_ip_helper": "Indica l'IP pubblico che permetterà di raggiungere il VCDA sul tuo Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Inserisci un IP pubblico.", + "managed_vcd_migration_order_ip_error_invalid": "Inserisci un IP valido in notazione CIDR (ad esempio xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Dopo aver ordinato il prodotto, riceverai un'e-mail di conferma della consegna con tutte le informazioni necessarie per connettere il tuo VCDA al nostro.", + "managed_vcd_migration_order_cancel": "Annulla", + "managed_vcd_migration_order_order": "Ordina", + "managed_vcd_migration_order_disabled_tooltip": "Inserisci un IP valido per continuare.", + "managed_vcd_migration_order_error_submit": "Si è verificato un errore durante l'invio del tuo ordine.", + "managed_vcd_migration_order_success_banner_body": "Il tuo servizio VCDA è in fase di attivazione. Riceverai a breve un'e-mail di conferma con le istruzioni di connessione.", + "managed_vcd_migration_order_terms_title": "Conferma il tuo ordine", + "managed_vcd_migration_order_terms_intro": "Leggi e accetta i contratti qui sotto per effettuare l’ordine.", + "managed_vcd_migration_order_terms_accept": "Ho letto e accetto i contratti e rinuncio al mio diritto di recesso.", + "managed_vcd_migration_order_terms_confirm": "Conferma e paga", + "managed_vcd_migration_order_terms_cancel": "Annulla" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pl_PL.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pl_PL.json new file mode 100644 index 000000000000..129d2e6b7627 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pl_PL.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migracja", + "managed_vcd_migration_order_breadcrumb": "Zamów", + "managed_vcd_migration_order_title": "Zarządzane VCDA dla Public VCF", + "managed_vcd_migration_order_subtitle": "Usługi opierają się na VMware Cloud Director Availability oraz wtyczce VCD, wdrożonych na infrastrukturze VMware Cloud Foundation. Ta opcja umożliwia migrację lub replikację obciążeń z źródłowego VCD do OVHcloud.", + "managed_vcd_migration_order_solution_title": "Twoje rozwiązanie", + "managed_vcd_migration_order_solution_body": "Usługa VCDA udostępnia wtyczkę dla VCD umożliwiającą replikację i migrację maszyn wirtualnych oraz vApp z dowolnego Virtual Data Center (VDC) organizacji. Jest dostępna na poziomie organizacji dla każdego użytkownika Public VCF as-a-Service posiadającego rolę administratora organizacji.", + "managed_vcd_migration_order_infrastructure_title": "Wprowadź informacje o swojej infrastrukturze", + "managed_vcd_migration_order_ip_label": "Twój publiczny adres IP", + "managed_vcd_migration_order_ip_helper": "Podaj publiczny adres IP, który będzie używany do połączenia z VCDA w Twoim Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Wprowadź publiczny adres IP.", + "managed_vcd_migration_order_ip_error_invalid": "Wprowadź prawidłowy adres IP w notacji CIDR (np. xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Po złożeniu zamówienia otrzymasz wiadomość e-mail z potwierdzeniem dostawy oraz wszystkimi informacjami niezbędnymi do połączenia Twojego VCDA z naszym VCDA.", + "managed_vcd_migration_order_cancel": "Anuluj", + "managed_vcd_migration_order_order": "Zamów", + "managed_vcd_migration_order_disabled_tooltip": "Wprowadź prawidłowy adres IP, aby kontynuować.", + "managed_vcd_migration_order_error_submit": "Podczas składania zamówienia wystąpił błąd.", + "managed_vcd_migration_order_success_banner_body": "Twoja usługa VCDA jest aktywowana. Wkrótce otrzymasz wiadomość e-mail z potwierdzeniem oraz instrukcjami połączenia.", + "managed_vcd_migration_order_terms_title": "Potwierdź zamówienie", + "managed_vcd_migration_order_terms_intro": "Zapoznaj się z poniższymi umowami i zaakceptuj je, aby złożyć zamówienie.", + "managed_vcd_migration_order_terms_accept": "Przeczytałem i akceptuję umowy oraz rezygnuję z prawa do odstąpienia od umowy.", + "managed_vcd_migration_order_terms_confirm": "Potwierdź i zapłać", + "managed_vcd_migration_order_terms_cancel": "Anuluj" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pt_PT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pt_PT.json new file mode 100644 index 000000000000..b14148a6dec0 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/migration/order/Messages_pt_PT.json @@ -0,0 +1,24 @@ +{ + "managed_vcd_migration_breadcrumb": "Migration", + "managed_vcd_migration_order_breadcrumb": "Order", + "managed_vcd_migration_order_title": "Managed VCDA for Public VCF", + "managed_vcd_migration_order_subtitle": "Services are based on VMware Cloud Director Availability and the VCD plugin, implemented on VMware Cloud Foundation infrastructure. This option allows you to migrate or replicate workloads from your source VCD to OVHcloud.", + "managed_vcd_migration_order_solution_title": "Your solution", + "managed_vcd_migration_order_solution_body": "The VCDA service provides a plugin for VCD to replicate and migrate VMs and vApps from any Virtual Data Center (VDC) of the organisation. It is available at organisation level for any Public VCF as-a-Service user with organisation administrator role.", + "managed_vcd_migration_order_infrastructure_title": "Enter your infrastructure information", + "managed_vcd_migration_order_ip_label": "Your Public IP", + "managed_vcd_migration_order_ip_helper": "Give the Public IP that will reach the VCDA on your Public VCFaaS.", + "managed_vcd_migration_order_ip_error_required": "Please enter a Public IP.", + "managed_vcd_migration_order_ip_error_invalid": "Please enter a valid IP in CIDR notation (e.g. xxx.xxx.xxx.xxx/32).", + "managed_vcd_migration_order_email_confirmation": "Following the order of the product, you will receive a mail confirming the delivery and all information needed to connect your VCDA to our VCDA.", + "managed_vcd_migration_order_cancel": "Cancel", + "managed_vcd_migration_order_order": "Order", + "managed_vcd_migration_order_disabled_tooltip": "Enter a valid IP to continue.", + "managed_vcd_migration_order_error_submit": "An error occurred while placing your order.", + "managed_vcd_migration_order_success_banner_body": "Your VCDA service is being activated. You will receive a confirmation email with connection instructions shortly.", + "managed_vcd_migration_order_terms_title": "Confirm your order", + "managed_vcd_migration_order_terms_intro": "Review and accept the contracts below to place your order.", + "managed_vcd_migration_order_terms_accept": "I have read and accept the contracts and waive my right of withdrawal.", + "managed_vcd_migration_order_terms_confirm": "Confirm and pay", + "managed_vcd_migration_order_terms_cancel": "Cancel" +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx index 960fbfb31b78..fe7e1c19fbc4 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx @@ -1,6 +1,8 @@ +import { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; -import { DashboardTile } from '@ovh-ux/manager-react-components'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useVcdOrganization } from '@ovh-ux/manager-module-vcd-api'; +import { DashboardTile, ManagerButton } from '@ovh-ux/manager-react-components'; import { OdsBadge, OdsButton, @@ -14,8 +16,12 @@ import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, } from '@ovhcloud/ods-components'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; import { useVcdaStatus } from '@/data/hooks/vcda/useVcdaStatus.hook'; +import { urls } from '@/routes/routes.constant'; +import { iamActions } from '@/utils/iam.constants'; import TEST_IDS from '@/utils/testIds.constants'; +import { TRACKING } from '@/tracking.constants'; import ComingSoonButton from './_components/ComingSoonButton.component'; import ActiveBody from './_components/ActiveBody.component'; @@ -24,6 +30,9 @@ const MIGRATION_TILE_ID = 'migration'; export default function MigrationTile() { const { t } = useTranslation('vcda'); const { id } = useParams(); + const navigate = useNavigate(); + const { trackClick } = useOvhTracking(); + const { data: vcdOrganization } = useVcdOrganization({ id }); const { data: status, isPending, isError, refetch } = useVcdaStatus(id); const renderBody = () => { @@ -56,10 +65,21 @@ export default function MigrationTile() { switch (status.kind) { case 'inactive': return ( - + + { + trackClick(TRACKING.dashboard.orderMigrationTile); + navigate(urls.migrationOrder.replace(':id', id ?? '')); + }} + /> + ); case 'provisioning': return ( diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx index b02017e828b6..44c4e352b2df 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.spec.tsx @@ -21,7 +21,7 @@ import { useVcdaStatus } from '../../../data/hooks/vcda/useVcdaStatus.hook'; import { labels, translations } from '../../../test-utils/test-i18n'; import vcdaMessages from '../../../../public/translations/vcda/Messages_fr_FR.json'; import TEST_IDS from '../../../utils/testIds.constants'; -import { APP_NAME } from '../../../tracking.constants'; +import { TRACKING, APP_NAME } from '../../../tracking.constants'; const navigateMock = vi.fn(); const trackClickMock = vi.fn(); @@ -147,6 +147,25 @@ describe('MigrationTile', () => { expect(refetchMock).toHaveBeenCalled(); }); + it('renders the Order CTA in the inactive state and navigates + tracks on click', async () => { + const user = userEvent.setup(); + mockStatus({ data: { kind: 'inactive' } }); + renderComponent(); + + const cta = await getElementByTestId(TEST_IDS.migrationTileOrderCta); + await assertElementLabel({ + element: cta, + label: labels.vcda.tile.cta.order, + }); + + await act(() => user.click(cta)); + expect(trackClickMock).toHaveBeenCalledWith( + TRACKING.dashboard.orderMigrationTile, + ); + expect(navigateMock).toHaveBeenCalledWith( + '/public-vcf-aas/org-123/migration/order-migration', + ); + }); it('renders the provisioning badge for CREATING', async () => { mockStatus({ data: { kind: 'provisioning' } }); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/api/vcdaOrder/vcdaOrder.spec.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/api/vcdaOrder/vcdaOrder.spec.ts new file mode 100644 index 000000000000..421a38c92f15 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/api/vcdaOrder/vcdaOrder.spec.ts @@ -0,0 +1,97 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { VCDOrganization , + checkoutVcdaOrder, + getVcdaDatacenterZone, + prepareVcdaOrder, + VCDA_ORDER, + VCDA_ORDER_CONFIG_LABEL, +} from '@ovh-ux/manager-module-vcd-api'; +import { + createCart, + postOrderCartCartIdCheckout, +} from '@ovh-ux/manager-module-order'; + +vi.mock('@ovh-ux/manager-module-order', () => ({ + createCart: vi.fn(), + postOrderCartCartIdCheckout: vi.fn(), +})); + +const makeOrg = (region: string) => + (({ currentState: { region } } as unknown) as VCDOrganization); + +describe('getVcdaDatacenterZone', () => { + it('returns the BARE lowercased region with no -a suffix (R11)', () => { + expect(getVcdaDatacenterZone(makeOrg('EU-WEST-RBX'))).toBe('eu-west-rbx'); + }); + + it('does NOT append the Veeam availability-zone -a suffix', () => { + expect(getVcdaDatacenterZone(makeOrg('CA-EAST-BHS'))).not.toMatch(/-a$/); + }); + + it('returns an empty string when the organisation is missing', () => { + expect(getVcdaDatacenterZone(undefined)).toBe(''); + }); +}); + +describe('prepareVcdaOrder', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createCart).mockResolvedValue({ + cartId: 'cart-123', + contractList: [{ name: 'Contract A', url: 'http://c/a.pdf' }], + }); + }); + + it('creates the vcdaMigration cart item with the 3 config labels and returns the contracts; NO checkout', async () => { + const result = await prepareVcdaOrder({ + ovhSubsidiary: 'FR', + config: { + orgId: 'org-1', + externalIp: '1.2.3.4/32', + datacenterZone: 'eu-west-rbx', + }, + }); + + expect(createCart).toHaveBeenCalledTimes(1); + const cartArg = vi.mocked(createCart).mock.calls[0][0]; + expect(cartArg.ovhSubsidiary).toBe('FR'); + + const [item] = cartArg.items; + expect(item.itemEndpoint).toBe(VCDA_ORDER.PRODUCT_FAMILY); + expect(item.options).toMatchObject({ + planCode: VCDA_ORDER.PLAN_CODE, + pricingMode: VCDA_ORDER.PRICING_MODE, + duration: VCDA_ORDER.DURATION, + quantity: VCDA_ORDER.QUANTITY, + }); + expect(item.configurations).toEqual([ + { label: VCDA_ORDER_CONFIG_LABEL.ORG_ID, value: 'org-1' }, + { label: VCDA_ORDER_CONFIG_LABEL.EXTERNAL_IPS, value: '1.2.3.4/32' }, + { label: VCDA_ORDER_CONFIG_LABEL.DATACENTER_ZONE, value: 'eu-west-rbx' }, + ]); + expect(result).toEqual({ + cartId: 'cart-123', + contractList: [{ name: 'Contract A', url: 'http://c/a.pdf' }], + }); + expect(postOrderCartCartIdCheckout).not.toHaveBeenCalled(); + }); +}); + +describe('checkoutVcdaOrder', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(postOrderCartCartIdCheckout).mockResolvedValue({ + data: { orderId: 42 }, + } as never); + }); + + it('checks out the cart with auto-pay and waived retractation period', async () => { + await checkoutVcdaOrder('cart-123'); + + expect(postOrderCartCartIdCheckout).toHaveBeenCalledWith({ + cartId: 'cart-123', + autoPayWithPreferredPaymentMethod: true, + waiveRetractationPeriod: true, + }); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcdaOrder/useOrderVcda.hook.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcdaOrder/useOrderVcda.hook.ts new file mode 100644 index 000000000000..f4d359a3aad2 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/data/hooks/vcdaOrder/useOrderVcda.hook.ts @@ -0,0 +1,88 @@ +import { useContext } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { ApiError } from '@ovh-ux/manager-core-api'; +import { VCDOrganization , + checkoutVcdaOrder, + getVcdaDatacenterZone, + PreparedVcdaOrder, + prepareVcdaOrder, +} from '@ovh-ux/manager-module-vcd-api'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { getVcdaMigrationQueryKey } from '@/data/hooks/vcda/vcdaQueryKey'; +import { getVcdaStatusQueryKey } from '@/data/hooks/vcda/useVcdaStatus.hook'; + +export type UsePrepareVcdaOrderParams = { + orgId: string; + organization?: VCDOrganization; +}; + +export type OrderVcdaVariables = { + initialWhitelistCidr: string; +}; + +export const usePrepareVcdaOrder = ({ + orgId, + organization, +}: UsePrepareVcdaOrderParams) => { + const { environment } = useContext(ShellContext); + const { ovhSubsidiary } = environment.getUser(); + + const mutation = useMutation( + { + mutationFn: ({ initialWhitelistCidr }) => + prepareVcdaOrder({ + ovhSubsidiary, + config: { + orgId, + externalIp: initialWhitelistCidr, + datacenterZone: getVcdaDatacenterZone(organization), + }, + }), + }, + ); + + return { + prepareVcda: mutation.mutate, + isPending: mutation.isPending, + isError: mutation.isError, + error: mutation.error, + reset: mutation.reset, + }; +}; + +export type UseCheckoutVcdaOrderParams = { + orgId: string; + onSuccess?: () => void; +}; + +/** + * The tile/tab (useVcdaStatus) and the migration page (useVcdaMigration) read two + * distinct caches — both are invalidated on success. + */ +export const useCheckoutVcdaOrder = ({ + orgId, + onSuccess, +}: UseCheckoutVcdaOrderParams) => { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: (cartId) => checkoutVcdaOrder(cartId), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getVcdaStatusQueryKey(orgId), + }); + queryClient.invalidateQueries({ + queryKey: getVcdaMigrationQueryKey(orgId), + }); + onSuccess?.(); + }, + }); + + return { + checkoutVcda: mutation.mutate, + isPending: mutation.isPending, + isError: mutation.isError, + error: mutation.error, + reset: mutation.reset, + }; +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/hooks/breadcrumb/useApplicationBreadcrumbItems.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/hooks/breadcrumb/useApplicationBreadcrumbItems.tsx index 2d6baaf8e389..ab0964072328 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/hooks/breadcrumb/useApplicationBreadcrumbItems.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/hooks/breadcrumb/useApplicationBreadcrumbItems.tsx @@ -18,6 +18,7 @@ export const useApplicationBreadcrumbItems = () => { 'datacentres/compute', 'datacentres/storage', 'datacentres/vrack-segment', + 'migration/order', NAMESPACES.ACTIONS, ]); const { id, vdcId } = useParams(); @@ -48,6 +49,12 @@ export const useApplicationBreadcrumbItems = () => { [subRoutes.datacentreStorageOrder]: t( 'datacentres/storage:managed_vcd_vdc_storage_order_cta', ), + [subRoutes.migration]: t( + 'migration/order:managed_vcd_migration_breadcrumb', + ), + [subRoutes.migrationOrder]: t( + 'migration/order:managed_vcd_migration_order_breadcrumb', + ), [subRoutes.vrackSegments]: VRACK_LABEL, [subRoutes.vrackEditVlanId]: t( 'datacentres/vrack-segment:managed_vcd_dashboard_vrack_edit_vlan', diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/MigrationOrder.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/MigrationOrder.page.tsx new file mode 100644 index 000000000000..469f14fa73fb --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/MigrationOrder.page.tsx @@ -0,0 +1,10 @@ +import VcdaFeatureGuard from '@/components/vcda/VcdaFeatureGuard.component'; +import MigrationOrderContent from './_components/MigrationOrderContent.component'; + +export default function MigrationOrderPage() { + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/_components/MigrationOrderContent.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/_components/MigrationOrderContent.component.tsx new file mode 100644 index 000000000000..f9294f00e007 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/_components/MigrationOrderContent.component.tsx @@ -0,0 +1,242 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import { Control, FieldValues, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + BaseLayout, + ManagerButton, + useAuthorizationIam, +} from '@ovh-ux/manager-react-components'; +import { useVcdOrganization , PreparedVcdaOrder } from '@ovh-ux/manager-module-vcd-api'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { + OdsButton, + OdsCard, + OdsMessage, + OdsSpinner, + OdsText, +} from '@ovhcloud/ods-components/react'; +import { + ODS_BUTTON_VARIANT, + ODS_SPINNER_SIZE, + ODS_TEXT_PRESET, +} from '@ovhcloud/ods-components'; +import Breadcrumb from '@/components/breadcrumb/Breadcrumb.component'; +import { RhfField } from '@/components/Fields'; +import { + useCheckoutVcdaOrder, + usePrepareVcdaOrder, +} from '@/data/hooks/vcdaOrder/useOrderVcda.hook'; +import { useMessageContext } from '@/context/Message.context'; +import { urls, subRoutes } from '@/routes/routes.constant'; +import { iamActions } from '@/utils/iam.constants'; +import TEST_IDS from '@/utils/testIds.constants'; +import { TRACKING } from '@/tracking.constants'; +import { OrderFormSchema, OrderFormValues } from '../order.schema'; + +/** Passed to the contracts/terms step via the router Outlet context. */ +export interface MigrationOrderTermsContext { + prepared: PreparedVcdaOrder | null; + isSubmitting: boolean; + isError: boolean; + onConfirm: () => void; + clearPrepared: () => void; +} + +export default function MigrationOrderContent() { + const { t } = useTranslation('migration/order'); + const { id } = useParams(); + const navigate = useNavigate(); + const { trackClick } = useOvhTracking(); + const { addSuccess } = useMessageContext(); + + const dashboardUrl = urls.dashboard.replace(':id', id ?? ''); + + const { data: organization } = useVcdOrganization({ id }); + const orgUrn = organization?.data?.iam?.urn; + + const { isAuthorized } = useAuthorizationIam( + [iamActions.vmwareCloudDirectorApiovhMigrationCreate], + orgUrn ?? '', + ); + + const { + control, + register, + handleSubmit, + formState: { isValid, errors }, + } = useForm({ + resolver: zodResolver(OrderFormSchema), + mode: 'onChange', + defaultValues: { initialWhitelistCidr: '' }, + }); + + const goToDashboard = () => navigate(dashboardUrl); + + const [prepared, setPrepared] = useState(null); + + const { prepareVcda, isPending, isError, reset } = usePrepareVcdaOrder({ + orgId: id ?? '', + organization: organization?.data, + }); + + const { + checkoutVcda, + isPending: isCheckingOut, + isError: isCheckoutError, + } = useCheckoutVcdaOrder({ + orgId: id ?? '', + onSuccess: () => { + addSuccess({ + content: t('managed_vcd_migration_order_success_banner_body'), + isDismissible: true, + includedSubRoutes: [id ?? ''], + excludedSubRoutes: [subRoutes.migrationOrder], + }); + goToDashboard(); + }, + }); + + const onSubmit = handleSubmit((values) => { + trackClick(TRACKING.dashboard.orderMigration); + prepareVcda( + { initialWhitelistCidr: values.initialWhitelistCidr }, + { + onSuccess: (result) => { + setPrepared(result); + navigate(subRoutes.migrationOrderTerms); + }, + }, + ); + }); + + const onConfirmTerms = () => { + if (prepared) checkoutVcda(prepared.cartId); + }; + + const clearPrepared = () => setPrepared(null); + + const onCancel = () => { + trackClick(TRACKING.dashboard.orderMigrationCancel); + goToDashboard(); + }; + + const isOrderDisabled = !isValid || isPending || !isAuthorized; + + return ( + }> +
+
+ + {t('managed_vcd_migration_order_title')} + + + {t('managed_vcd_migration_order_subtitle')} + +
+ + + + {t('managed_vcd_migration_order_solution_title')} + + + {t('managed_vcd_migration_order_solution_body')} + + + + + + {t('managed_vcd_migration_order_infrastructure_title')} + + } + errorMessage={ + errors.initialWhitelistCidr + ? t(errors.initialWhitelistCidr.message ?? '') + : undefined + } + > + + {t('managed_vcd_migration_order_ip_label')} + + + + + + {t('managed_vcd_migration_order_email_confirmation')} + + + + {isError && ( + + {t('managed_vcd_migration_order_error_submit')} + + )} + +
+ + + {isPending && ( + + )} + +
+
+ + +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.spec.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.spec.ts new file mode 100644 index 000000000000..aefaebbce3e5 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { OrderFormSchema } from './order.schema'; + +describe('OrderFormSchema', () => { + it('accepts a valid /32 host CIDR (the order express example)', () => { + const result = OrderFormSchema.safeParse({ + initialWhitelistCidr: '203.0.113.7/32', + }); + expect(result.success).toBe(true); + }); + + it('accepts a valid subnet CIDR', () => { + expect( + OrderFormSchema.safeParse({ initialWhitelistCidr: '10.0.0.0/24' }) + .success, + ).toBe(true); + }); + + it('rejects a bare IP without a CIDR mask', () => { + expect( + OrderFormSchema.safeParse({ initialWhitelistCidr: '203.0.113.7' }) + .success, + ).toBe(false); + }); + + it('rejects an out-of-range octet', () => { + expect( + OrderFormSchema.safeParse({ initialWhitelistCidr: '999.0.0.1/32' }) + .success, + ).toBe(false); + }); + + it('rejects an empty value with the required message', () => { + const result = OrderFormSchema.safeParse({ initialWhitelistCidr: '' }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe( + 'managed_vcd_migration_order_ip_error_required', + ); + } + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.ts new file mode 100644 index 000000000000..e227b0cea7d0 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/order.schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const Ipv4CidrSchema = z + .string() + .trim() + .min(1, { message: 'managed_vcd_migration_order_ip_error_required' }) + .refine((val) => z.cidrv4().safeParse(val).success, { + message: 'managed_vcd_migration_order_ip_error_invalid', + }); + +export const OrderFormSchema = z.object({ + initialWhitelistCidr: Ipv4CidrSchema, +}); + +export type OrderFormValues = z.infer; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.page.tsx new file mode 100644 index 000000000000..ca56c3b297a2 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.page.tsx @@ -0,0 +1,41 @@ +import { Suspense } from 'react'; +import { Navigate, useNavigate, useOutletContext } from 'react-router-dom'; +import MigrationOrderTermsModal from './_components/MigrationOrderTermsModal.component'; +import { MigrationOrderTermsContext } from '../_components/MigrationOrderContent.component'; + +/** + * Contracts/terms step. Without a prepared cart (e.g. a deep-link before a valid + * CIDR was submitted) it resolves back to the order page. + */ +export default function MigrationOrderTermsPage() { + const navigate = useNavigate(); + const { + prepared, + isSubmitting, + isError, + onConfirm, + clearPrepared, + } = useOutletContext(); + + if (!prepared) { + return ; + } + + const onClose = () => { + if (isSubmitting) return; + clearPrepared(); + navigate('..'); + }; + + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.spec.tsx new file mode 100644 index 000000000000..86b48329d123 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.spec.tsx @@ -0,0 +1,25 @@ +import { getElementByTestId } from '@ovh-ux/manager-core-test-utils'; +import { renderTest } from '@/test-utils'; +import TEST_IDS from '@/utils/testIds.constants'; +import { FEATURES } from '@/utils/features.constants'; +import { DEFAULT_ORGANIZATION_ID } from '@/mocks/vcda'; + +const termsRoute = `/${DEFAULT_ORGANIZATION_ID}/migration/order-migration/terms`; +const flagOn = { [FEATURES.HPC_VCFAAS_VCDA]: true }; + +describe('Migration — order contracts/terms step', () => { + // Deep-linking the navigable step with no prepared cart resolves back to the + // order page so the user completes the IP first — it never shows the CGV modal. + it('falls back to the order page on a direct deep-link without a prepared cart', async () => { + await renderTest({ initialRoute: termsRoute, feature: flagOn }); + + const submit = await getElementByTestId(TEST_IDS.migrationOrderSubmitCta); + expect(submit).toBeInTheDocument(); + + expect( + document.querySelector( + `[data-testid="${TEST_IDS.migrationOrderTermsModal}"]`, + ), + ).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/_components/MigrationOrderTermsModal.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/_components/MigrationOrderTermsModal.component.tsx new file mode 100644 index 000000000000..8cc0d472556b --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/order/terms/_components/MigrationOrderTermsModal.component.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + OdsButton, + OdsCheckbox, + OdsLink, + OdsMessage, + OdsModal, + OdsText, +} from '@ovhcloud/ods-components/react'; +import { ODS_BUTTON_VARIANT, ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; +import { VcdaContract } from '@ovh-ux/manager-module-vcd-api'; +import TEST_IDS from '@/utils/testIds.constants'; + +interface MigrationOrderTermsModalProps { + contracts: VcdaContract[]; + isSubmitting: boolean; + isError: boolean; + onConfirm: () => void; + onClose: () => void; +} + +/** The paying checkout is gated behind a mandatory contracts-acceptance checkbox. */ +export default function MigrationOrderTermsModal({ + contracts, + isSubmitting, + isError, + onConfirm, + onClose, +}: MigrationOrderTermsModalProps) { + const { t } = useTranslation('migration/order'); + const [accepted, setAccepted] = useState(false); + + return ( + + + {t('managed_vcd_migration_order_terms_title')} + + +
+ + {t('managed_vcd_migration_order_terms_intro')} + + + {contracts.length > 0 && ( +
    + {contracts.map((contract) => ( +
  • + +
  • + ))} +
+ )} + +
+ setAccepted(!accepted)} + data-testid={TEST_IDS.migrationOrderTermsAccept} + /> + +
+ + {isError && ( + + {t('managed_vcd_migration_order_error_submit')} + + )} +
+ +
+ + +
+
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts index 02e665993bbd..5b1c4d944d4d 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts @@ -15,6 +15,8 @@ export const subRoutes = { datacentreComputeId: ':computeId', datacentreComputeDelete: 'delete-compute', migration: 'migration', + migrationOrder: 'order-migration', + migrationOrderTerms: 'terms', migrationAddIp: 'add-ip', migrationDeleteIp: 'delete-ip', resetPassword: 'reset-password', @@ -40,6 +42,8 @@ export const urls = { dashboard: `/${subRoutes.root}/${subRoutes.dashboard}`, resetPassword: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.resetPassword}`, migration: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}`, + migrationOrder: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationOrder}`, + migrationOrderTerms: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationOrder}/${subRoutes.migrationOrderTerms}`, migrationAddIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationAddIp}`, migrationDeleteIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationDeleteIp}`, editName: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.editName}`, diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx index bf1188526645..4b1c257d2a12 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx @@ -38,6 +38,14 @@ const OrganizationResetPasswordPage = React.lazy(() => const MigrationPage = React.lazy(() => import('@/pages/dashboard/organization/migration/Migration.page'), ); +const MigrationOrderPage = React.lazy(() => + import('@/pages/dashboard/organization/migration/order/MigrationOrder.page'), +); +const MigrationOrderTermsPage = React.lazy(() => + import( + '@/pages/dashboard/organization/migration/order/terms/MigrationOrderTerms.page' + ), +); const DatacentresPage = React.lazy(() => import('@/pages/listing/datacentres/Datacentres.page'), ); @@ -239,6 +247,29 @@ export default ( }, }} /> + + + Date: Thu, 25 Jun 2026 16:34:00 +0200 Subject: [PATCH 4/4] feat(service-termination): add VCDA migration service termination ref: #BKP-1174 Signed-off-by: Thibault Barske --- .../translations/vcda/Messages_de_DE.json | 11 +- .../translations/vcda/Messages_en_GB.json | 11 +- .../translations/vcda/Messages_es_ES.json | 11 +- .../translations/vcda/Messages_fr_CA.json | 11 +- .../translations/vcda/Messages_fr_FR.json | 11 +- .../translations/vcda/Messages_it_IT.json | 11 +- .../translations/vcda/Messages_pl_PL.json | 11 +- .../translations/vcda/Messages_pt_PT.json | 11 +- .../MigrationTile.component.tsx | 9 +- .../_components/ActiveBody.component.tsx | 10 +- .../ComingSoonButton.component.tsx | 37 ------- .../_components/TerminateAction.component.tsx | 69 ++++++++++++ .../terminate/TerminateMigration.page.tsx | 10 ++ .../terminate/TerminateMigration.spec.tsx | 104 ++++++++++++++++++ .../TerminateMigrationContent.component.tsx | 79 +++++++++++++ .../src/routes/routes.constant.ts | 2 + .../src/routes/routes.tsx | 16 +++ .../src/tracking.constants.ts | 6 + .../src/utils/iam.constants.ts | 1 + .../src/utils/testIds.constants.ts | 2 + 20 files changed, 374 insertions(+), 59 deletions(-) delete mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/TerminateAction.component.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.page.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.spec.tsx create mode 100644 packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/_components/TerminateMigrationContent.component.tsx diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json index fa586136dc8d..df9cb9d02164 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_de_DE.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json index 408b70be9e4e..7cc9b817c6d1 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_en_GB.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json index a4adc8af9731..acf18f2063f2 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_es_ES.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json index 7db26b688811..a4b053f6a264 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_CA.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Résilier le service" + "cta": "Résilier le service", + "ariaLabel": "Résilier le service VCDA", + "modalDescription": "Cette action résiliera le service VCDA à la prochaine date d’expiration de facturation. Cette opération est irréversible depuis cette interface.", + "success": "Résiliation programmée. Le service sera supprimé à la prochaine date de facturation.", + "disabled": { + "updating": "Une opération est en cours.", + "suspended": "Le service est suspendu — contactez le support.", + "error": "Le service est en erreur — contactez le support.", + "deleting": "Résiliation déjà programmée." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json index 7db26b688811..a4b053f6a264 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_fr_FR.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Résilier le service" + "cta": "Résilier le service", + "ariaLabel": "Résilier le service VCDA", + "modalDescription": "Cette action résiliera le service VCDA à la prochaine date d’expiration de facturation. Cette opération est irréversible depuis cette interface.", + "success": "Résiliation programmée. Le service sera supprimé à la prochaine date de facturation.", + "disabled": { + "updating": "Une opération est en cours.", + "suspended": "Le service est suspendu — contactez le support.", + "error": "Le service est en erreur — contactez le support.", + "deleting": "Résiliation déjà programmée." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json index 1800a9eaf947..b28146423cfc 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_it_IT.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json index 07776fdcfb56..6ca8c2e272d0 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pl_PL.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json index cc8ce2f08be6..39ab5c011369 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/vcda/Messages_pt_PT.json @@ -26,6 +26,15 @@ } }, "serviceTermination": { - "cta": "Terminate service" + "cta": "Terminate service", + "ariaLabel": "Terminate VCDA service", + "modalDescription": "This action will terminate the VCDA service at the next billing expiration date. This cannot be undone from this interface.", + "success": "Termination scheduled. The service will be deleted at the next billing date.", + "disabled": { + "updating": "An operation is in progress.", + "suspended": "Service is suspended — contact support.", + "error": "Service is in error — contact support.", + "deleting": "Termination already scheduled." + } } } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx index fe7e1c19fbc4..fb9825a178fb 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/MigrationTile.component.tsx @@ -12,7 +12,6 @@ import { } from '@ovhcloud/ods-components/react'; import { ODS_BADGE_COLOR, - ODS_BUTTON_COLOR, ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, } from '@ovhcloud/ods-components'; @@ -22,8 +21,8 @@ import { urls } from '@/routes/routes.constant'; import { iamActions } from '@/utils/iam.constants'; import TEST_IDS from '@/utils/testIds.constants'; import { TRACKING } from '@/tracking.constants'; -import ComingSoonButton from './_components/ComingSoonButton.component'; import ActiveBody from './_components/ActiveBody.component'; +import TerminateAction from './_components/TerminateAction.component'; const MIGRATION_TILE_ID = 'migration'; @@ -98,11 +97,7 @@ export default function MigrationTile() { label={t('tile.badge.deleting')} data-testid={TEST_IDS.migrationTileDeletingBadge} /> - + ); case 'active': diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx index bb983023bac8..f43c8509d27d 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ActiveBody.component.tsx @@ -1,12 +1,12 @@ import { useTranslation } from 'react-i18next'; import { OdsBadge, OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_BADGE_COLOR, ODS_BUTTON_COLOR } from '@ovhcloud/ods-components'; +import { ODS_BADGE_COLOR } from '@ovhcloud/ods-components'; import { VcdaResourceStatus, VcdaTileStatus, } from '@ovh-ux/manager-module-vcd-api'; import TEST_IDS from '@/utils/testIds.constants'; -import ComingSoonButton from './ComingSoonButton.component'; +import TerminateAction from './TerminateAction.component'; type BadgeConfig = { color: ODS_BADGE_COLOR; @@ -62,11 +62,7 @@ export default function ActiveBody({ ) : ( badge )} - + ); } diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx deleted file mode 100644 index e4ddbab38553..000000000000 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/ComingSoonButton.component.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { OdsButton, OdsTooltip } from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_COLOR, - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, -} from '@ovhcloud/ods-components'; - -// Disabled placeholder CTA. The dashboard tile is complete & previewable, but the -// Order and Terminate actions are wired by the order / service-termination feature -// PRs — until then they are inert and flagged "coming soon" (not production-ready). -export default function ComingSoonButton({ - triggerId, - label, - color, -}: Readonly<{ - triggerId: string; - label: string; - color?: ODS_BUTTON_COLOR; -}>) { - const { t: tDashboard } = useTranslation('dashboard'); - return ( - <> - - - {tDashboard('managed_vcd_dashboard_coming_soon')} - - - ); -} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/TerminateAction.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/TerminateAction.component.tsx new file mode 100644 index 000000000000..634d19059b89 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/tiles/migration-tile/_components/TerminateAction.component.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { OdsButton, OdsTooltip } from '@ovhcloud/ods-components/react'; +import { + ODS_BUTTON_COLOR, + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, +} from '@ovhcloud/ods-components'; +import { VcdaResourceStatus } from '@ovh-ux/manager-module-vcd-api'; +import { urls } from '@/routes/routes.constant'; +import TEST_IDS from '@/utils/testIds.constants'; +import { TRACKING } from '@/tracking.constants'; + +const TERMINATE_DISABLED_TOOLTIP_KEYS: Partial> = { + UPDATING: 'serviceTermination.disabled.updating', + SUSPENDED: 'serviceTermination.disabled.suspended', + ERROR: 'serviceTermination.disabled.error', + DELETING: 'serviceTermination.disabled.deleting', +}; + +// VCDA service termination CTA — enabled only in READY (R2); disabled with a +// per-status tooltip otherwise (R1/R2). Route-based flow (service-termination +// ux_legacy): navigates to the terminate route which renders the DeleteModal. +export default function TerminateAction({ + resourceStatus, +}: Readonly<{ resourceStatus: VcdaResourceStatus }>) { + const { t } = useTranslation('vcda'); + const { id } = useParams(); + const navigate = useNavigate(); + const { trackClick } = useOvhTracking(); + const isReady = resourceStatus === 'READY'; + const tooltipKey = TERMINATE_DISABLED_TOOLTIP_KEYS[resourceStatus]; + + const button = ( + { + trackClick(TRACKING.dashboard.terminateMigration); + navigate(urls.migrationTerminate.replace(':id', id ?? '')); + } + : undefined + } + /> + ); + + if (isReady || !tooltipKey) { + return button; + } + + return ( + <> + {button} + {t(tooltipKey)} + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.page.tsx new file mode 100644 index 000000000000..d7494f18d26c --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.page.tsx @@ -0,0 +1,10 @@ +import VcdaFeatureGuard from '@/components/vcda/VcdaFeatureGuard.component'; +import TerminateMigrationContent from './_components/TerminateMigrationContent.component'; + +export default function TerminateMigrationPage() { + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.spec.tsx new file mode 100644 index 000000000000..2877e30c9657 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/TerminateMigration.spec.tsx @@ -0,0 +1,104 @@ +import { waitFor, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { getElementByTestId } from '@ovh-ux/manager-core-test-utils'; +import { renderTest } from '@/test-utils'; +import TEST_IDS from '@/utils/testIds.constants'; +import { FEATURES } from '@/utils/features.constants'; +import { DEFAULT_ORGANIZATION_ID, vcdaMigrationMock } from '@/mocks/vcda'; + +const dashboardRoute = `/${DEFAULT_ORGANIZATION_ID}`; +const flagOn = { [FEATURES.HPC_VCFAAS_VCDA]: true }; + +describe('VCDA — Service termination', () => { + it('READY: Terminate CTA is enabled and opens the confirmation modal', async () => { + await renderTest({ + initialRoute: dashboardRoute, + feature: flagOn, + vcdaMigration: vcdaMigrationMock, // READY + }); + + const cta = await getElementByTestId(TEST_IDS.migrationTerminateCta); + expect(cta).not.toHaveAttribute('is-disabled', 'true'); + + const user = userEvent.setup(); + await act(() => user.click(cta)); + + const description = await getElementByTestId( + TEST_IDS.migrationTerminateDescription, + ); + expect(description).toBeInTheDocument(); + }); + + it('non-READY (SUSPENDED): Terminate CTA is rendered but disabled', async () => { + await renderTest({ + initialRoute: dashboardRoute, + feature: flagOn, + vcdaMigration: { ...vcdaMigrationMock, resourceStatus: 'SUSPENDED' }, + }); + + const cta = await getElementByTestId(TEST_IDS.migrationTerminateCta); + await waitFor(() => + expect(cta).toHaveAttribute('is-disabled', 'true'), + ); + }); + + const modalDescription = () => + document.querySelector( + `[data-testid="${TEST_IDS.migrationTerminateDescription}"]`, + ); + + // `getServicesMocks` (wired in renderTest) mocks GET /services?resourceName= → + // [serviceId] and POST /services/:id/terminate — `useDeleteService` resolves the + // serviceId from the resourceName and terminates, so there is no extra mock to roll. + it('confirming terminates the service and closes the modal on success', async () => { + await renderTest({ + initialRoute: dashboardRoute, + feature: flagOn, + vcdaMigration: vcdaMigrationMock, + }); + + const user = userEvent.setup(); + const cta = await getElementByTestId(TEST_IDS.migrationTerminateCta); + await act(() => user.click(cta)); + + // The modal opens before useVcdaMigration settles, so Confirm starts disabled + // (isLoading). Wait until the migration loads and Confirm is enabled. + const confirm = await screen.findByTestId('manager-delete-modal-confirm'); + await waitFor(() => expect(confirm).not.toHaveAttribute('disabled'), { + timeout: 10000, + }); + await act(() => { + confirm.click(); + }); + + // onSuccess closes the modal (navigate back) — proves the terminate fired. + await waitFor(() => expect(modalDescription()).not.toBeInTheDocument(), { + timeout: 10000, + }); + }); + + it('keeps the modal open when termination fails', async () => { + await renderTest({ + initialRoute: dashboardRoute, + feature: flagOn, + vcdaMigration: vcdaMigrationMock, + deleteServicesKo: true, + }); + + const user = userEvent.setup(); + const cta = await getElementByTestId(TEST_IDS.migrationTerminateCta); + await act(() => user.click(cta)); + + const confirm = await screen.findByTestId('manager-delete-modal-confirm'); + await waitFor(() => expect(confirm).not.toHaveAttribute('disabled'), { + timeout: 10000, + }); + await act(() => { + confirm.click(); + }); + + // KO terminate → onSuccess never fires → the modal stays open for retry. + await act(() => new Promise((resolve) => setTimeout(resolve, 1000))); + expect(modalDescription()).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/_components/TerminateMigrationContent.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/_components/TerminateMigrationContent.component.tsx new file mode 100644 index 000000000000..0d8da3c8ac10 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/organization/migration/terminate/_components/TerminateMigrationContent.component.tsx @@ -0,0 +1,79 @@ +import { useDeleteService } from '@ovh-ux/manager-module-common-api'; +import { DeleteModal } from '@ovh-ux/manager-react-components'; +import { useQueryClient } from '@tanstack/react-query'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { OdsText } from '@ovhcloud/ods-components/react'; +import { useMessageContext } from '@/context/Message.context'; +import { useVcdaMigration } from '@/data/hooks/vcda/useVcdaMigration.hook'; +import { getVcdaStatusQueryKey } from '@/data/hooks/vcda/useVcdaStatus.hook'; +import { getVcdaMigrationQueryKey } from '@/data/hooks/vcda/vcdaQueryKey'; +import { urls } from '@/routes/routes.constant'; +import TEST_IDS from '@/utils/testIds.constants'; + +/** + * VCDA service termination — route-based critical confirmation (mirrors + * `pages/terminate/TerminateOrganization.page`). + * + * `migration.id` is the Agora resourceName: `useDeleteService` resolves it to the + * serviceId (GET /services?resourceName=) before terminating. + */ +export default function TerminateMigrationContent() { + const { id } = useParams(); + const { t } = useTranslation('vcda'); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { addSuccess } = useMessageContext(); + const { data: migration, isLoading: isMigrationLoading } = useVcdaMigration( + id ?? '', + ); + + const closeModal = () => navigate('..'); + + const { terminateService, isPending, error, isError } = useDeleteService({ + onSuccess: () => { + // Both caches must refresh → tile goes to DELETING, the tab unmounts. + void queryClient.invalidateQueries({ + queryKey: getVcdaStatusQueryKey(id ?? ''), + }); + void queryClient.invalidateQueries({ + queryKey: getVcdaMigrationQueryKey(id ?? ''), + }); + addSuccess({ + content: t('serviceTermination.success'), + isDismissible: true, + includedSubRoutes: [id ?? ''], + }); + closeModal(); + }, + }); + + const onConfirmDelete = () => { + if (!migration?.id) return; + terminateService({ resourceName: migration.id }); + }; + + // Termination is only valid in READY; a deep-link in any other state + // redirects to the Dashboard (the tile CTA is the only gated entry point). + if (migration && migration.resourceStatus !== 'READY') { + return ; + } + + return ( + + + {t('serviceTermination.modalDescription')} + + + ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts index 5b1c4d944d4d..39fbaf0b3666 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.constant.ts @@ -19,6 +19,7 @@ export const subRoutes = { migrationOrderTerms: 'terms', migrationAddIp: 'add-ip', migrationDeleteIp: 'delete-ip', + migrationTerminate: 'terminate-migration', resetPassword: 'reset-password', vrackSegments: 'vrack-segments', vrackSegmentId: ':vrackSegmentId', @@ -46,6 +47,7 @@ export const urls = { migrationOrderTerms: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationOrder}/${subRoutes.migrationOrderTerms}`, migrationAddIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationAddIp}`, migrationDeleteIp: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migration}/${subRoutes.migrationDeleteIp}`, + migrationTerminate: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.migrationTerminate}`, editName: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.editName}`, editDescription: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.editDescription}`, datacentres: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}`, diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx index 4b1c257d2a12..b7511d3aa579 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/routes/routes.tsx @@ -55,6 +55,11 @@ const MigrationAddIpPage = React.lazy(() => const MigrationDeleteIpPage = React.lazy(() => import('@/pages/dashboard/organization/migration/delete-ip/DeleteIp.page'), ); +const TerminateMigrationPage = React.lazy(() => + import( + '@/pages/dashboard/organization/migration/terminate/TerminateMigration.page' + ), +); const NetworkAclPage = React.lazy(() => import('@/pages/listing/networkAcl/NetworkAcl.page'), @@ -235,6 +240,17 @@ export default ( }, }} /> +