diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/datacentres/edge-gateway/Messages_fr_FR.json b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/datacentres/edge-gateway/Messages_fr_FR.json new file mode 100644 index 000000000000..75b4a293b03a --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/public/translations/datacentres/edge-gateway/Messages_fr_FR.json @@ -0,0 +1,19 @@ +{ + "edge_add": "Ajouter une Edge Gateway", + "edge_add_tooltip": "Vous pouvez créer {{maxEdge}} Edge Gateway maximum. Vous devez avoir au moins 1 IP Space disponible pour créer une Edge Gateway.", + "edge_ip_block": "IP Space", + "edge_edit_config": "Modifier la configuration", + "edge_add_title": "Nouvelle Edge Gateway", + "edge_add_input_name_label": "Nom de la Edge Gateway", + "edge_add_input_name_helper": "Le nom de la Edge Gateway doit contenir entre {{edgeNameMinLength}} et {{edgeNameMaxLength}} caractères", + "edge_add_input_ip_block_helper": "Vous devez sélectionner un IP Space", + "edge_add_submit": "Créer la Edge Gateway", + "edge_add_banner_success": "La demande de création de votre Edge Gateway a bien été prise en compte.", + "edge_update_banner_success": "La demande de modification de votre Edge Gateway a bien été prise en compte.", + "edge_update_ip_block_helper": "Cet IP Space est alloué automatiquement pour votre Edge Gateway et ne peut pas être modifié.", + "edge_delete_title": "Supprimer la Edge Gateway", + "edge_delete_description": "Voulez-vous vraiment supprimer votre Edge Gateway {{edgeName}} ?", + "edge_delete_warning": "Attention : supprimer votre Edge Gateway entraînera la suppression définitive de tous les services réseau et des configurations. Cette action est irréversible.", + "edge_delete_banner_success": "La demande de suppression de votre {{edgeName}} a été prise en compte.", + "edge_operation_error": "Une erreur est survenue. Veuillez attendre quelques minutes avant de réessayer." +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/app.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/app.constants.ts index 0f046179650c..2d053c003bc5 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/app.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/app.constants.ts @@ -1,4 +1,5 @@ export const FEATURE_FLAGS = { VRACK: 'hpc-vmware-public-vcf-aas:vrack', VRACK_ASSOCIATION: 'hpc-vmware-public-vcf-aas:vrack:association', + EDGE_GATEWAY: 'hpc-vmware-public-vcf-aas:edge-gateway', }; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/InputField.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/InputField.component.tsx new file mode 100644 index 000000000000..ed57e2e45e2e --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/InputField.component.tsx @@ -0,0 +1,60 @@ +import { ControllerRenderProps, FieldValues } from 'react-hook-form'; +import { + OdsFormField, + OdsInput, + OdsSpinner, + OdsText, +} from '@ovhcloud/ods-components/react'; + +type TextFieldValidatorProps = Pick< + HTMLOdsInputElement, + 'minlength' | 'maxlength' | 'pattern' +>; + +export type InputFieldProps = { + field: ControllerRenderProps; + label: string; + error?: string; + isDisabled?: boolean; + isLoading?: boolean; + placeholder?: string; + validator?: TextFieldValidatorProps; +}; + +export const InputField = ({ + field, + label, + error, + isDisabled = false, + isLoading = false, + placeholder, + validator = {}, +}: InputFieldProps) => { + return ( + + +
+ + {isLoading && } +
+ + {validator.maxlength && ( + + {`${field.value?.length || 0}/${validator.maxlength}`} + + )} +
+ ); +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/SelectField.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/SelectField.component.tsx new file mode 100644 index 000000000000..b848e764c439 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/components/form/SelectField.component.tsx @@ -0,0 +1,61 @@ +import { ControllerRenderProps, FieldValues } from 'react-hook-form'; +import { + OdsFormField, + OdsSelect, + OdsSpinner, + OdsText, +} from '@ovhcloud/ods-components/react'; + +type SelectFieldProps = { + field: ControllerRenderProps; + options: string[]; + label: string; + error?: string; + isDisabled?: boolean; + isLoading?: boolean; + placeholder?: string; + helperText?: string; +}; + +export const SelectField = ({ + field, + options = [], + label, + error, + isDisabled = false, + isLoading = false, + placeholder, + helperText, +}: SelectFieldProps) => { + return ( + + +
+ + {options.map((opt) => ( + + ))} + + {isLoading && } +
+ {helperText && ( + + {helperText} + + )} +
+ ); +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/DatacentreDashboard.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/DatacentreDashboard.page.tsx index 02c2b6becabb..ca63e49cc0db 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/DatacentreDashboard.page.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/DatacentreDashboard.page.tsx @@ -11,7 +11,11 @@ import { BreadcrumbItem } from '@/hooks/breadcrumb/useBreadcrumb'; import VcdDashboardLayout, { DashboardTab, } from '@/components/dashboard/layout/VcdDashboardLayout.component'; -import { COMPUTE_LABEL, STORAGE_LABEL } from './datacentreDashboard.constants'; +import { + COMPUTE_LABEL, + EDGE_GATEWAY_LABEL, + STORAGE_LABEL, +} from './datacentreDashboard.constants'; import { subRoutes, urls } from '@/routes/routes.constant'; import { useAutoRefetch } from '@/data/hooks/useAutoRefetch'; import { isUpdatingTargetSpec } from '@/utils/refetchConditions'; @@ -21,16 +25,21 @@ import { VIRTUAL_DATACENTERS_LABEL } from '../organization/organizationDashboard import { VRACK_LABEL } from '../dashboard.constants'; import { FEATURE_FLAGS } from '@/app.constants'; import MessageSuspendedService from '@/components/message/MessageSuspendedService.component'; +// import { isEdgeCompatibleVDC } from '@/utils/edgeGatewayCompatibility'; // TODO: [EDGE] implement when unmocking (testing only) function DatacentreDashboardPage() { const { id, vdcId } = useParams(); - const { t } = useTranslation('dashboard'); + const { t } = useTranslation(['dashboard', 'datacentres/edge-gateway']); const { data: vcdDatacentre } = useVcdDatacentre(id, vdcId); const { data: vcdOrganization } = useVcdOrganization({ id }); const { data: featuresAvailable } = useFeatureAvailability([ FEATURE_FLAGS.VRACK, + FEATURE_FLAGS.EDGE_GATEWAY, ]); const isVrackFeatureAvailable = featuresAvailable?.[FEATURE_FLAGS.VRACK]; + const isEdgeFeatureAvailable = + featuresAvailable?.[FEATURE_FLAGS.EDGE_GATEWAY]; + const navigate = useNavigate(); useAutoRefetch({ @@ -66,6 +75,14 @@ function DatacentreDashboardPage() { disabled: !isVrackFeatureAvailable || !vcdDatacentre?.data?.currentState?.vrack, }, + { + name: 'edge-gateway', + title: EDGE_GATEWAY_LABEL, + to: useResolvedPath(subRoutes.edgeGateway).pathname, + trackingActions: TRACKING_TABS_ACTIONS.edgeGateway, + disabled: !isEdgeFeatureAvailable, // TODO: [EDGE] replace by !isEdgeCompatible when unmocking (testing only) + // disabled: !isEdgeCompatibleVDC(vcdDatacentre?.data) // TODO: [EDGE] implement when unmocking (testing only) + }, ].filter(({ disabled }) => !disabled); const serviceName = vcdDatacentre?.data?.currentState?.description ?? vdcId; @@ -123,12 +140,6 @@ function DatacentreDashboardPage() { 'datacentres/vrack-segment:managed_vcd_dashboard_vrack_segment_add_title', ), }, - { - id: subRoutes.edit, - label: t( - 'datacentres/vrack-segment:managed_vcd_dashboard_vrack_edit_vlan', - ), - }, { id: subRoutes.deleteSegment, label: t( @@ -147,6 +158,18 @@ function DatacentreDashboardPage() { 'datacentres/vrack-segment:managed_vcd_dashboard_vrack_segment_add_title', ), }, + { + id: subRoutes.edgeGateway, + label: EDGE_GATEWAY_LABEL, + }, + { + id: subRoutes.addEdgeGateway, + label: t('datacentres/edge-gateway:edge_add_title'), + }, + { + id: subRoutes.deleteEdgeGateway, + label: t('datacentres/edge-gateway:edge_delete_title'), + }, ]; return ( diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/compute/DatacentreCompute.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/compute/DatacentreCompute.spec.tsx index 4fba6824deeb..4686cfc8636c 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/compute/DatacentreCompute.spec.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/compute/DatacentreCompute.spec.tsx @@ -71,7 +71,7 @@ describe('Datacentre Compute Listing Page', () => { testId: TEST_IDS.cellDeleteCta, }); await assertElementVisibility(deleteButton); - expect(deleteButton).toBeDisabled(); + expect(deleteButton).toHaveAttribute('is-disabled', 'true'); const tooltip = await getNthElementByTestId({ testId: TEST_IDS.cellDeleteTooltip, diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/datacentreDashboard.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/datacentreDashboard.constants.ts index 4cfb0de17ed2..71e717f5aa32 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/datacentreDashboard.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/datacentreDashboard.constants.ts @@ -1,2 +1,3 @@ export const COMPUTE_LABEL = 'Compute'; export const STORAGE_LABEL = 'Storage'; +export const EDGE_GATEWAY_LABEL = 'Edge Gateway'; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.page.tsx new file mode 100644 index 000000000000..b3034b92287e --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.page.tsx @@ -0,0 +1,66 @@ +import { + useVcdDatacentre, + useVcdEdgeGatewaysMocks, +} from '@ovh-ux/manager-module-vcd-api'; +import { + Datagrid, + ErrorBanner, + RedirectionGuard, +} from '@ovh-ux/manager-react-components'; +import { OdsText } from '@ovhcloud/ods-components/react'; +import { Outlet, useParams } from 'react-router-dom'; +import { Suspense } from 'react'; +import { urls } from '@/routes/routes.constant'; +import { EDGE_GATEWAY_LABEL } from '../datacentreDashboard.constants'; +import Loading from '@/components/loading/Loading.component'; +import { useEdgeGatewayListingColumns } from './hooks/useEdgeGatewayListingColumns'; +import { EdgeGatewayOrderButton } from './components/EdgeGatewayOrderButton.component'; +import { isEdgeCompatibleVDC } from '@/utils/edgeGatewayCompatibility'; +import { EDGE_GATEWAY_MAX_QUANTITY } from './datacentreEdgeGateway.constants'; + +export default function EdgeGatewayListingPage() { + const { id, vdcId } = useParams(); + const columns = useEdgeGatewayListingColumns(); + const vdcQuery = useVcdDatacentre(id, vdcId); + const edgeQuery = useVcdEdgeGatewaysMocks({ id, vdcId }); + + const queryList = [vdcQuery, edgeQuery]; + const queries = { + isLoading: queryList.some((q) => q.isLoading), + isError: queryList.some((q) => q.isError), + error: queryList.find((q) => q.isError)?.error?.message ?? null, + data: { vdc: vdcQuery?.data?.data, edges: edgeQuery?.data }, + }; + const hasMaxEdges = queries.data?.edges?.length >= EDGE_GATEWAY_MAX_QUANTITY; + + if (queries.isLoading) return ; + if (queries.isError) { + return ( + + ); + } + + return ( + + }> +
+ {EDGE_GATEWAY_LABEL} + + +
+
+ +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.spec.tsx new file mode 100644 index 000000000000..9ad6f1dba9d0 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.spec.tsx @@ -0,0 +1,62 @@ +import { expect, it, describe } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { + organizationList, + datacentreList, + EDGE_GATEWAY_MOCKS, +} from '@ovh-ux/manager-module-vcd-api'; +import { assertTextVisibility } from '@ovh-ux/manager-core-test-utils'; +import { labels, renderTest } from '../../../../test-utils'; +import { EDGE_GATEWAY_LABEL } from '../datacentreDashboard.constants'; +import { urls } from '../../../../routes/routes.constant'; +import TEST_IDS from '../../../../utils/testIds.constants'; + +const config = { + org: organizationList[0], + vdc: datacentreList[0], + edge: EDGE_GATEWAY_MOCKS[0], + labels: { ...labels.datacentresEdgeGateway, ...labels.commonDashboard }, + waitOptions: { timeout: 10_000 }, +}; + +describe('Datacentre Edge Gateway Listing Page', () => { + const initialRoute = urls.edgeGateway + .replace(':id', config.org.id) + .replace(':vdcId', config.vdc.id); + + it('access and display Edge Gateway listing page', async () => { + await renderTest({ initialRoute }); + + // check page title + await assertTextVisibility(EDGE_GATEWAY_LABEL); + + // wait for data to be loaded + await waitFor(() => { + expect( + screen.getByText(config.edge.currentState.edgeGatewayName), + ).toBeVisible(); + }, config.waitOptions); + + // check order button + const orderCta = screen.getByTestId(TEST_IDS.edgeGatewayOrderCta); + expect(orderCta).toBeVisible(); + expect(orderCta).toBeEnabled(); + expect(orderCta).toHaveAttribute('label', config.labels.edge_add); + + // check datagrid columns + const elements = [ + labels.commonDashboard.name, + config.labels.edge_ip_block, + config.edge.currentState.edgeGatewayName, + config.edge.currentState.ipBlock, + ]; + elements.forEach((el) => expect(screen.getByText(el)).toBeVisible()); + }); + + // TODO: [EDGE] remove when unmocking + it.skip('display an error', async () => { + await renderTest({ initialRoute, isEdgeGatewayKO: true }); + + await assertTextVisibility(labels.error.manager_error_page_default); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.page.tsx new file mode 100644 index 000000000000..b9e0941769bd --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.page.tsx @@ -0,0 +1,106 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { + useAddEdgeGateway, + useVcdIpBlocksMocks, +} from '@ovh-ux/manager-module-vcd-api'; +import { Drawer } from '@ovh-ux/manager-react-components'; +import { useMessageContext } from '@/context/Message.context'; +import { AddEdgeForm, EDGE_FORM_SCHEMA } from '@/schemas/edge.schema'; +import { InputField } from '@/components/form/InputField.component'; +import { SelectField } from '@/components/form/SelectField.component'; +import { + EDGE_GATEWAY_NAME_MAX_LENGTH, + EDGE_GATEWAY_NAME_MIN_LENGTH, +} from './adgeEdgeGateway.constants'; + +export default function AddEdgeGatewayPage() { + const { t } = useTranslation('datacentres/edge-gateway'); + const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const { id, vdcId } = useParams(); + const navigate = useNavigate(); + const closeDrawer = () => navigate('..'); + const { addSuccess, addError } = useMessageContext(); + + const { data: ipBlocks, isLoading: isLoadingIpBlocks } = useVcdIpBlocksMocks({ + id, + select: (data) => + data.filter((ip) => ip.resource_status.status === 'AVAILABLE'), + }); + + const { mutate: addEdgeGateway, isPending: isCreating } = useAddEdgeGateway({ + id, + vdcId, + onSettled: closeDrawer, + onSuccess: () => addSuccess({ content: t('edge_add_banner_success') }), + onError: () => addError({ content: t('edge_operation_error') }), + }); + + const { + control, + handleSubmit, + formState: { isValid }, + } = useForm({ + mode: 'onTouched', + resolver: zodResolver(EDGE_FORM_SCHEMA), + defaultValues: { + edgeGatewayName: '', + ipBlock: '', + }, + }); + + const onSubmit = (data: AddEdgeForm) => addEdgeGateway(data); + + return ( + +
+ ( + + )} + /> + ( + b.ipBlock)} + isDisabled={isLoadingIpBlocks || !ipBlocks?.length} + isLoading={isLoadingIpBlocks} + error={fieldState.error && t('edge_add_input_ip_block_helper')} + /> + )} + /> + +
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.spec.tsx new file mode 100644 index 000000000000..2402b6e54c1c --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.spec.tsx @@ -0,0 +1,74 @@ +import { vi, expect, it, describe } from 'vitest'; +import { screen, waitFor, within } from '@testing-library/react'; +import { + organizationList, + datacentreList, +} from '@ovh-ux/manager-module-vcd-api'; +import { labels, renderTest } from '../../../../../test-utils'; +import { urls } from '../../../../../routes/routes.constant'; + +vi.mock('@ovh-ux/manager-react-components', async (ogMod) => { + const actual: typeof import('@ovh-ux/manager-react-components') = await ogMod(); + return { + ...actual, + Drawer: vi.fn( + ({ heading, primaryButtonLabel, secondaryButtonLabel, children }) => ( +
+
{heading}
+
{children}
+
+ + +
+
+ ), + ), + }; +}); + +const config = { + org: organizationList[0], + vdc: datacentreList[0], + labels: { ...labels.datacentresEdgeGateway, ...labels.actions }, + waitOptions: { timeout: 10_000 }, +}; +const content = { + drawerElements: [ + config.labels.edge_add_title, + config.labels.edge_add_input_name_label, + config.labels.edge_ip_block, + ], + drawerButtons: [ + { label: config.labels.edge_add_submit }, + { label: config.labels.cancel }, + ], +}; + +describe('Datacentre Edge Gateway Add Page', () => { + const initialRoute = urls.edgeGatewayAdd + .replace(':id', config.org.id) + .replace(':vdcId', config.vdc.id); + + it('access and display AddEdgeGateway page drawer', async () => { + await renderTest({ initialRoute }); + + // wait for drawer to be loaded + await waitFor(() => { + expect(screen.getByTestId('drawer')).toBeVisible(); + }, config.waitOptions); + + const drawer = screen.getByTestId('drawer'); + + // check drawer content + content.drawerElements.forEach((el) => + expect(within(drawer).getByText(el)).toBeVisible(), + ); + + // check drawer buttons + content.drawerButtons.forEach((btn) => { + expect( + within(drawer).getByRole('button', { name: btn.label }), + ).toBeVisible(); + }); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/adgeEdgeGateway.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/adgeEdgeGateway.constants.ts new file mode 100644 index 000000000000..1ec4bfca127f --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/add/adgeEdgeGateway.constants.ts @@ -0,0 +1,3 @@ +export const EDGE_GATEWAY_NAME_MIN_LENGTH = 1; +export const EDGE_GATEWAY_NAME_MAX_LENGTH = 128; +export const EDGE_GATEWAY_NAME_REGEX = /^[\x20-\x7E]*$/; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayCells.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayCells.component.tsx new file mode 100644 index 000000000000..8a0f8509870c --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayCells.component.tsx @@ -0,0 +1,57 @@ +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { VCDEdgeGateway } from '@ovh-ux/manager-module-vcd-api'; +import { + ActionMenu, + ActionMenuItem, + DataGridTextCell, +} from '@ovh-ux/manager-react-components'; +import { ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHref } from 'react-router-dom'; +import { subRoutes } from '@/routes/routes.constant'; + +export const EdgeGatewayNameCell = (edge: VCDEdgeGateway) => ( + {edge.currentState.edgeGatewayName} +); + +export const EdgeGatewayIPBlockCell = (edge: VCDEdgeGateway) => ( + {edge.currentState.ipBlock} +); + +export const EdgeGatewayActionCell = (edge: VCDEdgeGateway) => { + const { t } = useTranslation([ + 'datacentres/edge-gateway', + NAMESPACES.ACTIONS, + ]); + const id = useId(); + const deleteHref = useHref(`${edge.id}/${subRoutes.deleteEdgeGateway}`); + const editHref = useHref(`${edge.id}/${subRoutes.editEdgeGateway}`); + + const actionMenuItems: ActionMenuItem[] = [ + { + id: 1, + label: t('datacentres/edge-gateway:edge_edit_config'), + isDisabled: false, + onClick: () => {}, + href: editHref, + }, + { + id: 2, + label: t(`${NAMESPACES.ACTIONS}:delete`), + isDisabled: false, + onClick: () => {}, + href: deleteHref, + }, + ]; + + return ( + + ); +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayOrderButton.component.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayOrderButton.component.tsx new file mode 100644 index 000000000000..9eb76a2b4d93 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/components/EdgeGatewayOrderButton.component.tsx @@ -0,0 +1,53 @@ +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; +import { ODS_BUTTON_COLOR } from '@ovhcloud/ods-components'; +import { + OdsButton, + OdsIcon, + OdsText, + OdsTooltip, +} from '@ovhcloud/ods-components/react'; +import { useNavigate } from 'react-router-dom'; +import TEST_IDS from '@/utils/testIds.constants'; +import { subRoutes } from '@/routes/routes.constant'; +import { EDGE_GATEWAY_MAX_QUANTITY } from '../datacentreEdgeGateway.constants'; + +type EdgeGatewayOrderButtonProps = React.HTMLAttributes & { + isOrderDisabled?: boolean; +}; + +export const EdgeGatewayOrderButton = ({ + isOrderDisabled = false, + ...props +}: EdgeGatewayOrderButtonProps) => { + const { t } = useTranslation('datacentres/edge-gateway'); + const tooltipId = useId(); + const navigate = useNavigate(); + + return ( +
+ navigate(subRoutes.addEdgeGateway)} + data-testid={TEST_IDS.edgeGatewayOrderCta} + isDisabled={isOrderDisabled} + /> + + + + {t('edge_add_tooltip', { maxEdge: EDGE_GATEWAY_MAX_QUANTITY })} + + +
+ ); +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/datacentreEdgeGateway.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/datacentreEdgeGateway.constants.ts new file mode 100644 index 000000000000..2c2e9c51a5ae --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/datacentreEdgeGateway.constants.ts @@ -0,0 +1 @@ +export const EDGE_GATEWAY_MAX_QUANTITY = 5; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.page.tsx new file mode 100644 index 000000000000..572111881982 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.page.tsx @@ -0,0 +1,66 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { Modal } from '@ovh-ux/manager-react-components'; +import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components'; +import { OdsText, OdsMessage } from '@ovhcloud/ods-components/react'; +import { + GetEdgeGatewayParams, + useDeleteEdgeGateway, + useVcdEdgeGatewayMocks, +} from '@ovh-ux/manager-module-vcd-api'; +import { useMessageContext } from '@/context/Message.context'; +import { subRoutes } from '@/routes/routes.constant'; + +export default function DeleteEdgeGatewayPage() { + const { t } = useTranslation('datacentres/edge-gateway'); + const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const { id, vdcId, edgeGatewayId } = useParams(); + const navigate = useNavigate(); + const closeModal = () => navigate('..'); + const { addSuccess, addError } = useMessageContext(); + + const edgeParams: GetEdgeGatewayParams = { id, vdcId, edgeGatewayId }; + const { data: edge, isLoading } = useVcdEdgeGatewayMocks(edgeParams); + const edgeName = edge?.currentState.edgeGatewayName; + + const { mutate: deleteEdge, isPending: isDeleting } = useDeleteEdgeGateway({ + ...edgeParams, + onSettled: closeModal, + onSuccess: () => { + addSuccess({ + content: t('edge_delete_banner_success', { edgeName }), + includedSubRoutes: [subRoutes.edgeGateway], + }); + }, + onError: () => { + addError({ + content: t('edge_operation_error'), + includedSubRoutes: [subRoutes.edgeGateway], + }); + }, + }); + + return ( + +
+ {t('edge_delete_description', { edgeName })} + + {t('edge_delete_warning')} + +
+
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.spec.tsx new file mode 100644 index 000000000000..1058cafe6c39 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.spec.tsx @@ -0,0 +1,70 @@ +import { expect, it, describe } from 'vitest'; +import { screen, waitFor, within } from '@testing-library/react'; +import { + organizationList, + datacentreList, + EDGE_GATEWAY_MOCKS, +} from '@ovh-ux/manager-module-vcd-api'; +import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components'; +import { labels, renderTest } from '../../../../../test-utils'; +import { urls } from '../../../../../routes/routes.constant'; + +const config = { + org: organizationList[0], + vdc: datacentreList[0], + edge: EDGE_GATEWAY_MOCKS[0], + labels: { ...labels.datacentresEdgeGateway, ...labels.actions }, + waitOptions: { timeout: 10_000 }, +}; +const content = { + modalElements: [ + config.labels.edge_delete_title, + config.labels.edge_delete_warning, + config.labels.edge_delete_description.replace( + '{{edgeName}}', + config.edge.currentState.edgeGatewayName, + ), + ], + modalButtons: [ + { testId: 'primary-button', label: config.labels.delete }, + { testId: 'secondary-button', label: config.labels.cancel }, + ], +}; + +describe('Datacentre Edge Gateway Delete Page', () => { + const initialRoute = urls.edgeGatewayDelete + .replace(':id', config.org.id) + .replace(':vdcId', config.vdc.id) + .replace(':edgeGatewayId', config.edge.id); + + it('access and display DeleteEdgeGateway page modal', async () => { + await renderTest({ initialRoute }); + + // wait for data to be loaded + await waitFor(() => { + expect( + screen.getByText(config.edge.currentState.edgeGatewayName), + ).toBeVisible(); + }, config.waitOptions); + + // check modal + const modal = screen.getByTestId('modal'); + expect(modal).toBeVisible(); + expect(modal).toHaveAttribute('color', ODS_MODAL_COLOR.critical); + + // check modal content + await waitFor(() => { + content.modalElements.forEach((el) => + expect(within(modal).getByText(el)).toBeVisible(), + ); + }, config.waitOptions); + + // check modal buttons + content.modalButtons.forEach((btn) => { + const element = within(modal).getByTestId(btn.testId); + expect(element).toBeVisible(); + expect(element).toBeEnabled(); + expect(element).toHaveAttribute('label', btn.label); + }); + }); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/edit/EditEdgeGateway.page.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/edit/EditEdgeGateway.page.tsx new file mode 100644 index 000000000000..cc2b59a1a80d --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/edit/EditEdgeGateway.page.tsx @@ -0,0 +1,115 @@ +import { Suspense } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { + GetEdgeGatewayParams, + useUpdateEdgeGateway, + useVcdEdgeGatewayMocks, +} from '@ovh-ux/manager-module-vcd-api'; +import { Drawer } from '@ovh-ux/manager-react-components'; +import { useMessageContext } from '@/context/Message.context'; +import { AddEdgeForm, EDGE_FORM_SCHEMA } from '@/schemas/edge.schema'; +import { InputField } from '@/components/form/InputField.component'; +import { SelectField } from '@/components/form/SelectField.component'; +import { EDGE_GATEWAY_NAME_MAX_LENGTH } from '../add/adgeEdgeGateway.constants'; +import Loading from '@/components/loading/Loading.component'; + +export default function EditEdgeGatewayPage() { + const { t } = useTranslation('datacentres/edge-gateway'); + const { t: tActions } = useTranslation(NAMESPACES.ACTIONS); + const { id, vdcId, edgeGatewayId } = useParams(); + const navigate = useNavigate(); + const closeDrawer = () => navigate('..'); + const { addSuccess, addError } = useMessageContext(); + + const edgeParams: GetEdgeGatewayParams = { id, vdcId, edgeGatewayId }; + const { data: edge, isLoading, refetch } = useVcdEdgeGatewayMocks({ + ...edgeParams, + staleTime: 5000, + }); + + const { + mutate: updateEdgeGateway, + isPending: isUpdating, + } = useUpdateEdgeGateway({ + ...edgeParams, + onSettled: closeDrawer, + onSuccess: () => addSuccess({ content: t('edge_update_banner_success') }), + onError: () => addError({ content: t('edge_operation_error') }), + }); + + const { + control, + handleSubmit, + formState: { isValid }, + } = useForm({ + mode: 'onTouched', + resolver: zodResolver(EDGE_FORM_SCHEMA), + defaultValues: async () => { + try { + const data = await refetch(); + if (!data?.data) throw new Error('Cannot access Edge data.'); + return { + edgeGatewayName: data.data.currentState.edgeGatewayName, + ipBlock: data.data.currentState.ipBlock, + }; + } catch (error) { + return { edgeGatewayName: '', ipBlock: '' }; + } + }, + }); + + const onSubmit = (data: AddEdgeForm) => + updateEdgeGateway({ edgeGatewayNewName: data.edgeGatewayName }); + + return ( + }> + +
+ ( + + )} + /> + ( + + )} + /> + +
+
+ ); +} diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/hooks/useEdgeGatewayListingColumns.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/hooks/useEdgeGatewayListingColumns.ts new file mode 100644 index 000000000000..7582fe56bcef --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/edge-gateway/hooks/useEdgeGatewayListingColumns.ts @@ -0,0 +1,37 @@ +import { VCDEdgeGateway } from '@ovh-ux/manager-module-vcd-api'; +import { DatagridColumn } from '@ovh-ux/manager-react-components'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { + EdgeGatewayNameCell, + EdgeGatewayIPBlockCell, + EdgeGatewayActionCell, +} from '../components/EdgeGatewayCells.component'; + +export const useEdgeGatewayListingColumns = () => { + const { t } = useTranslation('datacentres/edge-gateway'); + const { t: tCommonDashboard } = useTranslation(NAMESPACES.DASHBOARD); + + const columns: Array> = [ + { + id: 'name', + cell: EdgeGatewayNameCell, + label: tCommonDashboard('name'), + isSortable: false, + }, + { + id: 'ip_block', + cell: EdgeGatewayIPBlockCell, + label: t('edge_ip_block'), + isSortable: false, + }, + { + id: 'actions', + cell: EdgeGatewayActionCell, + label: '', + isSortable: false, + }, + ]; + + return columns; +}; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/add-network/AddNetworkInVrackSegment.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/add-network/AddNetworkInVrackSegment.spec.tsx index 98f89c95426c..57310c794877 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/add-network/AddNetworkInVrackSegment.spec.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/add-network/AddNetworkInVrackSegment.spec.tsx @@ -6,7 +6,7 @@ import { } from '@ovh-ux/manager-module-vcd-api'; import { waitFor } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; -import { renderTest } from '../../../../../test-utils'; +import { labels, renderTest } from '../../../../../test-utils'; import fr_FR from '../../../../../../public/translations/datacentres/vrack-segment/Messages_fr_FR.json'; const checkTitleIsVisible = () => @@ -23,7 +23,10 @@ const checkFormInputAndCta = (container: HTMLElement) => { `[label="${fr_FR.managed_vcd_dashboard_vrack_add_network}"]`, ), ).toBeVisible(); - expect(container.querySelector('[label="cancel"]')).toBeVisible(); + + const cancelButton = screen.getByTestId('secondary-button'); + expect(cancelButton).toBeVisible(); + expect(cancelButton).toHaveAttribute('label', labels.actions.cancel); const input = container.querySelector('input[name="network"]'); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/edit/EditVrackSegmentId.spec.tsx b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/edit/EditVrackSegmentId.spec.tsx index 08f47289c93f..17d9d4094267 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/edit/EditVrackSegmentId.spec.tsx +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/pages/dashboard/datacentre/vrack-segment/edit/EditVrackSegmentId.spec.tsx @@ -7,7 +7,7 @@ import { } from '@ovh-ux/manager-module-vcd-api'; import { waitFor } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; -import { renderTest } from '../../../../../test-utils'; +import { labels, renderTest } from '../../../../../test-utils'; import fr_FR from '../../../../../../public/translations/datacentres/vrack-segment/Messages_fr_FR.json'; const queryModalTitle = () => { @@ -23,8 +23,15 @@ const checkFormInputAndCta = (container: HTMLElement) => { expect( screen.getByText(fr_FR.managed_vcd_dashboard_vrack_edit_vlan), ).toBeVisible(); - expect(container.querySelector('[label="modify"]')).toBeVisible(); - expect(container.querySelector('[label="cancel"]')).toBeVisible(); + + [ + { testId: 'primary-button', label: labels.actions.modify }, + { testId: 'secondary-button', label: labels.actions.cancel }, + ].forEach((btn) => { + const element = screen.getByTestId(btn.testId); + expect(element).toBeVisible(); + expect(element).toHaveAttribute('label', btn.label); + }); const input = container.querySelector('input[name="vlanId"]'); @@ -39,16 +46,8 @@ const checkVlanValue = (container: HTMLElement, vlanId: string) => { expect(input).toBeInTheDocument(); }; -const expectSubmitButton = (container) => - expect(container.querySelector('ods-button[label="modify"]')); - -const submitForm = (container: HTMLElement) => { - return act(() => - userEvent.click( - container.querySelector('ods-button[label="modify"]') as Element, - ), - ); -}; +const submitForm = async () => + act(() => userEvent.click(screen.getByTestId('primary-button'))); const editVlanValue = (newValue: string | number) => { const odsQuantity = document.querySelector( @@ -104,22 +103,28 @@ describe('Edit Vrack Segment Id Page', () => { { timeout: 2000 }, ); - expectSubmitButton(container).toBeDisabled(); + const submitCta = screen.getByTestId('primary-button'); + expect(submitCta).toBeVisible(); + expect(submitCta).toHaveAttribute('label', labels.actions.modify); + expect(submitCta).toHaveAttribute('is-disabled', 'true'); await editVlanValue(430); - await waitFor(() => expectSubmitButton(container).not.toBeDisabled(), { - timeout: 2000, - }); + await waitFor( + () => expect(submitCta).toHaveAttribute('is-disabled', 'false'), + { + timeout: 2000, + }, + ); - await submitForm(container); + await submitForm(); await waitFor( () => { expect(queryModalTitle()).not.toBeInTheDocument(); checkSuccessBannerIsVisible(); }, - { timeout: 2000 }, + { timeout: 10_000 }, ); }); @@ -141,14 +146,14 @@ describe('Edit Vrack Segment Id Page', () => { { timeout: 2000 }, ); - await submitForm(container); + await submitForm(); await waitFor( () => { expect(queryModalTitle()).toBeInTheDocument(); checkErrorBannerIsVisible(); }, - { timeout: 2000 }, + { timeout: 10_000 }, ); }); 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 e9addc1b5d94..567d396751a3 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 @@ -18,6 +18,11 @@ export const subRoutes = { addNetwork: 'add-network', deleteSegment: 'delete-segment', deleteNetwork: 'delete-network', + edgeGateway: 'edge-gateway', + edgeGatewayId: ':edgeGatewayId', + addEdgeGateway: 'add-edge-gateway', + editEdgeGateway: 'edit-edge-gateway', + deleteEdgeGateway: 'delete-edge-gateway', terminate: 'terminate', } as const; @@ -42,6 +47,10 @@ export const urls = { vrackSegmentEditVlanId: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.vrackSegments}/${subRoutes.vrackSegmentId}/${subRoutes.edit}`, vrackSegmentAddNetwork: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.vrackSegments}/${subRoutes.vrackSegmentId}/${subRoutes.addNetwork}`, vrackSegmentDeleteNetwork: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.vrackSegments}/${subRoutes.vrackSegmentId}/${subRoutes.vrackNetwork}/${subRoutes.vrackNetworkId}/${subRoutes.deleteNetwork}`, + edgeGateway: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.edgeGateway}`, + edgeGatewayAdd: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.edgeGateway}/${subRoutes.addEdgeGateway}`, + edgeGatewayEdit: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.edgeGateway}/${subRoutes.edgeGatewayId}/${subRoutes.editEdgeGateway}`, + edgeGatewayDelete: `/${subRoutes.root}/${subRoutes.dashboard}/${subRoutes.virtualDatacenters}/${subRoutes.vdcId}/${subRoutes.edgeGateway}/${subRoutes.edgeGatewayId}/${subRoutes.deleteEdgeGateway}`, } as const; export const veeamBackupAppName = 'veeam-backup'; 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 08158cc7ba0a..3ef3fc51bf75 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 @@ -96,6 +96,26 @@ const DeleteVrackSegmentPage = React.lazy(() => ), ); +const EdgeGatewayListingPage = React.lazy(() => + import( + '@/pages/dashboard/datacentre/edge-gateway/DatacentreEdgeGateway.page' + ), +); + +const EdgeGatewayAddPage = React.lazy(() => + import('@/pages/dashboard/datacentre/edge-gateway/add/AddEdgeGateway.page'), +); + +const EdgeGatewayEditPage = React.lazy(() => + import('@/pages/dashboard/datacentre/edge-gateway/edit/EditEdgeGateway.page'), +); + +const EdgeGatewayDeletePage = React.lazy(() => + import( + '@/pages/dashboard/datacentre/edge-gateway/delete/DeleteEdgeGateway.page' + ), +); + const TerminateOrganizationPage = React.lazy(() => import('@/pages/terminate/TerminateOrganization.page'), ); @@ -310,6 +330,27 @@ export default ( }, }} /> + + + + + ; 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 c5f0619fbd0d..a8041563deca 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 @@ -1,4 +1,3 @@ -import React from 'react'; import { SetupServer } from 'msw/node'; import { i18n } from 'i18next'; import { I18nextProvider } from 'react-i18next'; @@ -25,6 +24,10 @@ import { getIamMocks, getVrackSegmentsMocks, GetVrackSegmentsMocksParams, + getEdgeGatewayMocks, + GetEdgeGatewayMocksParams, + GetIpBlockMocksParams, + getIpBlockMocks, } from '@ovh-ux/manager-module-vcd-api'; import { initTestI18n, @@ -58,6 +61,8 @@ export const renderTest = async ({ GetVeeamBackupMocksParams & TFeatureAvailabilityMockParams & GetVrackSegmentsMocksParams & + GetEdgeGatewayMocksParams & + GetIpBlockMocksParams & GetServicesMocksParams = {}) => { ((global as unknown) as { server: SetupServer }).server?.resetHandlers( ...toMswHandlers([ @@ -70,6 +75,8 @@ export const renderTest = async ({ ...getServicesMocks(mockParams), ...getVrackSegmentsMocks(mockParams), ...getFeatureAvailabilityMocks(mockParams), + ...getEdgeGatewayMocks(mockParams), + ...getIpBlockMocks(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 da5155576bdf..1cb65984d49e 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 @@ -1,3 +1,6 @@ +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import commonDashboard from '@ovh-ux/manager-common-translations/dist/@ovh-ux/manager-common-translations/dashboard/Messages_fr_FR.json'; +import actions from '@ovh-ux/manager-common-translations/dist/@ovh-ux/manager-common-translations/actions/Messages_fr_FR.json'; import listing from '../../public/translations/listing/Messages_fr_FR.json'; import dashboard from '../../public/translations/dashboard/Messages_fr_FR.json'; import onboarding from '../../public/translations/onboarding/Messages_fr_FR.json'; @@ -6,6 +9,7 @@ import datacentresCompute from '../../public/translations/datacentres/compute/Me import datacentresOrder from '../../public/translations/datacentres/order/Messages_fr_FR.json'; import datacentresStorage from '../../public/translations/datacentres/storage/Messages_fr_FR.json'; import datacentresVrackSegment from '../../public/translations/datacentres/vrack-segment/Messages_fr_FR.json'; +import datacentresEdgeGateway from '../../public/translations/datacentres/edge-gateway/Messages_fr_FR.json'; import terminate from '../../public/translations/terminate/Messages_fr_FR.json'; const error = { @@ -27,8 +31,11 @@ export const translations = { 'datacentres/order': datacentresOrder, 'datacentres/storage': datacentresStorage, 'datacentres/vrack-segment': datacentresVrackSegment, + 'datacentres/edge-gateway': datacentresEdgeGateway, error, terminate, + [NAMESPACES.ACTIONS]: actions, + [NAMESPACES.DASHBOARD]: commonDashboard, }; export const labels = { @@ -41,5 +48,8 @@ export const labels = { datacentresOrder, datacentresStorage, datacentresVrackSegment, + datacentresEdgeGateway, terminate, + actions, + commonDashboard, }; diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/tracking.constants.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/tracking.constants.ts index 760061775c43..8878f7a2fdc2 100644 --- a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/tracking.constants.ts +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/tracking.constants.ts @@ -301,7 +301,7 @@ export const TRACKING_TABS_ACTIONS = { compute: ['compute'], storage: ['storage'], vrack: ['vrack'], - edgeGateways: ['edge-gateways'], + edgeGateway: ['edge-gateway'], }; export const getTabTrackingParams = ( diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.spec.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.spec.ts new file mode 100644 index 000000000000..0401d19b2266 --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { + VCDDatacentre, + VCDDatacentreState, + datacentreList, +} from '@ovh-ux/manager-module-vcd-api'; +import { isEdgeCompatibleVDC } from './edgeGatewayCompatibility'; + +describe('isEdgeCompatibleVDC test suite', () => { + const mockVDC = datacentreList[0]; + + const testCases: { + offerProfile: VCDDatacentreState['offerProfile']; + expected: boolean; + }[] = [ + { offerProfile: 'NSX', expected: true }, + { offerProfile: 'STANDARD', expected: false }, + { offerProfile: undefined, expected: false }, + ]; + + it.each(testCases)( + 'return $expected if offerProfile is $offerProfile', + ({ expected, offerProfile }) => { + const testVDC: VCDDatacentre = { + ...mockVDC, + currentState: { ...mockVDC.currentState, offerProfile }, + }; + + expect(isEdgeCompatibleVDC(testVDC)).toBe(expected); + }, + ); +}); diff --git a/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.ts b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.ts new file mode 100644 index 000000000000..4c9a77349c1c --- /dev/null +++ b/packages/manager/apps/hpc-vmware-public-vcf-aas/src/utils/edgeGatewayCompatibility.ts @@ -0,0 +1,4 @@ +import { VCDDatacentre } from '@ovh-ux/manager-module-vcd-api'; + +export const isEdgeCompatibleVDC = (vdc: VCDDatacentre) => + vdc?.currentState?.offerProfile === 'NSX'; 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 65d6842f03d6..7d338c583b20 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 @@ -17,6 +17,7 @@ const TEST_IDS = { backupBadgeError: 'backup-badge-error', backupBadgeNone: 'backup-badge-none', vcdOrderCta: 'vcd-order-cta', + edgeGatewayOrderCta: 'edge-gateway-order-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 45d99ad8b914..d124425d4dc4 100644 --- a/packages/manager/modules/vcd-api/src/api/index.ts +++ b/packages/manager/modules/vcd-api/src/api/index.ts @@ -1,5 +1,7 @@ export * from './catalog'; export * from './vcd-datacentre'; +export * from './vcd-edge-gateway'; +export * from './vcd-ip-block'; export * from './vcd-organization'; export * from './vcd-reset-password'; export * from './veeam-backup'; diff --git a/packages/manager/modules/vcd-api/src/api/vcd-edge-gateway.ts b/packages/manager/modules/vcd-api/src/api/vcd-edge-gateway.ts new file mode 100644 index 000000000000..201162aabae1 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/api/vcd-edge-gateway.ts @@ -0,0 +1,66 @@ +import { v2 } from '@ovh-ux/manager-core-api'; +import { + GetEdgeGatewayParams, + UpdateEdgeGatewayPayload, + VCDEdgeGateway, + VCDEdgeGatewayState, +} from '../types'; +import { getVcdEdgeGatewayListRoute, getVcdEdgeGatewayRoute } from '../utils'; + +export type AddEdgeGatewayParams = { + id: string; + vdcId: string; + payload: VCDEdgeGatewayState; +}; +export type UpdateEdgeGatewayParams = GetEdgeGatewayParams & { + payload: UpdateEdgeGatewayPayload; +}; + +export const createVcdEdgeGateway = async ({ + id, + vdcId, + payload, +}: AddEdgeGatewayParams): Promise => { + const { data } = await v2.put( + getVcdEdgeGatewayListRoute(id, vdcId), + payload, + ); + return data; +}; + +export const getVcdEdgeGateways = async ( + id: string, + vdcId: string, +): Promise => { + const { data } = await v2.get( + getVcdEdgeGatewayListRoute(id, vdcId), + ); + return data; +}; + +export const getVcdEdgeGateway = async ( + params: GetEdgeGatewayParams, +): Promise => { + const { data } = await v2.get(getVcdEdgeGatewayRoute(params)); + return data; +}; + +export const updateVcdEdgeGateway = async ({ + payload, + ...params +}: UpdateEdgeGatewayParams): Promise => { + const { data } = await v2.put( + getVcdEdgeGatewayRoute(params), + payload, + ); + return data; +}; + +export const deleteVcdEdgeGateway = async ( + params: GetEdgeGatewayParams, +): Promise => { + const { data } = await v2.delete( + getVcdEdgeGatewayRoute(params), + ); + return data; +}; diff --git a/packages/manager/modules/vcd-api/src/api/vcd-ip-block.ts b/packages/manager/modules/vcd-api/src/api/vcd-ip-block.ts new file mode 100644 index 000000000000..48e027a35940 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/api/vcd-ip-block.ts @@ -0,0 +1,8 @@ +import { v2 } from '@ovh-ux/manager-core-api'; +import { getVcdIpBlockListRoute } from '../utils'; +import { VCDIpBlock } from '../types'; + +export const getVcdIpBlocks = async (id: string): Promise => { + const { data } = await v2.get(getVcdIpBlockListRoute(id)); + return data; +}; diff --git a/packages/manager/modules/vcd-api/src/hook/index.ts b/packages/manager/modules/vcd-api/src/hook/index.ts index 10b9bbf9a15d..667c73710b30 100644 --- a/packages/manager/modules/vcd-api/src/hook/index.ts +++ b/packages/manager/modules/vcd-api/src/hook/index.ts @@ -1,11 +1,16 @@ +export * from './useAddEdgeGateway'; +export * from './useDeleteEdgeGateway'; export * from './useDeleteVcdVrackSegment'; export * from './useOrderableResource'; export * from './useResetVcdPassword'; +export * from './useUpdateEdgeGateway'; export * from './useUpdateVcdDatacentre'; export * from './useUpdateVcdOrganization'; export * from './useUpdateVcdVrackSegment'; export * from './useVcdCatalog'; export * from './useVcdDatacentre'; +export * from './useVcdEdgeGateway'; +export * from './useVcdIpBlocks'; export * from './useVcdOrder'; export * from './useVcdOrganization'; export * from './useVeeamBackup'; diff --git a/packages/manager/modules/vcd-api/src/hook/useAddEdgeGateway.ts b/packages/manager/modules/vcd-api/src/hook/useAddEdgeGateway.ts new file mode 100644 index 000000000000..0dfb26af4f51 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/hook/useAddEdgeGateway.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createVcdEdgeGateway } from '../api'; +import { + GetVCDDatacentreParams, + RestrictedMutationOptions, + VCDEdgeGateway, + VCDEdgeGatewayState, +} from '../types'; +import { getVcdEdgeGatewayListQueryKey } from '../utils'; + +type UseAddEdgeGatewayParams = GetVCDDatacentreParams & + RestrictedMutationOptions; + +export const useAddEdgeGateway = ({ + id, + vdcId, + onSuccess, + ...options +}: UseAddEdgeGatewayParams) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: VCDEdgeGatewayState) => + createVcdEdgeGateway({ id, vdcId, payload }), + ...options, + onSuccess: (...params) => { + queryClient.invalidateQueries({ + queryKey: getVcdEdgeGatewayListQueryKey(id, vdcId), + }); + onSuccess?.(...params); + }, + }); +}; diff --git a/packages/manager/modules/vcd-api/src/hook/useDeleteEdgeGateway.ts b/packages/manager/modules/vcd-api/src/hook/useDeleteEdgeGateway.ts new file mode 100644 index 000000000000..02ad96ac3503 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/hook/useDeleteEdgeGateway.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { getVcdEdgeGatewayListQueryKey } from '../utils'; +import { + GetEdgeGatewayParams, + RestrictedMutationOptions, + VCDEdgeGateway, +} from '../types'; +import { deleteVcdEdgeGateway } from '../api'; + +type UseDeleteEdgeGatewayParams = GetEdgeGatewayParams & + RestrictedMutationOptions; + +export const useDeleteEdgeGateway = ({ + id, + vdcId, + edgeGatewayId, + onSuccess, + ...options +}: UseDeleteEdgeGatewayParams) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => deleteVcdEdgeGateway({ id, vdcId, edgeGatewayId }), + ...options, + onSuccess: (...params) => { + queryClient.invalidateQueries({ + queryKey: getVcdEdgeGatewayListQueryKey(id, vdcId), + }); + onSuccess?.(...params); + }, + }); +}; diff --git a/packages/manager/modules/vcd-api/src/hook/useUpdateEdgeGateway.ts b/packages/manager/modules/vcd-api/src/hook/useUpdateEdgeGateway.ts new file mode 100644 index 000000000000..b1f0faf16655 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/hook/useUpdateEdgeGateway.ts @@ -0,0 +1,34 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { updateVcdEdgeGateway } from '../api'; +import { + GetEdgeGatewayParams, + RestrictedMutationOptions, + UpdateEdgeGatewayPayload, + VCDEdgeGateway, +} from '../types'; +import { getVcdEdgeGatewayListQueryKey } from '../utils'; + +type UseUpdateEdgeGatewayParams = GetEdgeGatewayParams & + RestrictedMutationOptions; + +export const useUpdateEdgeGateway = ({ + id, + vdcId, + edgeGatewayId, + onSuccess, + ...options +}: UseUpdateEdgeGatewayParams) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: UpdateEdgeGatewayPayload) => + updateVcdEdgeGateway({ id, vdcId, edgeGatewayId, payload }), + ...options, + onSuccess: (...params) => { + queryClient.invalidateQueries({ + queryKey: getVcdEdgeGatewayListQueryKey(id, vdcId), + }); + onSuccess?.(...params); + }, + }); +}; diff --git a/packages/manager/modules/vcd-api/src/hook/useVcdEdgeGateway.ts b/packages/manager/modules/vcd-api/src/hook/useVcdEdgeGateway.ts new file mode 100644 index 000000000000..863234172ab2 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/hook/useVcdEdgeGateway.ts @@ -0,0 +1,79 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { + GetEdgeGatewayParams, + GetVCDDatacentreParams, + VCDEdgeGateway, +} from '../types'; +import { getVcdEdgeGateway, getVcdEdgeGateways } from '../api'; +import { EDGE_GATEWAY_MOCKS } from '../mocks'; +import { + getVcdEdgeGatewayListQueryKey, + getVcdEdgeGatewayQueryKey, +} from '../utils'; +import { RestrictedQueryOptions } from '../types/query-options'; + +type UseVcdEdgeGatewayListParams = GetVCDDatacentreParams & + RestrictedQueryOptions; + +type UseVcdEdgeGatewayParams = GetEdgeGatewayParams & + RestrictedQueryOptions; + +export const useVcdEdgeGateways = ({ + id, + vdcId, + ...options +}: UseVcdEdgeGatewayListParams): UseQueryResult => + useQuery({ + queryKey: getVcdEdgeGatewayListQueryKey(id, vdcId), + queryFn: () => getVcdEdgeGateways(id, vdcId), + ...options, + }); + +// TODO: [EDGE] remove when unmocking +export const useVcdEdgeGatewaysMocks = ({ + id, + vdcId, + ...options +}: UseVcdEdgeGatewayListParams): UseQueryResult => + useQuery({ + queryKey: getVcdEdgeGatewayListQueryKey(id, vdcId), + queryFn: () => + new Promise((resolve) => { + console.log('🛜 mocking useVcdEdgeGateways api call...'); + setTimeout(() => { + resolve(EDGE_GATEWAY_MOCKS); + }, 2_000); + }), + ...options, + }); + +export const useVcdEdgeGateway = ({ + id, + vdcId, + edgeGatewayId, + ...options +}: UseVcdEdgeGatewayParams): UseQueryResult => + useQuery({ + queryKey: getVcdEdgeGatewayQueryKey({ id, vdcId, edgeGatewayId }), + queryFn: () => getVcdEdgeGateway({ id, vdcId, edgeGatewayId }), + ...options, + }); + +// TODO: [EDGE] remove when unmocking +export const useVcdEdgeGatewayMocks = ({ + id, + vdcId, + edgeGatewayId, + ...options +}: UseVcdEdgeGatewayParams): UseQueryResult => + useQuery({ + queryKey: getVcdEdgeGatewayQueryKey({ id, vdcId, edgeGatewayId }), + queryFn: () => + new Promise((resolve) => { + console.log('🛜 mocking useVcdEdgeGateway api call...'); + setTimeout(() => { + resolve(EDGE_GATEWAY_MOCKS.find((edge) => edge.id === edgeGatewayId)); + }, 2_000); + }), + ...options, + }); diff --git a/packages/manager/modules/vcd-api/src/hook/useVcdIpBlocks.ts b/packages/manager/modules/vcd-api/src/hook/useVcdIpBlocks.ts new file mode 100644 index 000000000000..50432b5a41a0 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/hook/useVcdIpBlocks.ts @@ -0,0 +1,40 @@ +import { + useQuery, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; +import { VCDIpBlock } from '../types'; +import { getVcdIpBlocks } from '../api'; +import { getVcdIpBlockListQueryKey } from '../utils'; +import { IP_BLOCK_MOCKS } from '../mocks/vcd-organization/vcd-ip-block.mock'; + +type UseVcdIpBlockParams = { + id: string; +} & Omit, 'queryKey' | 'queryFn'>; + +export const useVcdIpBlocks = ({ + id, + ...options +}: UseVcdIpBlockParams): UseQueryResult => + useQuery({ + queryKey: getVcdIpBlockListQueryKey(id), + queryFn: () => getVcdIpBlocks(id), + ...options, + }); + +// TODO: [EDGE] remove when unmocking +export const useVcdIpBlocksMocks = ({ + id, + ...options +}: UseVcdIpBlockParams): UseQueryResult => + useQuery({ + queryKey: getVcdIpBlockListQueryKey(id), + queryFn: () => + new Promise((resolve) => { + console.log('🛜 mocking useVcdIpBlocks api call...'); + setTimeout(() => { + resolve(IP_BLOCK_MOCKS); + }, 2_000); + }), + ...options, + }); diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/index.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/index.ts index 77724ef0beb6..4c22143c47f7 100644 --- a/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/index.ts +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/index.ts @@ -6,3 +6,5 @@ export * from './vdc-orderable-resource.mock'; export * from './vdc-storage.mock'; export * from './vcd-datacentre-vrack-segment.handler'; export * from './vcd-datacentre-vrack-segment.mock'; +export * from './vdc-edge-gateway.mock'; +export * from './vdc-edge-gateway.handler'; diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.handler.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.handler.ts new file mode 100644 index 000000000000..f149d224b47f --- /dev/null +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.handler.ts @@ -0,0 +1,22 @@ +import { Handler } from '@ovh-ux/manager-core-test-utils'; +import { EDGE_GATEWAY_MOCKS } from './vdc-edge-gateway.mock'; + +export type GetEdgeGatewayMocksParams = { + isEdgeGatewayKO?: boolean; + nbEdgeGateway?: number; +}; + +export const getEdgeGatewayMocks = ({ + isEdgeGatewayKO, + nbEdgeGateway = Number.POSITIVE_INFINITY, +}: GetEdgeGatewayMocksParams): Handler[] => [ + { + url: + '/vmwareCloudDirector/organization/:id/virtualDataCenter/:id/edgeGateway', + response: isEdgeGatewayKO + ? { message: 'edgeGateway error' } + : EDGE_GATEWAY_MOCKS.slice(0, nbEdgeGateway), + api: 'v2', + status: isEdgeGatewayKO ? 500 : 200, + }, +]; diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.mock.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.mock.ts new file mode 100644 index 000000000000..6dc3ae66f1b5 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-datacentre/vdc-edge-gateway.mock.ts @@ -0,0 +1,43 @@ +import { VCDEdgeGateway } from '../../types'; + +export const EDGE_GATEWAY_MOCKS: VCDEdgeGateway[] = [ + { + id: 'c96b1c7c1b910', + targetSpec: { + edgeGatewayName: 'EdgeGW-A', + ipBlock: '111.111.111.111/42', + }, + currentState: { + edgeGatewayName: 'EdgeGW-A', + ipBlock: '111.111.111.111/42', + }, + currentTasks: [], + resourceStatus: 'READY', + }, + { + id: 'b2d8dded34510', + targetSpec: { + edgeGatewayName: 'EdgeGW-B', + ipBlock: '222.222.222.222/42', + }, + currentState: { + edgeGatewayName: 'EdgeGW-B', + ipBlock: '222.222.222.222/42', + }, + currentTasks: [], + resourceStatus: 'CREATING', + }, + { + id: 'b2d8dded1c4', + targetSpec: { + edgeGatewayName: 'EdgeGW-C', + ipBlock: '333.333.333.333/42', + }, + currentState: { + edgeGatewayName: 'EdgeGW-C', + ipBlock: '333.333.333.333/42', + }, + currentTasks: [], + resourceStatus: 'DELETING', + }, +]; diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-organization/index.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/index.ts index e6d96900556a..8071c8584d14 100644 --- a/packages/manager/modules/vcd-api/src/mocks/vcd-organization/index.ts +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/index.ts @@ -1,3 +1,5 @@ export * from './vcd-catalog.mock'; +export * from './vcd-ip-block.handler'; +export * from './vcd-ip-block.mock'; export * from './vcd-organization.handler'; export * from './vcd-organization.mock'; diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.handler.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.handler.ts new file mode 100644 index 000000000000..b80205c28067 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.handler.ts @@ -0,0 +1,21 @@ +import { Handler } from '@ovh-ux/manager-core-test-utils'; +import { IP_BLOCK_MOCKS } from './vcd-ip-block.mock'; + +export type GetIpBlockMocksParams = { + isIpBlockKO?: boolean; + nbIpBlock?: number; +}; + +export const getIpBlockMocks = ({ + isIpBlockKO, + nbIpBlock = Number.POSITIVE_INFINITY, +}: GetIpBlockMocksParams): Handler[] => [ + { + url: '/vmwareCloudDirector/organization/:id/ipblock', + response: isIpBlockKO + ? { message: 'ipBlock error' } + : IP_BLOCK_MOCKS.slice(0, nbIpBlock), + api: 'v2', + status: isIpBlockKO ? 500 : 200, + }, +]; diff --git a/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.mock.ts b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.mock.ts new file mode 100644 index 000000000000..37fe9b4ed118 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/mocks/vcd-organization/vcd-ip-block.mock.ts @@ -0,0 +1,48 @@ +import { VCDIpBlock } from '../../types'; + +export const IP_BLOCK_MOCKS: VCDIpBlock[] = [ + { + ipBlockID: 'c96b1c7c1b91000', + ipBlock: '123.123.123.123/24', + destination: {}, + resource_status: { + status: 'AVAILABLE', + unroutable: true, + unroutable_reason: 'ok', + }, + }, + { + ipBlockID: 'b2d8dded3451000', + ipBlock: '234.234.234.234/42', + destination: {}, + resource_status: { + status: 'AVAILABLE', + unroutable: true, + unroutable_reason: 'ok', + }, + }, + { + ipBlockID: 'e2e5f8d7000', + ipBlock: '345.345.345.345/42', + destination: { + edgeGatewayName: 'EdgeGW-A', + }, + resource_status: { + status: 'READY', + unroutable: false, + unroutable_reason: 'used by customer resources', + }, + }, + { + ipBlockID: 'b2d8dded1c4000', + ipBlock: '456.456.456.456/42', + destination: { + edgeGatewayName: 'EdgeGW-B', + }, + resource_status: { + status: 'READY', + unroutable: false, + unroutable_reason: 'used by customer resources', + }, + }, +]; diff --git a/packages/manager/modules/vcd-api/src/types/index.ts b/packages/manager/modules/vcd-api/src/types/index.ts index ecdd7559c830..3dab6871618f 100644 --- a/packages/manager/modules/vcd-api/src/types/index.ts +++ b/packages/manager/modules/vcd-api/src/types/index.ts @@ -1,8 +1,11 @@ +export * from './query-options'; export * from './veeam-backup.type'; export * from './veeam-backup-catalog.type'; export * from './vcd-catalog.type'; export * from './vcd-compute.type'; export * from './vcd-datacentre.type'; +export * from './vcd-edge-gateway.type'; +export * from './vcd-ip-block.type'; export * from './vcd-order.type'; export * from './vcd-orderable-resource.type'; export * from './vcd-organization.type'; diff --git a/packages/manager/modules/vcd-api/src/types/query-options.ts b/packages/manager/modules/vcd-api/src/types/query-options.ts new file mode 100644 index 000000000000..2c7be42ebe0f --- /dev/null +++ b/packages/manager/modules/vcd-api/src/types/query-options.ts @@ -0,0 +1,12 @@ +import { ApiError } from '@ovh-ux/manager-core-api'; +import { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; + +export type RestrictedQueryOptions = Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' +>; + +export type RestrictedMutationOptions = Omit< + UseMutationOptions, + 'mutationFn' +>; diff --git a/packages/manager/modules/vcd-api/src/types/vcd-datacentre.type.ts b/packages/manager/modules/vcd-api/src/types/vcd-datacentre.type.ts index f110dbab5c41..074177d2eb61 100644 --- a/packages/manager/modules/vcd-api/src/types/vcd-datacentre.type.ts +++ b/packages/manager/modules/vcd-api/src/types/vcd-datacentre.type.ts @@ -1,12 +1,18 @@ import { VCDResourceStatus, Task, WithIam } from './vcd-utility.type'; +export type GetVCDDatacentreParams = { + id: string; + vdcId: string; +}; + export type VCDDatacentreTargetSpec = { vCPUSpeed: number; description: string; }; export type VCDDatacentreState = VCDDatacentreTargetSpec & { - commercialRange: string; + commercialRange: 'STANDARD' | 'NSX' | 'VSAN-NSX'; + offerProfile?: 'STANDARD' | 'NSX'; ipQuota: number; storageQuota: number; vCPUCount: number; diff --git a/packages/manager/modules/vcd-api/src/types/vcd-edge-gateway.type.ts b/packages/manager/modules/vcd-api/src/types/vcd-edge-gateway.type.ts new file mode 100644 index 000000000000..472feab902e0 --- /dev/null +++ b/packages/manager/modules/vcd-api/src/types/vcd-edge-gateway.type.ts @@ -0,0 +1,25 @@ +import { Task, VCDResourceStatus } from './vcd-utility.type'; + +export type GetEdgeGatewayParams = { + id: string; + vdcId: string; + edgeGatewayId: string; +}; + +export type UpdateEdgeGatewayPayload = { + edgeGatewayNewName: string; +}; + +export type VCDEdgeGatewayState = { + edgeGatewayName: string; + ipBlock: string; +}; + +export type VCDEdgeGateway = { + id: string; + targetSpec: VCDEdgeGatewayState; + currentState: VCDEdgeGatewayState; + currentTasks: Task[]; + resourceStatus: VCDResourceStatus; + updatedAt?: string; +}; diff --git a/packages/manager/modules/vcd-api/src/types/vcd-ip-block.type.ts b/packages/manager/modules/vcd-api/src/types/vcd-ip-block.type.ts new file mode 100644 index 000000000000..137a8bf047ea --- /dev/null +++ b/packages/manager/modules/vcd-api/src/types/vcd-ip-block.type.ts @@ -0,0 +1,15 @@ +import { VCDEdgeGatewayState } from './vcd-edge-gateway.type'; + +type IpBlockStatus = 'AVAILABLE' | 'READY'; + +// TODO: check case for 'resource_status' & 'unroutable_reason' +export type VCDIpBlock = { + ipBlockID: string; + ipBlock: string; + destination: Partial>; + resource_status: { + status: IpBlockStatus; + unroutable: boolean; + unroutable_reason: string; + }; +}; diff --git a/packages/manager/modules/vcd-api/src/utils/apiRoutes.ts b/packages/manager/modules/vcd-api/src/utils/apiRoutes.ts index d732d600451b..56f79bfb7b83 100644 --- a/packages/manager/modules/vcd-api/src/utils/apiRoutes.ts +++ b/packages/manager/modules/vcd-api/src/utils/apiRoutes.ts @@ -1,3 +1,5 @@ +import { GetEdgeGatewayParams } from '../types'; + export const VCD_ORGANIZATION_ROUTE = '/vmwareCloudDirector/organization'; export const getVcdOrganizationRoute = (id: string) => { @@ -7,6 +9,7 @@ export const getVcdOrganizationRoute = (id: string) => { export const getVcdResetPasswordRoute = (id: string) => `${VCD_ORGANIZATION_ROUTE}/${id}/password`; +// VDC export const getVcdDatacentresRoute = (id: string) => { return `${VCD_ORGANIZATION_ROUTE}/${id}/virtualDataCenter`; }; @@ -27,8 +30,9 @@ export const getVdcOrderableResourceRoute = (id: string, vdcId: string) => { return `${getVcdDatacentreRoute(id, vdcId)}/orderableResource`; }; +// Vrack export const getVdcVrackSegmentListRoute = (id: string, vdcId: string) => { - return `${getVcdDatacentresRoute(id)}/${vdcId}/vrackSegment`; + return `${getVcdDatacentreRoute(id, vdcId)}/vrackSegment`; }; export const getVdcVrackSegmentRoute = ({ @@ -42,3 +46,21 @@ export const getVdcVrackSegmentRoute = ({ }) => { return `${getVdcVrackSegmentListRoute(id, vdcId)}/${vrackSegmentId}`; }; + +// Edge Gateway +export const getVcdEdgeGatewayListRoute = (id: string, vdcId: string) => { + return `${getVcdDatacentreRoute(id, vdcId)}/edgeGateway`; +}; + +export const getVcdEdgeGatewayRoute = ({ + id, + vdcId, + edgeGatewayId, +}: GetEdgeGatewayParams) => { + return `${getVcdEdgeGatewayListRoute(id, vdcId)}/${edgeGatewayId}`; +}; + +// IP Block +export const getVcdIpBlockListRoute = (id: string) => { + return `${getVcdOrganizationRoute(id)}/ipblock`; +}; diff --git a/packages/manager/modules/vcd-api/src/utils/queryKeys.ts b/packages/manager/modules/vcd-api/src/utils/queryKeys.ts index e5fe6ac65abb..fc4fefd5c737 100644 --- a/packages/manager/modules/vcd-api/src/utils/queryKeys.ts +++ b/packages/manager/modules/vcd-api/src/utils/queryKeys.ts @@ -1,3 +1,5 @@ +import { GetEdgeGatewayParams } from '../types'; + const vcdBaseKey = 'vmwareCloudDirector'; const vdcBaseKey = 'virtualDataCenter'; const veeamBackupBaseKey = 'backup'; @@ -69,6 +71,7 @@ export const updateVdcDetailsMutationKey = (vdcId: string) => [ ...getVdcQueryKey(vdcId), ]; +// Vrack export const getVrackSegmentListQueryKey = (id: string, vdcId: string) => [ ...getVcdDatacentreQueryKey(id, vdcId), 'vrackSegment', @@ -83,3 +86,20 @@ export const getVrackSegmentQueryKey = ({ vdcId: string; vrackSegmentId: string; }) => [...getVrackSegmentListQueryKey(id, vdcId), vrackSegmentId]; + +// Edge Gateway +export const getVcdEdgeGatewayListQueryKey = (id: string, vdcId: string) => [ + ...getVcdDatacentreQueryKey(id, vdcId), + 'edgeGateway', +]; + +export const getVcdEdgeGatewayQueryKey = (params: GetEdgeGatewayParams) => [ + ...getVcdEdgeGatewayListQueryKey(params.id, params.vdcId), + params.edgeGatewayId, +]; + +// IP Block +export const getVcdIpBlockListQueryKey = (id: string) => [ + ...getVcdOrganizationQueryKey(id), + 'ipBlock', +];