diff --git a/packages/manager/apps/pci-block-storage/public/translations/add/Messages_fr_FR.json b/packages/manager/apps/pci-block-storage/public/translations/add/Messages_fr_FR.json
index 1081791c6f9e..2bdfa71cbe03 100644
--- a/packages/manager/apps/pci-block-storage/public/translations/add/Messages_fr_FR.json
+++ b/packages/manager/apps/pci-block-storage/public/translations/add/Messages_fr_FR.json
@@ -11,6 +11,7 @@
"pci_projects_project_storages_blocks_add_type_addon_iops_not_guaranteed": "Jusqu'à {{iops}} IOPS{{separator}} ",
"pci_projects_project_storages_blocks_add_type_addon_capacity_max": "{{capacity}} max.",
"pci_projects_project_storages_blocks_add_type_addon_price": "{{price}} HT/Go/heure",
+ "pci_projects_project_storages_blocks_add_availability_zone": "Sélectionnez une zone de disponibilité",
"pci_projects_project_storages_blocks_add_size_title": "Capacité du volume",
"pci_projects_project_storages_blocks_add_size_help": "La taille maximale dépend de votre quota disponible.",
"pci_projects_project_storages_blocks_add_size_unit": "Go",
diff --git a/packages/manager/apps/pci-block-storage/src/api/data/availableVolumes.ts b/packages/manager/apps/pci-block-storage/src/api/data/availableVolumes.ts
index 443995c41db5..a65a21d030e3 100644
--- a/packages/manager/apps/pci-block-storage/src/api/data/availableVolumes.ts
+++ b/packages/manager/apps/pci-block-storage/src/api/data/availableVolumes.ts
@@ -1,4 +1,5 @@
import { v6 } from '@ovh-ux/manager-core-api';
+import { TLocalisation } from '@/api/hooks/useRegions';
export type TAvailableVolumesResponse = {
plans: {
@@ -20,3 +21,15 @@ export const getProjectsAvailableVolumes = async (
return data;
};
+
+export function isRegionWith3AZ(region: TLocalisation) {
+ return region.type === 'region-3-az';
+}
+
+/**
+ * TODO: use real informations
+ * @param planCode
+ */
+export function isProductWithAvailabilityZone(planCode: string) {
+ return planCode.startsWith('volume.high-speed');
+}
diff --git a/packages/manager/apps/pci-block-storage/src/api/data/volume.ts b/packages/manager/apps/pci-block-storage/src/api/data/volume.ts
index 58406f5c13f3..7e2e0d2beb38 100644
--- a/packages/manager/apps/pci-block-storage/src/api/data/volume.ts
+++ b/packages/manager/apps/pci-block-storage/src/api/data/volume.ts
@@ -185,6 +185,7 @@ export interface AddVolumeProps {
regionName: string;
volumeCapacity: number;
volumeType: string;
+ availabilityZone?: string;
}
export const addVolume = async ({
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/New.page.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/New.page.tsx
index 65480db49d69..ddd1cea561b6 100644
--- a/packages/manager/apps/pci-block-storage/src/pages/new/New.page.tsx
+++ b/packages/manager/apps/pci-block-storage/src/pages/new/New.page.tsx
@@ -23,6 +23,7 @@ import { LocationStep } from './components/LocationStep.component';
import { useVolumeStepper } from './hooks/useVolumeStepper';
import { useAddVolume } from '@/api/hooks/useVolume';
import { ExtenBannerBeta } from '@/components/exten-banner-beta/ExtenBannerBeta';
+import { AvailabilityZoneStep } from '@/pages/new/components/AvailabilityZoneStep';
export default function NewPage(): JSX.Element {
const { t } = useTranslation('common');
@@ -35,7 +36,7 @@ export default function NewPage(): JSX.Element {
const backHref = useHref('..');
const isDiscovery = isDiscoveryProject(project);
const { addError, addSuccess, clearNotifications } = useNotifications();
- const stepper = useVolumeStepper();
+ const stepper = useVolumeStepper(projectId);
const { addVolume } = useAddVolume({
projectId,
@@ -109,7 +110,7 @@ export default function NewPage(): JSX.Element {
+ {stepper.availabilityZone.step.isShown && (
+
+ {!!stepper.form.region?.name && (
+
+ )}
+
+ )}
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/AvailabilityZoneStep.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/AvailabilityZoneStep.tsx
new file mode 100644
index 000000000000..ad67d99e879f
--- /dev/null
+++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/AvailabilityZoneStep.tsx
@@ -0,0 +1,70 @@
+import { TilesInputComponent } from '@ovh-ux/manager-react-components';
+import { useMemo, useState } from 'react';
+import { OsdsButton, OsdsText } from '@ovhcloud/ods-components/react';
+import { ODS_BUTTON_SIZE } from '@ovhcloud/ods-components';
+import {
+ ODS_THEME_COLOR_INTENT,
+ ODS_THEME_TYPOGRAPHY_LEVEL,
+ ODS_THEME_TYPOGRAPHY_SIZE,
+} from '@ovhcloud/ods-common-theming';
+import { useTranslation } from 'react-i18next';
+import { Step } from '@/pages/new/hooks/useStep';
+
+type Props = {
+ regionName: string;
+ step: Step;
+ onSubmit: (zone: string) => void;
+};
+
+export function AvailabilityZoneStep({ regionName, step, onSubmit }: Props) {
+ const { t } = useTranslation('stepper');
+
+ // TODO: use real informations
+ const zones = useMemo(
+ () =>
+ ['a', 'b', 'c'].map((suffix) => `${regionName.toLowerCase()}-${suffix}`),
+ [regionName],
+ );
+ const [selectedZone, setSelectedZone] = useState(
+ undefined,
+ );
+ const displayedZones = useMemo(
+ () => (!!selectedZone && step.isLocked ? [selectedZone] : zones),
+ [zones, selectedZone, step],
+ );
+
+ return (
+
+
+ items={displayedZones}
+ value={selectedZone}
+ onInput={(z) => setSelectedZone(z)}
+ label={(z) => (
+
+ )}
+ />
+ {!!selectedZone && !step.isLocked && (
+
+ onSubmit(selectedZone)}
+ className="w-fit"
+ >
+ {t('common_stepper_next_button_label')}
+
+
+ )}
+
+ );
+}
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/form.type.ts b/packages/manager/apps/pci-block-storage/src/pages/new/form.type.ts
index b8364cc91548..2752ed1ee007 100644
--- a/packages/manager/apps/pci-block-storage/src/pages/new/form.type.ts
+++ b/packages/manager/apps/pci-block-storage/src/pages/new/form.type.ts
@@ -6,4 +6,5 @@ export type TFormState = {
volumeType: TAddon;
volumeName: string;
volumeCapacity: number;
+ availabilityZone: string;
};
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useStep.ts b/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useStep.ts
index f777b8611be8..57276714d09f 100644
--- a/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useStep.ts
+++ b/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useStep.ts
@@ -4,21 +4,40 @@ export interface StepState {
isOpen: boolean;
isChecked: boolean;
isLocked: boolean;
+ isShown: boolean;
}
-export function useStep(initialState?: Readonly>) {
+export type Step = StepState & {
+ open: () => void;
+ close: () => void;
+ check: () => void;
+ uncheck: () => void;
+ lock: () => void;
+ unlock: () => void;
+ show: () => void;
+ hide: () => void;
+};
+
+export function useStep(initialState?: Readonly>): Step {
const [isOpen, setIsOpen] = useState(!!initialState?.isOpen);
const [isChecked, setIsChecked] = useState(!!initialState?.isChecked);
const [isLocked, setIsLocked] = useState(!!initialState?.isLocked);
+ const [isShown, setIsShown] = useState(
+ initialState && 'isShown' in initialState ? initialState.isShown : true,
+ );
+
return {
isOpen,
isChecked,
isLocked,
+ isShown,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
check: () => setIsChecked(true),
uncheck: () => setIsChecked(false),
lock: () => setIsLocked(true),
unlock: () => setIsLocked(false),
+ show: () => setIsShown(true),
+ hide: () => setIsShown(false),
};
}
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useVolumeStepper.ts b/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useVolumeStepper.ts
index dbd2b0c219e7..05c3b89f698a 100644
--- a/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useVolumeStepper.ts
+++ b/packages/manager/apps/pci-block-storage/src/pages/new/hooks/useVolumeStepper.ts
@@ -1,45 +1,74 @@
-import { useState } from 'react';
-import { TAddon } from '@ovh-ux/manager-pci-common';
-import { useStep } from '@/pages/new/hooks/useStep';
+import { useEffect, useMemo, useState } from 'react';
+import { TAddon, useProjectRegions } from '@ovh-ux/manager-pci-common';
+import { Step, useStep } from '@/pages/new/hooks/useStep';
import { TFormState } from '@/pages/new/form.type';
import { TLocalisation } from '@/api/hooks/useRegions';
+import {
+ isProductWithAvailabilityZone,
+ isRegionWith3AZ,
+} from '@/api/data/availableVolumes';
-export const DEFAULT_FORM_STATE: TFormState = {
- region: undefined,
- volumeType: undefined,
- volumeName: '',
- volumeCapacity: 10,
-};
+export function useVolumeStepper(projectId: string) {
+ const { data } = useProjectRegions(projectId);
+ const is3AZAvailable = useMemo(() => !!data && data.some(isRegionWith3AZ), [
+ data,
+ ]);
-export function useVolumeStepper() {
- const [form, setForm] = useState({
- ...DEFAULT_FORM_STATE,
- });
+ const [form, setForm] = useState>({});
const locationStep = useStep({ isOpen: true });
const volumeTypeStep = useStep();
+ const availabilityZoneStep = useStep();
const capacityStep = useStep();
const volumeNameStep = useStep();
const validationStep = useStep();
+ useEffect(() => {
+ if (is3AZAvailable) {
+ availabilityZoneStep.show();
+ } else {
+ availabilityZoneStep.hide();
+ }
+ }, [is3AZAvailable]);
+
+ const order = [
+ locationStep,
+ volumeTypeStep,
+ availabilityZoneStep,
+ capacityStep,
+ volumeNameStep,
+ validationStep,
+ ];
+
return {
form,
location: {
step: locationStep,
edit: () => {
locationStep.unlock();
- [volumeTypeStep, capacityStep, volumeNameStep, validationStep].forEach(
- (step) => {
- step.uncheck();
- step.unlock();
- step.close();
- },
- );
- setForm({ ...DEFAULT_FORM_STATE });
+ [
+ volumeTypeStep,
+ availabilityZoneStep,
+ capacityStep,
+ volumeNameStep,
+ validationStep,
+ ].forEach((step) => {
+ step.uncheck();
+ step.unlock();
+ step.close();
+ });
+ setForm({});
},
submit: (region: TLocalisation) => {
locationStep.check();
locationStep.lock();
volumeTypeStep.open();
+ if (is3AZAvailable) {
+ if (isRegionWith3AZ(region)) {
+ availabilityZoneStep.show();
+ } else {
+ availabilityZoneStep.hide();
+ }
+ }
setForm((f) => ({
...f,
region,
@@ -50,23 +79,64 @@ export function useVolumeStepper() {
step: volumeTypeStep,
edit: () => {
volumeTypeStep.unlock();
- [capacityStep, volumeNameStep, validationStep].forEach((step) => {
+ [
+ availabilityZoneStep,
+ capacityStep,
+ volumeNameStep,
+ validationStep,
+ ].forEach((step) => {
step.uncheck();
step.unlock();
step.close();
});
- setForm((f) => ({ ...DEFAULT_FORM_STATE, region: f.region }));
+ setForm((f) => ({ region: f.region }));
},
submit: (volumeType: TAddon) => {
volumeTypeStep.check();
volumeTypeStep.lock();
- capacityStep.open();
+ if (
+ is3AZAvailable &&
+ isRegionWith3AZ(form.region) &&
+ isProductWithAvailabilityZone(volumeType.planCode)
+ ) {
+ availabilityZoneStep.show();
+ availabilityZoneStep.open();
+ } else {
+ availabilityZoneStep.hide();
+ capacityStep.open();
+ }
setForm((f) => ({
...f,
volumeType,
}));
},
},
+ availabilityZone: {
+ step: availabilityZoneStep,
+ edit: () => {
+ volumeTypeStep.unlock();
+ [
+ availabilityZoneStep,
+ capacityStep,
+ volumeNameStep,
+ validationStep,
+ ].forEach((step) => {
+ step.uncheck();
+ step.unlock();
+ step.close();
+ });
+ setForm((f) => ({ region: f.region, volumeType: f.volumeType }));
+ },
+ submit: (availabilityZone: string) => {
+ availabilityZoneStep.check();
+ availabilityZoneStep.lock();
+ capacityStep.open();
+ setForm((f) => ({
+ ...f,
+ availabilityZone,
+ }));
+ },
+ },
capacity: {
step: capacityStep,
edit: () => {
@@ -77,9 +147,9 @@ export function useVolumeStepper() {
step.close();
});
setForm((f) => ({
- ...DEFAULT_FORM_STATE,
region: f.region,
volumeType: f.volumeType,
+ availabilityZone: f.availabilityZone,
}));
},
submit: (volumeCapacity: number) => {
@@ -100,8 +170,10 @@ export function useVolumeStepper() {
validationStep.unlock();
validationStep.close();
setForm((f) => ({
- ...f,
- volumeName: DEFAULT_FORM_STATE.volumeName,
+ region: f.region,
+ volumeType: f.volumeType,
+ availabilityZone: f.availabilityZone,
+ volumeCapacity: f.volumeCapacity,
}));
},
submit: (volumeName: string) => {
@@ -120,5 +192,8 @@ export function useVolumeStepper() {
validationStep.lock();
},
},
+ getOrder(step: Step) {
+ return order.filter((o) => o.isShown).findIndex((o) => o === step) + 1;
+ },
};
}