Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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."
}
Original file line number Diff line number Diff line change
@@ -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',
};
Original file line number Diff line number Diff line change
@@ -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<TValues extends FieldValues = FieldValues> = {
field: ControllerRenderProps<TValues>;
label: string;
error?: string;
isDisabled?: boolean;
isLoading?: boolean;
placeholder?: string;
validator?: TextFieldValidatorProps;
};

export const InputField = <TValues extends FieldValues = FieldValues>({
field,
label,
error,
isDisabled = false,
isLoading = false,
placeholder,
validator = {},
}: InputFieldProps<TValues>) => {
return (
<OdsFormField error={error}>
<label htmlFor={field.name} slot="label">
{label}
</label>
<div className="flex items-center gap-x-4">
<OdsInput
className="w-full"
id={field.name}
placeholder={placeholder}
isDisabled={isDisabled}
hasError={!!error}
{...validator}
{...field}
onOdsBlur={field.onBlur}
onOdsChange={field.onChange}
/>
{isLoading && <OdsSpinner size="sm" />}
</div>

{validator.maxlength && (
<OdsText slot="visual-hint" preset="caption">
{`${field.value?.length || 0}/${validator.maxlength}`}
</OdsText>
)}
</OdsFormField>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ControllerRenderProps, FieldValues } from 'react-hook-form';
import {
OdsFormField,
OdsSelect,
OdsSpinner,
OdsText,
} from '@ovhcloud/ods-components/react';

type SelectFieldProps<TValues extends FieldValues = FieldValues> = {
field: ControllerRenderProps<TValues>;
options: string[];
label: string;
error?: string;
isDisabled?: boolean;
isLoading?: boolean;
placeholder?: string;
helperText?: string;
};

export const SelectField = <TValues extends FieldValues = FieldValues>({
field,
options = [],
label,
error,
isDisabled = false,
isLoading = false,
placeholder,
helperText,
}: SelectFieldProps<TValues>) => {
return (
<OdsFormField error={error}>
<label htmlFor={field.name} slot="label">
{label}
</label>
<div className="flex items-center gap-x-4">
<OdsSelect
className="w-full"
id={field.name}
placeholder={placeholder}
isDisabled={isDisabled}
hasError={!!error}
{...field}
onOdsBlur={field.onBlur}
onOdsChange={field.onChange}
>
{options.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</OdsSelect>
{isLoading && <OdsSpinner size="sm" />}
</div>
{helperText && (
<OdsText slot="helper" preset="caption">
{helperText}
</OdsText>
)}
</OdsFormField>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const COMPUTE_LABEL = 'Compute';
export const STORAGE_LABEL = 'Storage';
export const EDGE_GATEWAY_LABEL = 'Edge Gateway';
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
if (queries.isError) {
return (
<ErrorBanner error={{ status: 404, data: { message: queries.error } }} />
);
}

return (
<RedirectionGuard
isLoading={queries.isLoading}
route={urls.listing}
condition={isEdgeCompatibleVDC(queries.data.vdc)}
>
<Suspense fallback={<Loading />}>
<section className="px-10 flex flex-col">
<OdsText preset="heading-3">{EDGE_GATEWAY_LABEL}</OdsText>
<EdgeGatewayOrderButton
className="mt-4 mb-8"
isOrderDisabled={hasMaxEdges}
/>
<Datagrid
columns={columns}
items={queries.data.edges}
totalItems={queries.data.edges?.length}
/>
</section>
</Suspense>
<Outlet />
</RedirectionGuard>
);
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading