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) => ( +
+
+ + {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; + }, }; }