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 2bdfa71cbe03..9c6332353916 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 @@ -25,5 +25,15 @@ "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 une configuration optimale pour assurer la disponibilité, la résilience et la latence appropriées pour vos données selon vos cas d’usage. En savoir plus", + "pci_projects_project_storages_blocks_add_deployment_mode_title_region": "Région 1-AZ", + "pci_projects_project_storages_blocks_add_deployment_mode_description_region": "Déploiement résilient et économique sur 1 zone de disponibilité.", + "pci_projects_project_storages_blocks_add_deployment_mode_title_region-3-az": "Région 3-AZ", + "pci_projects_project_storages_blocks_add_deployment_mode_description_region-3-az": "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_localzone": "Local Zone", + "pci_projects_project_storages_blocks_add_deployment_mode_description_localzone": "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", + "pci_projects_project_storages_blocks_add_beta_free": "Gratuit" } 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 deleted file mode 100644 index bae1be8ff819..000000000000 --- a/packages/manager/apps/pci-block-storage/src/api/data/availableVolumes.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { v6 } from '@ovh-ux/manager-core-api'; -import { TLocalisation } from '@/api/hooks/useRegions'; - -export type TAvailableVolumesResponse = { - plans: { - code: string; - regions: { - name: string; - enabled: boolean; - type: string; - }[]; - }[]; -}; - -export const getProjectsAvailableVolumes = async ( - projectId: string, - ovhSubsidiary: string, -): Promise => { - const { data } = await v6.get( - `/cloud/project/${projectId}/capabilities/productAvailability?addonFamily=volume&ovhSubsidiary=${ovhSubsidiary}`, - ); - - return data; -}; - -export function isRegionWith3AZ(region: Pick) { - 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/catalog.ts b/packages/manager/apps/pci-block-storage/src/api/data/catalog.ts index 192f6048ebd2..1df3b81ccbfa 100644 --- a/packages/manager/apps/pci-block-storage/src/api/data/catalog.ts +++ b/packages/manager/apps/pci-block-storage/src/api/data/catalog.ts @@ -1,28 +1,74 @@ -export type TPricing = { - capacities: string[]; - mode: string; - phase: number; - commitment: number; - description: string; - price: { - currencyCode: string; - text: string; - value: number; - }; - tax: number; - interval: number; - intervalUnit: string; - quantity: { - max?: number; - min?: number; - }; - repeat: { - max?: number; - min?: number; - }; - strategy: string; - mustBeCompleted: boolean; - type: string; - promotions: unknown[]; - engagementConfiguration?: unknown; +import { TAddon } from '@ovh-ux/manager-pci-common'; +import { v6 } from '@ovh-ux/manager-core-api'; +import { TRegion } from '@/api/data/regions'; + +export type TCatalogGroup = { + name: string; + tags: string[]; }; + +export type TVolumePricing = Pick & { + regions: TRegion['name'][]; + showAvailabilityZones: boolean; + interval: 'day' | 'hour' | 'month' | 'none'; + specs: TAddon['blobs']['technical']; +}; + +export type TVolumeCatalogFilter = { + [key in 'deployment' | 'region']: TCatalogGroup[]; +}; + +export type TVolumeCatalogElementFilter = { + [Property in keyof TVolumeCatalogFilter]?: TVolumeCatalogFilter[Property][number]['name'][]; +}; + +export type TVolumeAddon = { + name: string; + tags: string[]; + filters: TVolumeCatalogElementFilter; + pricings: TVolumePricing[]; +}; + +export type TVolumeCatalog = { + filters: TVolumeCatalogFilter; + regions: TRegion[]; + models: TVolumeAddon[]; +}; + +export const getVolumeCatalog = async ( + projectId: string, +): Promise => + (await v6.get(`/cloud/project/${projectId}/catalog/volume`)) + .data; + +export function getLeastPrice(pricings: TVolumePricing[]) { + return pricings.reduce( + (leastPrice, p) => + leastPrice === null ? p.price : Math.min(p.price, leastPrice), + null, + ); +} + +export function getGroupLeastPrice( + group: TCatalogGroup, + regions: TRegion[], + models: TVolumeAddon[], +): number | null { + const groupRegions = regions + .filter((r) => r.type === group.name) + .map((r) => r.name); + + const hasGroupRegions = (p: TVolumePricing) => + p.regions.some(groupRegions.includes); + + return models + .map((m) => getLeastPrice(m.pricings.filter(hasGroupRegions))) + .filter((p) => p !== null) + .reduce( + (leastPrice, modelLeastPrice) => + leastPrice === null + ? modelLeastPrice + : Math.min(modelLeastPrice, leastPrice), + null, + ); +} diff --git a/packages/manager/apps/pci-block-storage/src/api/data/instance.ts b/packages/manager/apps/pci-block-storage/src/api/data/instance.ts index 4537681f5d29..f95aa1014de0 100644 --- a/packages/manager/apps/pci-block-storage/src/api/data/instance.ts +++ b/packages/manager/apps/pci-block-storage/src/api/data/instance.ts @@ -11,6 +11,7 @@ export interface Instance { planCode: string; operationIds: string[]; currentMonthOutgoingTraffic: number; + availabilityZone?: string; } export interface MonthlyBilling { diff --git a/packages/manager/apps/pci-block-storage/src/api/data/regions.ts b/packages/manager/apps/pci-block-storage/src/api/data/regions.ts index cb9f3fa65fcb..afb06cd26ccc 100644 --- a/packages/manager/apps/pci-block-storage/src/api/data/regions.ts +++ b/packages/manager/apps/pci-block-storage/src/api/data/regions.ts @@ -1,23 +1,16 @@ -import { fetchIcebergV6 } from '@ovh-ux/manager-core-api'; +import { TVolumeCatalogElementFilter } from '@/api/data/catalog'; export type TRegion = { name: string; - type: string; - status: string; - continentCode: string; - services: { - name: string; - status: string; - endpoint: string; - }[]; - datacenterLocation: string; + type: 'region-3-az' | 'region' | 'localzone'; + availabilityZones: string[]; + isInMaintenance: boolean; + isActivated: boolean; + country: string; + filters: TVolumeCatalogElementFilter; + datacenter: string; }; -export const getProjectRegions = async ( - projectId: string, -): Promise => { - const { data } = await fetchIcebergV6({ - route: `/cloud/project/${projectId}/region`, - }); - return data; -}; +export function isRegionWith3AZ(region: Pick) { + return region.type === 'region-3-az'; +} 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 7e2e0d2beb38..3a4984bca188 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 @@ -17,6 +17,7 @@ export type TVolume = { planCode: string; type: string; regionName: string; + availabilityZone?: string; }; export type VolumeOptions = { @@ -185,7 +186,7 @@ export interface AddVolumeProps { regionName: string; volumeCapacity: number; volumeType: string; - availabilityZone?: string; + availabilityZone: string | null; } export const addVolume = async ({ @@ -194,6 +195,7 @@ export const addVolume = async ({ regionName, volumeCapacity, volumeType, + availabilityZone, }: AddVolumeProps): Promise => { const { data } = await v6.post( `/cloud/project/${projectId}/region/${regionName}/volume`, @@ -201,6 +203,7 @@ export const addVolume = async ({ name, size: volumeCapacity, type: volumeType, + availabilityZone, }, ); diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useCatalog.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useCatalog.ts index f71c5d1cafad..7b6c2488d34f 100644 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useCatalog.ts +++ b/packages/manager/apps/pci-block-storage/src/api/hooks/useCatalog.ts @@ -1,12 +1,16 @@ 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({ @@ -14,3 +18,9 @@ export const useCatalog = () => { enabled: !!me, }); }; + +export const useVolumeCatalog = (projectId: string) => + useQuery({ + queryKey: ['projects', projectId, 'catalog', 'volume'], + queryFn: () => getVolumeCatalog(projectId), + }); diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useConsumptionVolumesAddon.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useConsumptionVolumesAddon.ts deleted file mode 100644 index 9e041a711423..000000000000 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useConsumptionVolumesAddon.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useEffect, useState } from 'react'; -import { TCatalog } from '@ovh-ux/manager-pci-common'; -import { TRegion } from '@/api/data/regions'; -import { useCatalog } from '@/api/hooks/useCatalog'; -import { useProjectsAvailableVolumes } from '@/api/hooks/useProjectsAvailableVolumes'; - -export const useConsumptionVolumesAddon = ( - projectId: string, - region: TRegion, -) => { - const [state, setState] = useState(undefined); - - const { data: catalog, isPending: isCatalogPending } = useCatalog(); - const { - data: availableVolumes, - isPending: isVolumesPending, - } = useProjectsAvailableVolumes(projectId); - const isPending = isCatalogPending || isVolumesPending; - - useEffect(() => { - if (catalog && availableVolumes && region) { - // Get volumes addons ids - const volumeAddonsIds = catalog.plans - .find((plan) => plan.planCode === 'project') - .addonFamilies.find(({ name }) => name === 'volume').addons; - - // Get volumes addons details - const totalConsumptionVolumeAddons = catalog.addons.filter( - (addon) => - volumeAddonsIds.includes(addon.planCode) && - addon.planCode.includes('consumption'), - ); - - const volumeAddonsIdss = availableVolumes.plans?.filter(({ regions }) => - regions.some(({ name }) => name === region.name), - ); - - setState( - totalConsumptionVolumeAddons.filter(({ planCode }) => - volumeAddonsIdss?.some(({ code }) => code === planCode), - ), - ); - } - }, [catalog, region, availableVolumes]); - - return { volumeTypes: state, isPending }; -}; diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useHas3AZRegion.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useHas3AZRegion.ts new file mode 100644 index 000000000000..e992ac54d208 --- /dev/null +++ b/packages/manager/apps/pci-block-storage/src/api/hooks/useHas3AZRegion.ts @@ -0,0 +1,13 @@ +import { useVolumeCatalog } from '@/api/hooks/useCatalog'; + +export const useHas3AZRegion = (projectId: string) => { + const { data: volumeCatalog, isPending } = useVolumeCatalog(projectId); + + return { + has3AZ: + volumeCatalog?.models.some((m) => + m.filters.deployment.includes('region-3-az'), + ) || false, + isPending, + }; +}; diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useInstance.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useInstance.ts index 0e2709f8c3f3..feadd2f4cf88 100644 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useInstance.ts +++ b/packages/manager/apps/pci-block-storage/src/api/hooks/useInstance.ts @@ -11,8 +11,7 @@ export const getInstanceQueryKey = (projectId: string, instanceId: string) => [ export const useInstance = (projectId: string, instanceId: string) => useQuery({ queryKey: getInstanceQueryKey(projectId, instanceId), - queryFn: (): Promise> => - getInstance(projectId, instanceId), + queryFn: (): Promise => getInstance(projectId, instanceId), enabled: !!instanceId, }); @@ -25,7 +24,7 @@ export const getInstancesQueryKey = (projectId: string, region: string) => [ export const useInstances = (projectId: string, region: string) => useQuery({ queryKey: getInstancesQueryKey(projectId, region), - queryFn: (): Promise[]> => - getInstancesByRegion(projectId, region) as Promise[]>, + queryFn: (): Promise => + getInstancesByRegion(projectId, region) as Promise, enabled: !!region, }); diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useProjectsAvailableVolumes.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useProjectsAvailableVolumes.ts deleted file mode 100644 index ab08f644f4e3..000000000000 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useProjectsAvailableVolumes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useMe } from '@ovh-ux/manager-react-components'; -import { getProjectsAvailableVolumes } from '@/api/data/availableVolumes'; - -export const getProjectsAvailableVolumesQuery = ( - projectId: string, - ovhSubsidiary: string, -) => ({ - queryKey: ['project', projectId, 'availableVolumes'], - queryFn: () => getProjectsAvailableVolumes(projectId, ovhSubsidiary), -}); - -export const useProjectsAvailableVolumes = (projectId: string) => { - const { me } = useMe(); - return useQuery({ - ...getProjectsAvailableVolumesQuery(projectId, me?.ovhSubsidiary), - enabled: !!me, - }); -}; diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useRegions.ts b/packages/manager/apps/pci-block-storage/src/api/hooks/useRegions.ts deleted file mode 100644 index 39cc2c56c89c..000000000000 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useRegions.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useTranslation } from 'react-i18next'; -import { - getMacroRegion, - useTranslatedMicroRegions, -} from '@ovh-ux/manager-react-components'; -import { TRegion, getProjectRegions } from '@/api/data/regions'; - -export const useProjectRegions = (projectId: string) => - useQuery({ - queryKey: ['project', projectId, 'regions'], - queryFn: () => getProjectRegions(projectId), - }); - -export interface TContinent { - id: string; // unique ID - code: string; // continent code - name: string; // translated continent name -} - -export type TLocalisation = TRegion & { - isMacro: boolean; // is a macro region ? - isLocalZone: boolean; // is a localzone ? - macro: string; // associated macro region code - continentLabel: string; // WARNING: used to group regions - macroLabel: string; // translated macro region label - microLabel: string; // translated micro region label -}; - -export interface ProjectLocalisation { - regions: TLocalisation[]; - continents: TContinent[]; -} - -export const useProjectLocalisation = (projectId: string) => { - const { t: tLoc } = useTranslation('localisation'); - const { - translateMicroRegion, - translateMacroRegion, - translateContinentRegion, - } = useTranslatedMicroRegions(); - - return useQuery({ - queryKey: ['project', projectId, 'localisation'], - queryFn: () => getProjectRegions(projectId), - select: (data): ProjectLocalisation => { - const regions = data - .map((region) => { - const macro = getMacroRegion(region.name); - return { - ...region, - isMacro: region.name === macro, - macro, - macroLabel: translateMacroRegion(region.name) || macro, - microLabel: translateMicroRegion(region.name) || region.name, - continentLabel: translateContinentRegion(region.name) || macro, - isLocalZone: region.type === 'localzone', - }; - }) - .sort(({ name: a }, { name: b }) => { - const regionA = a.replace(/[\d]+/, ''); - const regionB = b.replace(/[\d]+/, ''); - if (regionA === regionB) { - const regionIdA = parseInt(a.replace(/[^\d]+/, ''), 10) || 0; - const regionIdB = parseInt(b.replace(/[^\d]+/, ''), 10) || 0; - return regionIdA - regionIdB; - } - return regionA.localeCompare(regionB); - }); - - const allContinents = { - id: 'WORLD', - code: 'WORLD', - name: tLoc('pci_project_regions_list_continent_all'), - }; - - const continents = [...new Set(regions.map((r) => r.continentLabel))].map( - (c) => { - const region = regions.find((r) => r.continentLabel === c); - return { - id: region.macro, - code: region.continentCode, - name: c, - }; - }, - ); - - return { - regions, - continents: [allContinents, ...continents], - }; - }, - }); -}; diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.spec.tsx b/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.spec.tsx index 36eba921e20e..fe633e47fe06 100644 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.spec.tsx +++ b/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.spec.tsx @@ -29,12 +29,6 @@ vi.mock('@/api/data/volume', async (importOriginal) => { }; }); -vi.mock('@ovh-ux/manager-react-components', () => ({ - useTranslatedMicroRegions: vi.fn(() => ({ - translateMicroRegion: vi.fn((t) => t), - })), -})); - const queryClient = new QueryClient(); const wrapper = ({ children }) => ( @@ -98,7 +92,7 @@ describe('useVolumes', () => { size: 0, status: 'available', statusGroup: '', - region: 'region', + region: 'region1', bootable: false, planCode: '', type: '', @@ -113,7 +107,7 @@ describe('useVolumes', () => { size: 0, status: 'available', statusGroup: '', - region: 'region', + region: 'region2', bootable: false, planCode: '', type: '', @@ -147,8 +141,8 @@ describe('useVolumes', () => { id: '1', name: 'Volume 1', planCode: '', - region: 'region', - regionName: 'region', + region: 'region1', + regionName: 'manager_components_region_region_micro', size: 0, status: 'available', statusGroup: 'ACTIVE', @@ -162,8 +156,8 @@ describe('useVolumes', () => { id: '2', name: 'Volume 2', planCode: '', - region: 'region', - regionName: 'region', + region: 'region2', + regionName: 'manager_components_region_region_micro', size: 0, status: 'available', statusGroup: 'ACTIVE', diff --git a/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.tsx b/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.tsx index b50220bf896f..042fbea296d7 100644 --- a/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.tsx +++ b/packages/manager/apps/pci-block-storage/src/api/hooks/useVolume.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react'; import { applyFilters, Filter } from '@ovh-ux/manager-core-api'; -import { useTranslatedMicroRegions } from '@ovh-ux/manager-react-components'; +import { getMacroRegion } from '@ovh-ux/manager-react-components'; import { useMutation, useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { addVolume, AddVolumeProps, @@ -22,7 +23,7 @@ import { UCENTS_FACTOR } from '@/hooks/currency-constants'; import queryClient from '@/queryClient'; export const useAllVolumes = (projectId: string) => { - const { translateMicroRegion } = useTranslatedMicroRegions(); + const { t } = useTranslation('region'); return useQuery({ queryKey: ['project', projectId, 'volumes'], queryFn: () => getAllVolumes(projectId), @@ -59,7 +60,12 @@ export const useAllVolumes = (projectId: string) => { return { ...volume, statusGroup, - regionName: translateMicroRegion(volume.region), + regionName: t( + `manager_components_region_${getMacroRegion(volume.region)}_micro`, + { + micro: volume.availabilityZone ?? volume.region, + }, + ), }; }), enabled: !!projectId, @@ -270,6 +276,7 @@ export const useAddVolume = ({ regionName, volumeCapacity, volumeType, + availabilityZone, onError, onSuccess, }: UseAddVolumeProps) => { @@ -281,6 +288,7 @@ export const useAddVolume = ({ regionName, volumeCapacity, volumeType, + availabilityZone, }), onError, onSuccess: async () => { diff --git a/packages/manager/apps/pci-block-storage/src/components/exten-banner-beta/ExtenBannerBeta.tsx b/packages/manager/apps/pci-block-storage/src/components/exten-banner-beta/ExtenBannerBeta.tsx index 8f18e6f7672c..d147c004ceed 100644 --- a/packages/manager/apps/pci-block-storage/src/components/exten-banner-beta/ExtenBannerBeta.tsx +++ b/packages/manager/apps/pci-block-storage/src/components/exten-banner-beta/ExtenBannerBeta.tsx @@ -9,34 +9,31 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { useMemo } from 'react'; import { FA_EXTEN_BANNER } from '@/api/data/quota'; -import { useProjectsAvailableVolumes } from '@/api/hooks/useProjectsAvailableVolumes'; +import { useVolumeCatalog } from '@/api/hooks/useCatalog'; -const extenProducts = [ - 'volume.high-speed-BETA.consumption', - 'volume.classic-BETA.consumption', -]; +const extenProducts = ['high-speed-BETA', 'classic-BETA']; function Banner() { const { t } = useTranslation('exten-banner-beta'); const { projectId } = useParams(); - const { data: availableVolumes } = useProjectsAvailableVolumes(projectId); + const { data: volumeCatalog } = useVolumeCatalog(projectId); const regionsString = useMemo( () => - availableVolumes + volumeCatalog ? [ ...new Set( - availableVolumes.plans - .filter((p) => extenProducts.includes(p.code)) - .flatMap((p) => p.regions) - .map((r) => r.name), + volumeCatalog.models + .filter((p) => extenProducts.includes(p.name)) + .flatMap((p) => p.pricings) + .flatMap((p) => p.regions), ), ].join(', ') : '', - [availableVolumes], + [volumeCatalog], ); - if (!availableVolumes) return null; + if (!volumeCatalog) return null; return ( diff --git a/packages/manager/apps/pci-block-storage/src/pages/attach/AttachStorage.page.tsx b/packages/manager/apps/pci-block-storage/src/pages/attach/AttachStorage.page.tsx index 395cdebe0b90..cfad04ec6d91 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/attach/AttachStorage.page.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/attach/AttachStorage.page.tsx @@ -7,7 +7,7 @@ import { } from '@ovhcloud/ods-components/react'; import { ODS_BUTTON_VARIANT, ODS_SPINNER_SIZE } from '@ovhcloud/ods-components'; import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Translation, useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import { useNotifications } from '@ovh-ux/manager-react-components'; @@ -22,10 +22,19 @@ export default function AttachStorage() { const { t } = useTranslation('attach'); const { addError, addSuccess } = useNotifications(); const { data: volume } = useVolume(projectId, volumeId); - const { data: instances, isPending: isInstancesPending } = useInstances( + const { data: dataInstances, isPending: isInstancesPending } = useInstances( projectId, volume?.region, ); + const instances = useMemo( + () => + !!dataInstances && !!volume.availabilityZone + ? dataInstances.filter( + (i) => i.availabilityZone === volume.availabilityZone, + ) + : dataInstances, + [dataInstances, volume], + ); const [selectedInstance, setSelectedInstance] = useState(null); const onClose = () => navigate('..'); diff --git a/packages/manager/apps/pci-block-storage/src/pages/edit/Edit.page.tsx b/packages/manager/apps/pci-block-storage/src/pages/edit/Edit.page.tsx index 617268f6c1c4..7de5724bc38a 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/edit/Edit.page.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/edit/Edit.page.tsx @@ -43,7 +43,7 @@ import HidePreloader from '@/core/HidePreloader'; import { useVolumeMaxSize } from '@/api/data/quota'; import { useRegionsQuota } from '@/api/hooks/useQuota'; import { PriceEstimate } from '@/pages/new/components/PriceEstimate'; -import { useCatalog } from '@/api/hooks/useCatalog'; +import { useVolumeCatalog } from '@/api/hooks/useCatalog'; type TFormState = { name: string; @@ -84,18 +84,23 @@ export default function EditPage() { isLoading: isLoadingVolume, isPending: isPendingVolume, } = useVolume(projectId, volumeId); - const { data: catalog } = useCatalog(); + const { data: catalog } = useVolumeCatalog(projectId); const catalogVolume = useMemo(() => { if (!!catalog && !!volume) { - return ( - catalog.addons.find((addon) => addon.planCode === volume.planCode) || - null - ); + return catalog.models.find((addon) => addon.name === volume.type) || null; } return null; }, [catalog, volume]); + const pricing = useMemo( + () => + catalogVolume + ? catalogVolume.pricings.find((p) => p.regions.includes(volume.region)) + : null, + [catalogVolume, volume], + ); + const { volumeMaxSize } = useVolumeMaxSize(volume?.region); const { @@ -427,7 +432,7 @@ export default function EditPage() {
)} 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 ddd1cea561b6..b0b774ea0924 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 @@ -43,7 +43,8 @@ export default function NewPage(): JSX.Element { name: stepper.form.volumeName, regionName: stepper.form.region?.name, volumeCapacity: stepper.form.volumeCapacity, - volumeType: stepper.form.volumeType?.blobs.technical.name, + volumeType: stepper.form.volumeType?.name, + availabilityZone: stepper.form.availabilityZone, onSuccess: () => { navigate('..'); addSuccess( @@ -159,7 +160,7 @@ export default function NewPage(): JSX.Element { {!!stepper.form.region?.name && ( )} @@ -179,6 +180,7 @@ export default function NewPage(): JSX.Element { projectId={projectId} region={stepper.form.region} volumeType={stepper.form.volumeType} + pricing={stepper.form.pricing} step={stepper.capacity.step} onSubmit={stepper.capacity.submit} /> @@ -206,7 +208,7 @@ export default function NewPage(): JSX.Element { > { clearNotifications(); stepper.validation.submit(); 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 index ad67d99e879f..6a8750b90b20 100644 --- 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 @@ -1,5 +1,5 @@ import { TilesInputComponent } from '@ovh-ux/manager-react-components'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { OsdsButton, OsdsText } from '@ovhcloud/ods-components/react'; import { ODS_BUTTON_SIZE } from '@ovhcloud/ods-components'; import { @@ -9,36 +9,35 @@ import { } from '@ovhcloud/ods-common-theming'; import { useTranslation } from 'react-i18next'; import { Step } from '@/pages/new/hooks/useStep'; +import { TRegion } from '@/api/data/regions'; -type Props = { - regionName: string; +interface AvailabilityZoneStepProps { + region: TRegion; step: Step; onSubmit: (zone: string) => void; -}; +} -export function AvailabilityZoneStep({ regionName, step, onSubmit }: Props) { +export function AvailabilityZoneStep({ + region, + step, + onSubmit, +}: Readonly) { 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} + items={ + step.isLocked && selectedZone + ? [selectedZone] + : region.availabilityZones + } value={selectedZone} - onInput={(z) => setSelectedZone(z)} + onInput={setSelectedZone} label={(z) => (
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/CapacityStep.component.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/CapacityStep.component.tsx index 0b4a2d059a92..210690846080 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/new/components/CapacityStep.component.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/CapacityStep.component.tsx @@ -22,21 +22,22 @@ 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, TVolumePricing } from '@/api/data/catalog'; +import { TRegion } from '@/api/data/regions'; 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; + region: TRegion; + volumeType: TVolumeAddon; + pricing: TVolumePricing; step: StepState; onSubmit: (volumeCapacity: number) => void; } @@ -45,6 +46,7 @@ export function CapacityStep({ projectId, region, volumeType, + pricing, step, onSubmit, }: Readonly) { @@ -146,8 +148,9 @@ export function CapacityStep({ - +
{volumeCapacity < VOLUME_MIN_SIZE && (
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeSelector.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeSelector.tsx new file mode 100644 index 000000000000..ff46ffdb58be --- /dev/null +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeSelector.tsx @@ -0,0 +1,29 @@ +import { TilesInputComponent } from '@ovh-ux/manager-react-components'; +import { TCatalogGroup } from '@/api/data/catalog'; +import { DeploymentModeTile } from '@/pages/new/components/DeploymentModeTile'; + +interface DeploymentModeSelectorProps { + deploymentGroups: TCatalogGroup[]; + selectedRegionGroup?: TCatalogGroup | null; + onChange?: (group: TCatalogGroup) => void; +} + +export const DeploymentModeSelector = ({ + deploymentGroups, + selectedRegionGroup, + onChange, +}: DeploymentModeSelectorProps) => ( +
+ + items={deploymentGroups} + value={selectedRegionGroup} + onInput={onChange} + label={(group) => ( + + )} + /> +
+); diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTile.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTile.tsx new file mode 100644 index 000000000000..4931656da076 --- /dev/null +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTile.tsx @@ -0,0 +1,129 @@ +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { useCatalogPrice } from '@ovh-ux/manager-react-components'; +import { useMemo } from 'react'; +import { OsdsChip, OsdsText } 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 } from '@ovhcloud/ods-components'; +import { + Region3AZChip, + RegionGlobalzoneChip, + RegionLocalzoneChip, +} from '@ovh-ux/manager-pci-common'; +import { clsx } from 'clsx'; +import { useVolumeCatalog } from '@/api/hooks/useCatalog'; +import { getGroupLeastPrice, TCatalogGroup } from '@/api/data/catalog'; + +export const RegionChipByGroupName = ({ + group, +}: Readonly<{ group: TCatalogGroup }>) => { + switch (group.name) { + case 'localzone': + return ; + case 'region': + return ; + case 'region-3-az': + return ; + default: + return null; + } +}; + +interface DeploymentModeTileProps { + group: TCatalogGroup; + selected?: boolean; +} + +export function DeploymentModeTile({ + group, + selected, +}: Readonly) { + const { t } = useTranslation(['add', 'common']); + const { projectId } = useParams(); + const { + data: { regions, models }, + } = useVolumeCatalog(projectId); + + const { getFormattedCatalogPrice } = useCatalogPrice(6, { + hideTaxLabel: true, + }); + + const leastPrice = useMemo(() => getGroupLeastPrice(group, regions, models), [ + group, + regions, + models, + ]); + + return ( +
+
+ + {t( + `pci_projects_project_storages_blocks_add_deployment_mode_title_${group.name}`, + )} + +
+
+ +
+
+ + {t( + `pci_projects_project_storages_blocks_add_deployment_mode_description_${group.name}`, + )} + +
+
+ {group.tags.includes('is_new') ? ( +
+ + {t('common:pci_projects_project_storages_blocks_new')} + + + {t('pci_projects_project_storages_blocks_add_beta_free')} + +
+ ) : ( + leastPrice !== null && ( + + {t( + 'pci_projects_project_storages_blocks_add_deployment_mode_price_from', + { + price: getFormattedCatalogPrice(leastPrice), + }, + )} + + ) + )} +
+
+ ); +} diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTileSummary.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTileSummary.tsx new file mode 100644 index 000000000000..41ef68e4189c --- /dev/null +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/DeploymentModeTileSummary.tsx @@ -0,0 +1,22 @@ +import { OsdsTile } from '@ovhcloud/ods-components/react'; +import { DeploymentModeTile } from '@/pages/new/components/DeploymentModeTile'; +import { TCatalogGroup } from '@/api/data/catalog'; + +interface DeploymentModeTileSummaryProps { + group: TCatalogGroup; +} + +export function DeploymentModeTileSummary({ + group, +}: Readonly) { + return ( +
+ + + +
+ ); +} diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/HighSpeedV2Infos.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/HighSpeedV2Infos.tsx index 9fc2281229b0..f5fd1f0c41ba 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/new/components/HighSpeedV2Infos.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/HighSpeedV2Infos.tsx @@ -5,9 +5,9 @@ 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, TVolumePricing } from '@/api/data/catalog'; -export const HIGHSPEED_V2_PLANCODE = 'volume.high-speed-gen2.consumption'; +export const HIGHSPEED_V2_PLANCODE = 'high-speed-gen2'; export function getDisplayUnit(unit: string) { const perGB = '/GB'; @@ -17,8 +17,11 @@ export function getDisplayUnit(unit: string) { return unit; } -export function computeBandwidthToAllocate(capacity: number, addon: TAddon) { - const { bandwidth } = addon.blobs.technical; +export function computeBandwidthToAllocate( + capacity: number, + pricing: TVolumePricing, +) { + const { bandwidth } = pricing.specs; const allocatedBandwidth = capacity * bandwidth.level; const maxBandwidthInMb = bandwidth.max * 1000; @@ -27,8 +30,11 @@ export function computeBandwidthToAllocate(capacity: number, addon: TAddon) { : `${maxBandwidthInMb} ${getDisplayUnit(bandwidth.unit)}`; } -export function computeIopsToAllocate(capacity: number, addon: TAddon) { - const { volume } = addon.blobs.technical; +export function computeIopsToAllocate( + capacity: number, + pricing: TVolumePricing, +) { + const { volume } = pricing.specs; const allocatedIops = capacity * volume.iops.level; return allocatedIops <= volume.iops.max @@ -38,15 +44,17 @@ export function computeIopsToAllocate(capacity: number, addon: TAddon) { export interface HighSpeedV2InfosProps { volumeCapacity: number; - volumeType: TAddon; + volumeType: TVolumeAddon; + pricing: TVolumePricing; } export function HighSpeedV2Infos({ volumeCapacity, volumeType, + pricing, }: HighSpeedV2InfosProps) { const { t } = useTranslation('add'); - const isHighSpeedV2 = volumeType?.planCode === HIGHSPEED_V2_PLANCODE; + const isHighSpeedV2 = volumeType.name === HIGHSPEED_V2_PLANCODE; return ( isHighSpeedV2 && (
@@ -56,7 +64,7 @@ export function HighSpeedV2Infos({ color={ODS_THEME_COLOR_INTENT.text} > {t('pci_projects_project_storages_blocks_add_size_bandwidth', { - bandwidth: computeBandwidthToAllocate(volumeCapacity, volumeType), + bandwidth: computeBandwidthToAllocate(volumeCapacity, pricing), })}
@@ -66,7 +74,7 @@ export function HighSpeedV2Infos({ color={ODS_THEME_COLOR_INTENT.text} > {t('pci_projects_project_storages_blocks_add_size_iops', { - iops: computeIopsToAllocate(volumeCapacity, volumeType), + iops: computeIopsToAllocate(volumeCapacity, pricing), })}
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/LocationStep.component.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/LocationStep.component.tsx index eeecacc71618..8d9d6cd1c7a7 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/new/components/LocationStep.component.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/LocationStep.component.tsx @@ -1,76 +1,169 @@ -import { useState } from 'react'; -import { OsdsButton } from '@ovhcloud/ods-components/react'; -import { ODS_BUTTON_SIZE } from '@ovhcloud/ods-components'; +import { useCallback, useContext, useMemo, useState } from 'react'; +import { + OsdsButton, + OsdsLink, + OsdsSpinner, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ODS_BUTTON_SIZE, + ODS_SPINNER_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, + PCICommonContext, RegionSelector, RegionSummary, + TLocalisation, usePCICommonContextFactory, - PCICommonContext, + useProject, } from '@ovh-ux/manager-pci-common'; -import { TLocalisation } from '@/api/hooks/useRegions'; +import { Subtitle } from '@ovh-ux/manager-react-components'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { StepState } from '@/pages/new/hooks/useStep'; -import { useProjectsAvailableVolumes } from '@/api/hooks/useProjectsAvailableVolumes'; -import { isRegionWith3AZ } from '@/api/data/availableVolumes'; +import { DeploymentModeSelector } from '@/pages/new/components/DeploymentModeSelector'; +import { useVolumeCatalog } from '@/api/hooks/useCatalog'; +import { TCatalogGroup } from '@/api/data/catalog'; +import { DeploymentModeTileSummary } from '@/pages/new/components/DeploymentModeTileSummary'; +import { useHas3AZRegion } from '@/api/hooks/useHas3AZRegion'; +import { TRegion } from '@/api/data/regions'; +import { GLOBAL_INFRASTRUCTURE_URL } from '@/pages/new/components/website-link'; interface LocationProps { projectId: string; step: StepState; - onSubmit: (region: TLocalisation) => void; + onSubmit: (region: TRegion) => void; } -const useHas3AZRegion = (projectId: string) => { - const { data: availableVolumes, isPending } = useProjectsAvailableVolumes( - projectId, - ); - - return { - has3AZ: - availableVolumes?.plans.some((p) => p.regions.some(isRegionWith3AZ)) || - false, - isPending, - }; -}; - export function LocationStep({ projectId, step, - onSubmit, + onSubmit: parentSubmit, }: Readonly) { - const { t: tStepper } = useTranslation('stepper'); - const [region, setRegion] = useState(undefined); + const { t } = useTranslation(['stepper', 'add']); + const { data: volumeCatalog, isPending } = useVolumeCatalog(projectId); + const context = useContext(ShellContext); + const { ovhSubsidiary } = context.environment.getUser(); + + const [ + selectedRegionGroup, + setSelectedRegionGroup, + ] = useState(null); + const [selectedLocalisation, setSelectedLocalisation] = useState< + TLocalisation + >(undefined); + const selectedRegion = useMemo( + () => + selectedLocalisation + ? volumeCatalog.regions.find( + (r) => r.name === selectedLocalisation.name, + ) + : null, + [volumeCatalog, selectedLocalisation], + ); + const { data: project } = useProject(); const isDiscovery = isDiscoveryProject(project); - const hasRegion = !!region; + const hasRegion = !!selectedLocalisation; const { has3AZ } = useHas3AZRegion(projectId); const pciCommonProperties = usePCICommonContextFactory({ has3AZ }); + const regions = useMemo( + () => + selectedRegionGroup + ? volumeCatalog?.regions.filter((r) => + r.filters.deployment.includes(selectedRegionGroup.name), + ) + : volumeCatalog?.regions, + [volumeCatalog, selectedRegionGroup], + ); + + const onSubmit = useCallback(() => { + setSelectedRegionGroup( + volumeCatalog.filters.deployment.find( + (g) => g.name === selectedRegion.filters.deployment[0], + ), + ); + parentSubmit(selectedRegion); + }, [volumeCatalog, selectedRegion, parentSubmit]); + + if (isPending) return ; + return ( - {hasRegion && step.isLocked && } + {hasRegion && step.isLocked && ( +
+ + +
+ )} {(!step.isLocked || isDiscovery) && ( - - r.isMacro || - r.services.some((s) => s.name === 'volume' && s.status === 'UP') || - r.type === 'region-3-az' - } - /> +
+
+ + {t( + 'add:pci_projects_project_storages_blocks_add_deployment_mode_title', + )} + +
+ + + ), + }} + /> + +
+
+ { + setSelectedLocalisation(undefined); + setSelectedRegionGroup(group); + }} + /> + +
+ + {t('add:pci_projects_project_storages_blocks_add_region_title')} + +
+ + r.isMacro || regions.some((r2) => r2.name === r.name) + } + /> +
)} {hasRegion && !step.isLocked && ( onSubmit(region)} + onClick={onSubmit} > - {tStepper('common_stepper_next_button_label')} + {t('common_stepper_next_button_label')} )}
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/PriceEstimate.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/PriceEstimate.tsx index b0d576b66555..0f5fade2fc07 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/new/components/PriceEstimate.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/PriceEstimate.tsx @@ -9,24 +9,21 @@ import { useCatalogPrice, convertHourlyPriceToMonthly, } from '@ovh-ux/manager-react-components'; -import { TAddon } from '@ovh-ux/manager-pci-common'; +import { TVolumePricing } from '@/api/data/catalog'; export interface PriceEstimateProps { volumeCapacity: number; - volumeType: TAddon; + pricing: TVolumePricing; } -export function PriceEstimate({ - volumeCapacity, - volumeType, -}: PriceEstimateProps) { +export function PriceEstimate({ volumeCapacity, pricing }: PriceEstimateProps) { const { t } = useTranslation('add'); const { getFormattedCatalogPrice } = useCatalogPrice(3, { hideTaxLabel: true, }); - const { price } = - volumeType?.pricings?.find(({ type }) => type === 'consumption') || {}; - const priceEstimate = convertHourlyPriceToMonthly(price * volumeCapacity); + const priceEstimate = convertHourlyPriceToMonthly( + pricing.price * volumeCapacity, + ); return ( void; } export function ValidationStep({ volumeCapacity, - volumeType, + pricing, onSubmit, }: Readonly) { const { t } = useTranslation('add'); const navigate = useNavigate(); const { clearNotifications } = useNotifications(); + return (
- +
diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/VolumeTypeStep.component.tsx b/packages/manager/apps/pci-block-storage/src/pages/new/components/VolumeTypeStep.component.tsx index 0b9a11cb0b18..bbb954854d89 100644 --- a/packages/manager/apps/pci-block-storage/src/pages/new/components/VolumeTypeStep.component.tsx +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/VolumeTypeStep.component.tsx @@ -1,37 +1,123 @@ -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 { useParams } from 'react-router-dom'; 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'; +import { TRegion } from '@/api/data/regions'; + +const BETA_TAG = 'is_new'; + +function VolumeTypeTile({ + volumeType, + currentRegion, +}: Readonly<{ volumeType: TVolumeAddon; currentRegion: TRegion }>) { + const { t } = useTranslation(['add', 'common']); + const { projectId } = useParams(); + const { + data: { filters }, + } = useVolumeCatalog(projectId); + const tBytes = useTranslateBytes(); + const { getFormattedCatalogPrice } = useCatalogPrice(6, { + hideTaxLabel: true, + }); + + const pricing = volumeType.pricings[0]; + + const isNew = useMemo( + () => + volumeType.tags.includes(BETA_TAG) || + filters.deployment.some( + (deployment) => + currentRegion.filters.deployment.includes(deployment.name) && + deployment.tags.includes(BETA_TAG), + ), + [filters, volumeType], + ); + + return ( +
+
+ + {volumeType.name} + + {isNew && ( + + {t('common:pci_projects_project_storages_blocks_new')} + + )} +
+
+ + {pricing.specs.volume.iops.guaranteed + ? t( + 'pci_projects_project_storages_blocks_add_type_addon_iops_guaranteed', + { + iops: pricing.specs.volume.iops.level, + separator: ', ', + }, + ) + : t( + 'pci_projects_project_storages_blocks_add_type_addon_iops_not_guaranteed', + { + iops: + pricing.specs.volume.iops.max || + pricing.specs.volume.iops.level, + separator: ', ', + }, + )} + {t( + 'pci_projects_project_storages_blocks_add_type_addon_capacity_max', + { + capacity: tBytes( + pricing.specs.volume.capacity.max, + 0, + false, + 'GB', + false, + ), + }, + )}{' '} +
+ {t('pci_projects_project_storages_blocks_add_type_addon_price', { + price: getFormattedCatalogPrice(volumeType.pricings[0]?.price), + })} +
+
+
+ ); +} export interface VolumeTypeStepProps { projectId: string; - region: TLocalisation; + region: TRegion; step: StepState; - onSubmit: (volumeType: TAddon) => void; + onSubmit: (volumeType: TVolumeAddon) => void; } export function VolumeTypeStep({ @@ -40,97 +126,32 @@ export function VolumeTypeStep({ step, onSubmit, }: Readonly) { - const { t } = useTranslation('add'); - const { t: tStepper } = useTranslation('stepper'); - const { t: tCommon } = useTranslation('common'); - const [volumeType, setVolumeType] = useState(undefined); - const tBytes = useTranslateBytes(); - const { getFormattedCatalogPrice } = useCatalogPrice(6, { - hideTaxLabel: true, - }); + const { t } = useTranslation('stepper'); + const { data } = useVolumeCatalog(projectId); - const { volumeTypes, isPending } = useConsumptionVolumesAddon( - projectId, - region, + const [volumeType, setVolumeType] = useState(undefined); + + const volumeTypes = useMemo( + () => + data?.models + .map((m) => ({ + ...m, + pricings: m.pricings.filter((p) => p.regions.includes(region.name)), + })) + .filter((m) => m.pricings.length > 0) || [], + [data, region], ); const displayedTypes = volumeType && step.isLocked ? [volumeType] : volumeTypes; - if (isPending) { - return ; - } - return ( <> - + value={volumeType} items={displayedTypes || []} - label={(vType: TCatalog['addons'][0]) => ( -
-
- - {vType.blobs.technical.name} - - {vType.blobs.tags.includes('is_new') && ( - - {tCommon('pci_projects_project_storages_blocks_new')} - - )} -
-
- - {vType.blobs.technical.volume.iops.guaranteed - ? t( - 'pci_projects_project_storages_blocks_add_type_addon_iops_guaranteed', - { - iops: vType.blobs.technical.volume.iops.level, - separator: ', ', - }, - ) - : t( - 'pci_projects_project_storages_blocks_add_type_addon_iops_not_guaranteed', - { - iops: - vType.blobs.technical.volume.iops.max || - vType.blobs.technical.volume.iops.level, - separator: ', ', - }, - )} - {t( - 'pci_projects_project_storages_blocks_add_type_addon_capacity_max', - { - capacity: tBytes( - vType.blobs.technical.volume.capacity.max, - 0, - false, - 'GB', - false, - ), - }, - )}{' '} -
- {t( - 'pci_projects_project_storages_blocks_add_type_addon_price', - { - price: getFormattedCatalogPrice(vType.pricings[0]?.price), - }, - )} -
-
-
+ label={(vType: TVolumeAddon) => ( + )} onInput={setVolumeType} /> @@ -142,7 +163,7 @@ export function VolumeTypeStep({ onClick={() => onSubmit(volumeType)} className="w-fit" > - {tStepper('common_stepper_next_button_label')} + {t('common_stepper_next_button_label')}
)} diff --git a/packages/manager/apps/pci-block-storage/src/pages/new/components/website-link.ts b/packages/manager/apps/pci-block-storage/src/pages/new/components/website-link.ts new file mode 100644 index 000000000000..144bf4a819f1 --- /dev/null +++ b/packages/manager/apps/pci-block-storage/src/pages/new/components/website-link.ts @@ -0,0 +1,23 @@ +export const GLOBAL_INFRASTRUCTURE_URL = { + ASIA: 'https://www.ovhcloud.com/asia/about-us/global-infrastructure/', + AU: 'https://www.ovhcloud.com/en-au/about-us/global-infrastructure/', + CA: 'https://www.ovhcloud.com/en-ca/about-us/global-infrastructure/', + GB: 'https://www.ovhcloud.com/en-gb/about-us/global-infrastructure/', + IE: 'https://www.ovhcloud.com/en-ie/about-us/global-infrastructure/', + IN: 'https://www.ovhcloud.com/en-in/about-us/global-infrastructure/', + SG: 'https://www.ovhcloud.com/en-sg/about-us/global-infrastructure/', + DE: 'https://www.ovhcloud.com/de/about-us/global-infrastructure/', + ES: 'https://www.ovhcloud.com/es-es/about-us/global-infrastructure/', + FR: 'https://www.ovhcloud.com/fr/about-us/global-infrastructure/', + IT: 'https://www.ovhcloud.com/it/about-us/global-infrastructure/', + MA: 'https://www.ovhcloud.com/fr-ma/about-us/global-infrastructure/', + SN: 'https://www.ovhcloud.com/fr-sn/about-us/global-infrastructure/', + TN: 'https://www.ovhcloud.com/fr-tn/about-us/global-infrastructure/', + NL: 'https://www.ovhcloud.com/nl/about-us/global-infrastructure/', + PL: 'https://www.ovhcloud.com/nl/about-us/global-infrastructure/', + PT: 'https://www.ovhcloud.com/pt/about-us/global-infrastructure/', + QC: 'https://www.ovhcloud.com/fr-ca/about-us/global-infrastructure/', + US: 'https://www.ovhcloud.com/en/about-us/global-infrastructure/', + WS: 'https://www.ovhcloud.com/es/about-us/global-infrastructure/', + DEFAULT: 'https://www.ovhcloud.com/en/about-us/global-infrastructure/', +}; 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 2752ed1ee007..048413a06067 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 @@ -1,9 +1,10 @@ -import { TAddon } from '@ovh-ux/manager-pci-common'; -import { TLocalisation } from '@/api/hooks/useRegions'; +import { TVolumeAddon, TVolumePricing } from '@/api/data/catalog'; +import { TRegion } from '@/api/data/regions'; export type TFormState = { - region: TLocalisation; - volumeType: TAddon; + region: TRegion; + volumeType: TVolumeAddon; + pricing: TVolumePricing; volumeName: string; volumeCapacity: number; availabilityZone: string; 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 05c3b89f698a..d12dac7d4741 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,18 +1,12 @@ -import { useEffect, useMemo, useState } from 'react'; -import { TAddon, useProjectRegions } from '@ovh-ux/manager-pci-common'; +import { useEffect, useState } from 'react'; 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'; +import { useHas3AZRegion } from '@/api/hooks/useHas3AZRegion'; +import { isRegionWith3AZ, TRegion } from '@/api/data/regions'; export function useVolumeStepper(projectId: string) { - const { data } = useProjectRegions(projectId); - const is3AZAvailable = useMemo(() => !!data && data.some(isRegionWith3AZ), [ - data, - ]); + const { has3AZ } = useHas3AZRegion(projectId); const [form, setForm] = useState>({}); const locationStep = useStep({ isOpen: true }); @@ -23,12 +17,12 @@ export function useVolumeStepper(projectId: string) { const validationStep = useStep(); useEffect(() => { - if (is3AZAvailable) { + if (has3AZ) { availabilityZoneStep.show(); } else { availabilityZoneStep.hide(); } - }, [is3AZAvailable]); + }, [has3AZ]); const order = [ locationStep, @@ -58,11 +52,11 @@ export function useVolumeStepper(projectId: string) { }); setForm({}); }, - submit: (region: TLocalisation) => { + submit: (region: TRegion) => { locationStep.check(); locationStep.lock(); volumeTypeStep.open(); - if (is3AZAvailable) { + if (has3AZ) { if (isRegionWith3AZ(region)) { availabilityZoneStep.show(); } else { @@ -91,14 +85,13 @@ export function useVolumeStepper(projectId: string) { }); setForm((f) => ({ region: f.region })); }, - submit: (volumeType: TAddon) => { + submit: (volumeType: TVolumeAddon) => { volumeTypeStep.check(); volumeTypeStep.lock(); - if ( - is3AZAvailable && - isRegionWith3AZ(form.region) && - isProductWithAvailabilityZone(volumeType.planCode) - ) { + const pricing = volumeType.pricings.find((p) => + p.regions.includes(form.region.name), + ); + if (has3AZ && pricing.showAvailabilityZones) { availabilityZoneStep.show(); availabilityZoneStep.open(); } else { @@ -108,24 +101,24 @@ export function useVolumeStepper(projectId: string) { setForm((f) => ({ ...f, volumeType, + pricing, })); }, }, availabilityZone: { step: availabilityZoneStep, edit: () => { - volumeTypeStep.unlock(); - [ - availabilityZoneStep, - capacityStep, - volumeNameStep, - validationStep, - ].forEach((step) => { + availabilityZoneStep.unlock(); + [capacityStep, volumeNameStep, validationStep].forEach((step) => { step.uncheck(); step.unlock(); step.close(); }); - setForm((f) => ({ region: f.region, volumeType: f.volumeType })); + setForm((f) => ({ + region: f.region, + volumeType: f.volumeType, + pricing: f.pricing, + })); }, submit: (availabilityZone: string) => { availabilityZoneStep.check(); @@ -149,6 +142,7 @@ export function useVolumeStepper(projectId: string) { setForm((f) => ({ region: f.region, volumeType: f.volumeType, + pricing: f.pricing, availabilityZone: f.availabilityZone, })); }, @@ -172,6 +166,7 @@ export function useVolumeStepper(projectId: string) { setForm((f) => ({ region: f.region, volumeType: f.volumeType, + pricing: f.pricing, availabilityZone: f.availabilityZone, volumeCapacity: f.volumeCapacity, }));