Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pci-block-storage): add deploy zone
Browse files Browse the repository at this point in the history
ref: TAPC-2112

Signed-off-by: Simon Chaumet <[email protected]>
SimonChaumet committed Jan 16, 2025

Verified

This commit was signed with the committer’s verified signature.
SimonChaumet Simon Chaumet
1 parent 27a7d0d commit 5be5c2a
Showing 14 changed files with 333 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -25,5 +25,14 @@
"pci_projects_project_storages_blocks_add_save_form": "Création du volume en cours",
"pci_projects_project_storages_blocks_add_error_query": "Une erreur est survenue lors de la récupération des régions : {{ message }}",
"pci_projects_project_storages_blocks_add_success_message": "Le volume {{volume}} a été ajouté",
"pci_projects_project_storages_blocks_add_error_post": "Une erreur est survenue lors de l'ajout du volume {{ volume }} : {{ message }}"
"pci_projects_project_storages_blocks_add_error_post": "Une erreur est survenue lors de l'ajout du volume {{ volume }} : {{ message }}",
"pci_projects_project_storages_blocks_add_deployment_mode_title": "Sélectionnez un mode de déploiement",
"pci_projects_project_storages_blocks_add_deployment_mode_description": "Sélectionnez la configuration optimale pour garantir la disponibilité, la résilience et la latence appropriée de vos données en fonction de vos cas d'usage. <Link>En savoir plus</Link>",
"pci_projects_project_storages_blocks_add_deployment_mode_title_1AZ": "Région 1-AZ",
"pci_projects_project_storages_blocks_add_deployment_mode_description_1AZ": "Déploiement résilient et économique sur 1 zone de disponibilité.",
"pci_projects_project_storages_blocks_add_deployment_mode_title_3AZ": "Région 3-AZ",
"pci_projects_project_storages_blocks_add_deployment_mode_description_3AZ": "Déploiement haute résilience/haute disponibilité pour vos applications critiques sur 3 zones de disponibilité.",
"pci_projects_project_storages_blocks_add_deployment_mode_title_LZ": "Local Zone",
"pci_projects_project_storages_blocks_add_deployment_mode_description_LZ": "Déploiement de vos applications au plus près de vos utilisatrices et utilisateurs pour une faible latence et la résidence des données.",
"pci_projects_project_storages_blocks_add_deployment_mode_price_from": "A partir de {{price}} HT/Go/heure"
}
104 changes: 104 additions & 0 deletions packages/manager/apps/pci-block-storage/src/api/data/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { TAddon } from '@ovh-ux/manager-pci-common';
import { v6 } from '@ovh-ux/manager-core-api';

export type TPricing = {
capacities: string[];
mode: string;
@@ -26,3 +29,104 @@ export type TPricing = {
promotions: unknown[];
engagementConfiguration?: unknown;
};

export type TRegionGroup = {
name: string;
tags: string[];
};

export type TModelGroup = {
name: string;
type: string;
tags: string[];
};

export type TRegion = {
name: string;
type: '3-az' | 'region' | 'localzone';
availabilityZone: string[];
isInMaintenance: boolean;
isUp: boolean;
isActivated: boolean;
country: string;
regionGroup: string;
datacenter: string;
};

export type TVolumePricing = Omit<
TAddon['pricings'][number],
'interval' | 'intervalUnit'
> & {
regions: TRegion['name'][];
interval: 'day' | 'hour' | 'month' | 'none';
};

export type TVolumeAddon = Omit<TAddon, 'pricings'> & {
groups: TRegionGroup['name'][];
pricings: TVolumePricing[];
pricingType: 'consumption' | string;
};

export type TVolumeCatalog = {
modelsGroups: TModelGroup[];
regionsGroups: TRegionGroup[];
regions: TRegion[];
models: TVolumeAddon[];
};

export const getVolumeCatalog = async (
projectId: string,
): Promise<TVolumeCatalog> =>
(await v6.get<TVolumeCatalog>(`/cloud/project/${projectId}/catalog/volume`))
.data;

export function getLeastPrice(pricings: TVolumePricing[]) {
return pricings.reduce<number | null>(
(leastPrice, p) =>
leastPrice === null ? p.price : Math.min(p.price, leastPrice),
null,
);
}

function isConsumptionAddon(
addon: TVolumeAddon,
): addon is TVolumeAddon & { pricingType: 'consumption' } {
return addon.pricingType === 'consumption';
}

function isConsumptionPricing(p: TVolumePricing) {
return p.capacities.includes('consumption');
}

export function mapPricesToGroups(
groups: TRegionGroup[],
regions: TRegion[],
models: TVolumeAddon[],
): (TRegionGroup & { leastPrice: number })[] {
return groups.map((group) => {
const groupRegions = regions
.filter((r) => r.type === group.name)
.map((r) => r.name);

const hasGroupRegions = (p: TVolumePricing) =>
p.regions.some((r) => groupRegions.includes(r));

return {
...group,
leastPrice: models
.filter(isConsumptionAddon)
.map((m) =>
getLeastPrice(
m.pricings.filter(isConsumptionPricing).filter(hasGroupRegions),
),
)
.reduce<number | null>(
(leastPrice, modelLeastPrice) =>
leastPrice === null
? modelLeastPrice
: Math.min(modelLeastPrice, leastPrice),
null,
),
};
});
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { useMe } from '@ovh-ux/manager-react-components';
import { getCatalog } from '@ovh-ux/manager-pci-common';
import { getVolumeCatalog } from '@/api/data/catalog';

export const getCatalogQuery = (ovhSubsidiary: string) => ({
queryKey: ['catalog'],
queryFn: () => getCatalog(ovhSubsidiary),
});

/**
* @deprecated use {@link useVolumeCatalog} instead
*/
export const useCatalog = () => {
const { me } = useMe();
return useQuery({
...getCatalogQuery(me?.ovhSubsidiary),
enabled: !!me,
});
};

export const useVolumeCatalog = (projectId: string) =>
useQuery({
queryKey: ['projects', projectId, 'catalog', 'volume'],
queryFn: () => getVolumeCatalog(projectId),
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -22,21 +22,21 @@ import {
} from '@ovhcloud/ods-components';

import { useTranslation } from 'react-i18next';
import { TAddon } from '@ovh-ux/manager-pci-common';
import { PriceEstimate } from '@/pages/new/components/PriceEstimate';
import { HighSpeedV2Infos } from '@/pages/new/components/HighSpeedV2Infos';
import { TLocalisation } from '@/api/hooks/useRegions';
import { StepState } from '@/pages/new/hooks/useStep';
import { useRegionsQuota } from '@/api/hooks/useQuota';
import { useVolumeMaxSize } from '@/api/data/quota';
import { TVolumeAddon } from '@/api/data/catalog';

export const VOLUME_MIN_SIZE = 10; // 10 Gio
export const VOLUME_UNLIMITED_QUOTA = -1; // Should be 10 * 1024 (but API is wrong)

interface CapacityStepProps {
projectId: string;
region: TLocalisation;
volumeType: TAddon;
volumeType: TVolumeAddon;
step: StepState;
onSubmit: (volumeCapacity: number) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useParams } from 'react-router-dom';
import {
OsdsChip,
OsdsSpinner,
OsdsText,
} from '@ovhcloud/ods-components/react';
import { useTranslation } from 'react-i18next';
import { ODS_CHIP_SIZE, ODS_SPINNER_SIZE } from '@ovhcloud/ods-components';
import { useMemo, useState } from 'react';
import { TilesInputComponent } from '@ovh-ux/manager-react-components';
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import { mapPricesToGroups } from '@/api/data/catalog';
import { useVolumeCatalog } from '@/api/hooks/useCatalog';

export const DeploymentModeStep = () => {
const { t } = useTranslation(['add', 'common']);
const { projectId } = useParams();

const { data, isPending } = useVolumeCatalog(projectId);

const mappedGroups = useMemo(
() =>
data
? mapPricesToGroups(data.regionsGroups, data.regions, data.models)
: [],
[data],
);

const [selectedRegionGroup, setSelectedRegionGroup] = useState<
null | ReturnType<typeof mapPricesToGroups>[number]
>(null);

if (isPending) return <OsdsSpinner inline size={ODS_SPINNER_SIZE.md} />;

return (
<div>
<TilesInputComponent<ReturnType<typeof mapPricesToGroups>[number]>
items={mappedGroups}
value={selectedRegionGroup}
onInput={setSelectedRegionGroup}
label={(group) => (
<div className="w-full">
<div className="border-solid border-0 border-b border-b-[#85d9fd] py-3 d-flex">
<OsdsText>
{t(
`pci_projects_project_storages_blocks_add_deployment_mode_title_${group.name}`,
)}
</OsdsText>
</div>
<div className="py-3">
{group.tags.includes('is_new') ? (
<div>
<OsdsChip
className="ms-3"
color={ODS_THEME_COLOR_INTENT.success}
size={ODS_CHIP_SIZE.sm}
inline
>
{t('common:pci_projects_project_storages_blocks_new')}
</OsdsChip>
Gratuit
</div>
) : (
group.leastPrice !== null &&
t(
'pci_projects_project_storages_blocks_add_deployment_mode_price_from',
{ price: group.leastPrice },
)
)}
</div>
</div>
)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import {
ODS_THEME_TYPOGRAPHY_SIZE,
} from '@ovhcloud/ods-common-theming';
import { useTranslation } from 'react-i18next';
import { TAddon } from '@ovh-ux/manager-pci-common';
import { TVolumeAddon } from '@/api/data/catalog';

export const HIGHSPEED_V2_PLANCODE = 'volume.high-speed-gen2.consumption';

@@ -17,7 +17,10 @@ export function getDisplayUnit(unit: string) {
return unit;
}

export function computeBandwidthToAllocate(capacity: number, addon: TAddon) {
export function computeBandwidthToAllocate(
capacity: number,
addon: TVolumeAddon,
) {
const { bandwidth } = addon.blobs.technical;
const allocatedBandwidth = capacity * bandwidth.level;
const maxBandwidthInMb = bandwidth.max * 1000;
@@ -27,7 +30,7 @@ export function computeBandwidthToAllocate(capacity: number, addon: TAddon) {
: `${maxBandwidthInMb} ${getDisplayUnit(bandwidth.unit)}`;
}

export function computeIopsToAllocate(capacity: number, addon: TAddon) {
export function computeIopsToAllocate(capacity: number, addon: TVolumeAddon) {
const { volume } = addon.blobs.technical;
const allocatedIops = capacity * volume.iops.level;

@@ -38,7 +41,7 @@ export function computeIopsToAllocate(capacity: number, addon: TAddon) {

export interface HighSpeedV2InfosProps {
volumeCapacity: number;
volumeType: TAddon;
volumeType: TVolumeAddon;
}

export function HighSpeedV2Infos({
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useState } from 'react';
import { OsdsButton } from '@ovhcloud/ods-components/react';
import { ODS_BUTTON_SIZE } from '@ovhcloud/ods-components';
import { useContext, useState } from 'react';
import { OsdsButton, OsdsLink, OsdsText } from '@ovhcloud/ods-components/react';
import {
ODS_BUTTON_SIZE,
ODS_TEXT_LEVEL,
ODS_TEXT_SIZE,
} from '@ovhcloud/ods-components';
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import {
isDiscoveryProject,
useProject,
@@ -11,10 +15,14 @@ import {
usePCICommonContextFactory,
PCICommonContext,
} from '@ovh-ux/manager-pci-common';
import { OvhSubsidiary, Subtitle } from '@ovh-ux/manager-react-components';
import { ShellContext } from '@ovh-ux/manager-react-shell-client';
import { TLocalisation } from '@/api/hooks/useRegions';
import { StepState } from '@/pages/new/hooks/useStep';
import { useProjectsAvailableVolumes } from '@/api/hooks/useProjectsAvailableVolumes';
import { isRegionWith3AZ } from '@/api/data/availableVolumes';
import { DeploymentModeStep } from '@/pages/new/components/DeploymentModeStep';
import { getBaseUrl } from '@/website/ovhWebsiteMapper';

interface LocationProps {
projectId: string;
@@ -40,7 +48,10 @@ export function LocationStep({
step,
onSubmit,
}: Readonly<LocationProps>) {
const { t: tStepper } = useTranslation('stepper');
const { t } = useTranslation(['stepper', 'add']);
const context = useContext(ShellContext);
const { ovhSubsidiary } = context.environment.getUser();

const [region, setRegion] = useState<TLocalisation>(undefined);
const { data: project } = useProject();
const isDiscovery = isDiscoveryProject(project);
@@ -53,15 +64,49 @@ export function LocationStep({
<PCICommonContext.Provider value={pciCommonProperties}>
{hasRegion && step.isLocked && <RegionSummary region={region} />}
{(!step.isLocked || isDiscovery) && (
<RegionSelector
projectId={projectId}
onSelectRegion={setRegion}
regionFilter={(r) =>
r.isMacro ||
r.services.some((s) => s.name === 'volume' && s.status === 'UP') ||
r.type === 'region-3-az'
}
/>
<div>
<div>
<Subtitle>
{t(
'add:pci_projects_project_storages_blocks_add_deployment_mode_title',
)}
</Subtitle>
<div>
<OsdsText
size={ODS_TEXT_SIZE._400}
level={ODS_TEXT_LEVEL.body}
color={ODS_THEME_COLOR_INTENT.text}
>
<Trans
i18nKey="add:pci_projects_project_storages_blocks_add_deployment_mode_description"
components={{
Link: (
<OsdsLink
href={getBaseUrl(ovhSubsidiary as OvhSubsidiary)}
color={ODS_THEME_COLOR_INTENT.info}
/>
),
}}
/>
</OsdsText>
</div>
</div>
{/* <DeploymentModeStep /> */}

<div>
<Subtitle>
{t('add:pci_projects_project_storages_blocks_add_region_title')}
</Subtitle>
</div>
<RegionSelector
projectId={projectId}
onSelectRegion={setRegion}
regionFilter={(r) =>
r.isMacro ||
r.services.some((s) => s.name === 'volume' && s.status === 'UP')||
r.type === 'region-3-az'}
/>
</div>
)}
{hasRegion && !step.isLocked && (
<OsdsButton
@@ -70,7 +115,7 @@ export function LocationStep({
color={ODS_THEME_COLOR_INTENT.primary}
onClick={() => onSubmit(region)}
>
{tStepper('common_stepper_next_button_label')}
{t('common_stepper_next_button_label')}
</OsdsButton>
)}
</PCICommonContext.Provider>
Original file line number Diff line number Diff line change
@@ -9,11 +9,11 @@ import {
useCatalogPrice,
convertHourlyPriceToMonthly,
} from '@ovh-ux/manager-react-components';
import { TAddon } from '@ovh-ux/manager-pci-common';
import { TVolumeAddon } from '@/api/data/catalog';

export interface PriceEstimateProps {
volumeCapacity: number;
volumeType: TAddon;
volumeType: TVolumeAddon;
}

export function PriceEstimate({
Original file line number Diff line number Diff line change
@@ -4,12 +4,13 @@ import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import { useNotifications } from '@ovh-ux/manager-react-components';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { PciTrustedZoneBanner, TAddon } from '@ovh-ux/manager-pci-common';
import { PciTrustedZoneBanner } from '@ovh-ux/manager-pci-common';
import { PriceEstimate } from '@/pages/new/components/PriceEstimate';
import { TVolumeAddon } from '@/api/data/catalog';

interface ValidationStepProps {
volumeCapacity: number;
volumeType: TAddon;
volumeType: TVolumeAddon;
onSubmit: () => void;
}

Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
import {
OsdsButton,
OsdsSpinner,
OsdsText,
OsdsChip,
} from '@ovhcloud/ods-components/react';
import { OsdsButton, OsdsText, OsdsChip } from '@ovhcloud/ods-components/react';
import {
ODS_THEME_COLOR_INTENT,
ODS_THEME_TYPOGRAPHY_LEVEL,
ODS_THEME_TYPOGRAPHY_SIZE,
} from '@ovhcloud/ods-common-theming';
import {
ODS_CHIP_SIZE,
ODS_BUTTON_SIZE,
ODS_SPINNER_SIZE,
} from '@ovhcloud/ods-components';
import { ODS_CHIP_SIZE, ODS_BUTTON_SIZE } from '@ovhcloud/ods-components';
import {
TilesInputComponent,
useCatalogPrice,
} from '@ovh-ux/manager-react-components';

import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { TAddon, TCatalog } from '@ovh-ux/manager-pci-common';
import { useMemo, useState } from 'react';
import { useTranslateBytes } from '@/pages/new/hooks/useTranslateBytes';
import { useConsumptionVolumesAddon } from '@/api/hooks/useConsumptionVolumesAddon';
import { StepState } from '@/pages/new/hooks/useStep';
import { TLocalisation } from '@/api/hooks/useRegions';
import { useVolumeCatalog } from '@/api/hooks/useCatalog';
import { TVolumeAddon } from '@/api/data/catalog';

export interface VolumeTypeStepProps {
projectId: string;
region: TLocalisation;
step: StepState;
onSubmit: (volumeType: TAddon) => void;
onSubmit: (volumeType: TVolumeAddon) => void;
}

export function VolumeTypeStep({
@@ -43,30 +34,32 @@ export function VolumeTypeStep({
const { t } = useTranslation('add');
const { t: tStepper } = useTranslation('stepper');
const { t: tCommon } = useTranslation('common');
const [volumeType, setVolumeType] = useState<TAddon>(undefined);
const [volumeType, setVolumeType] = useState<TVolumeAddon>(undefined);
const tBytes = useTranslateBytes();
const { getFormattedCatalogPrice } = useCatalogPrice(6, {
hideTaxLabel: true,
});

const { volumeTypes, isPending } = useConsumptionVolumesAddon(
projectId,
region,
const { data } = useVolumeCatalog(projectId);
const volumeTypes = useMemo(
() =>
data?.models.filter(
(m) =>
m.pricingType === 'consumption' &&
m.pricings.flatMap((p) => p.regions).includes(region.name),
) || [],
[data, region],
);

const displayedTypes =
volumeType && step.isLocked ? [volumeType] : volumeTypes;

if (isPending) {
return <OsdsSpinner inline size={ODS_SPINNER_SIZE.md} />;
}

return (
<>
<TilesInputComponent<TCatalog['addons'][0]>
<TilesInputComponent<TVolumeAddon>
value={volumeType}
items={displayedTypes || []}
label={(vType: TCatalog['addons'][0]) => (
label={(vType: TVolumeAddon) => (
<div className="w-full">
<div className="border-solid border-0 border-b border-b-[#85d9fd] py-3 d-flex">
<OsdsText
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TAddon } from '@ovh-ux/manager-pci-common';
import { TLocalisation } from '@/api/hooks/useRegions';
import { TVolumeAddon } from '@/api/data/catalog';

export type TFormState = {
region: TLocalisation;
volumeType: TAddon;
volumeType: TVolumeAddon;
volumeName: string;
volumeCapacity: number;
availabilityZone: string;
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useMemo, useState } from 'react';
import { TAddon, useProjectRegions } from '@ovh-ux/manager-pci-common';
import { 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';
import { TVolumeAddon } from '@/api/data/catalog';

export function useVolumeStepper(projectId: string) {
const { data } = useProjectRegions(projectId);
@@ -91,7 +92,7 @@ export function useVolumeStepper(projectId: string) {
});
setForm((f) => ({ region: f.region }));
},
submit: (volumeType: TAddon) => {
submit: (volumeType: TVolumeAddon) => {
volumeTypeStep.check();
volumeTypeStep.lock();
if (
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { OvhSubsidiary } from '@ovh-ux/manager-react-components';

export function getBaseUrl(ovhSubsidiary: OvhSubsidiary): string {
switch (ovhSubsidiary) {
case OvhSubsidiary.ASIA:
case OvhSubsidiary.DE:
case OvhSubsidiary.FR:
case OvhSubsidiary.IT:
case OvhSubsidiary.NL:
case OvhSubsidiary.PL:
case OvhSubsidiary.PT:
return `https://www.ovhcloud.com/${ovhSubsidiary.toLowerCase()}`;
case OvhSubsidiary.AU:
case OvhSubsidiary.CA:
case OvhSubsidiary.GB:
case OvhSubsidiary.IE:
case OvhSubsidiary.IN:
case OvhSubsidiary.SG:
return `https://www.ovhcloud.com/en-${ovhSubsidiary.toLowerCase()}`;
case OvhSubsidiary.ES:
return 'https://www.ovhcloud.com/es-es';
case OvhSubsidiary.MA:
case OvhSubsidiary.SN:
case OvhSubsidiary.TN:
return `https://www.ovhcloud.com/fr-${ovhSubsidiary.toLowerCase()}`;
case OvhSubsidiary.QC:
return 'https://www.ovhcloud.com/fr-ca';
case OvhSubsidiary.US:
return 'https://us.ovhcloud.com';
case OvhSubsidiary.WS:
return 'https://www.ovhcloud.com/es';
case OvhSubsidiary.DEFAULT:
default:
return 'https://www.ovhcloud.com/en';
}
}

0 comments on commit 5be5c2a

Please sign in to comment.