diff --git a/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/1AZ.svg b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/1AZ.svg new file mode 100644 index 000000000000..cac97f3ed8fc --- /dev/null +++ b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/1AZ.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/3AZ.svg b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/3AZ.svg new file mode 100644 index 000000000000..046dd63ec17e --- /dev/null +++ b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/3AZ.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/LZ.svg b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/LZ.svg new file mode 100644 index 000000000000..b71fdc0cc66f --- /dev/null +++ b/packages/manager/apps/pci-object-storage/public/assets/deploymentRegion/LZ.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/manager/apps/pci-object-storage/public/translations/pci-object-storage/order-funnel/Messages_fr_FR.json b/packages/manager/apps/pci-object-storage/public/translations/pci-object-storage/order-funnel/Messages_fr_FR.json index a4f4c3a30812..7bb6accddcda 100644 --- a/packages/manager/apps/pci-object-storage/public/translations/pci-object-storage/order-funnel/Messages_fr_FR.json +++ b/packages/manager/apps/pci-object-storage/public/translations/pci-object-storage/order-funnel/Messages_fr_FR.json @@ -20,8 +20,10 @@ "labelEncryption": "Chiffrement vos données", "descriptionEncryption": "Les données déversées dans ce conteneur sont chiffrées à la volée par OVHcloud.", "summaryTitle": "Résumé", - "pricingDisclaimer": "** Le prix affiché est une estimation pour 1 To d'Object Storage Standard pour 730 heures. Pour plus d'informations, <0>voir la page des prix.", - "orderButton": "Commander", + "pricingDisclaimer": " Pour plus d'informations sur les tarifs, <0>voir la page des prix.", + "regionActivationInfo": "Si une région n'est pas encore disponible sur votre projet. Vous pouvez l'activer en suivant ce lien : <0>activer la région.", + "regionsNoMatch": "Aucune région ne correspond à vos filtres. Ajustez vos critères ou <0>activez une région.", + "orderButton": "Créer", "discoveryModeActivate": "Vous êtes actuellement en mode « Découverte ». Pour finaliser la création de votre conteneur, vous devez activer votre projet.", "discoveryModeActivateButton": "Activer votre projet", "summaryContainerSection": "Conteneur", @@ -74,5 +76,13 @@ "offsiteReplicationRegionPlaceholder": "Sélectionnez une localisation", "offsiteReplicationRegionSearchPlaceholder": "Chercher une localisation", "offsiteReplicationRegionhSearchNoResult": "Aucune localisation trouvée", - "offsiteReplicationVersioningAlert": "En activant l’Offsite Replication, le versioning s’active également." + "offsiteReplicationVersioningAlert": "En activant l'Offsite Replication, le versioning s'active également.", + "pci_instances_common_instance_region_deployment_mode": "Région 1-AZ", + "pci_instances_common_instance_region-3-az_deployment_mode": "Région 3-AZ", + "pci_instances_common_instance_localzone_deployment_mode": "Local Zone", + "pci_instances_common_instance_region_deployment_mode_description": "Déploiement résilient et économique sur 1 zone de disponibilité.", + "pci_instances_common_instance_region-3-az_deployment_mode_description": "Déploiement haute résilience/haute disponibilité pour vos applications critiques sur 3 zones de disponibilité.", + "pci_instances_common_instance_localzone_deployment_mode_description": "Déploiement de vos applications au plus près de vos utilisateurs pour une faible latence et la résidence des données.", + "selectGeographicalZone": "Selectionnez une zone géographique", + "showAllRegions": "Affichez toutes les régions" } diff --git a/packages/manager/apps/pci-object-storage/src/components/region-type-badge/RegionTypeBadge.component.tsx b/packages/manager/apps/pci-object-storage/src/components/region-type-badge/RegionTypeBadge.component.tsx new file mode 100644 index 000000000000..9f44d5cc7273 --- /dev/null +++ b/packages/manager/apps/pci-object-storage/src/components/region-type-badge/RegionTypeBadge.component.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next'; +import { + Badge, + Popover, + PopoverTrigger, + PopoverContent, +} from '@datatr-ux/uxlib'; +import { ExternalLink, HelpCircle } from 'lucide-react'; +import { RegionTypeEnum } from '@datatr-ux/ovhcloud-types/cloud/index'; + +import A from '@/components/links/A.component'; + +const getBadgeConfig = (type: RegionTypeEnum) => { + switch (type) { + case RegionTypeEnum.region: + return { + label: '1-AZ', + className: 'bg-primary-400 text-white', + }; + case RegionTypeEnum['region-3-az']: + return { + label: '3-AZ', + className: 'bg-primary-500 text-white', + }; + case RegionTypeEnum.localzone: + return { + label: 'LocalZone', + className: 'bg-primary-300 text-white', + }; + default: + return { + label: '?', + className: 'bg-neutral-100 text-text', + }; + } +}; + +interface RegionTypeBadgeProps { + type: RegionTypeEnum; + className?: string; +} + +export const RegionTypeBadge = ({ type, className }: RegionTypeBadgeProps) => { + const config = getBadgeConfig(type); + + return ( + + {config.label} + + ); +}; + +interface RegionTypeBadgeWithPopoverProps extends RegionTypeBadgeProps { + showPopover?: boolean; +} + +export const RegionTypeBadgeWithPopover = ({ + type, + className, + showPopover = true, +}: RegionTypeBadgeWithPopoverProps) => { + const { t } = useTranslation(['regions', 'pci-object-storage/order-funnel']); + + const helpLink = ( + + {t('help-link-more-info')} + + + ); + + const getDescription = (regionType: RegionTypeEnum) => { + switch (regionType) { + case RegionTypeEnum.region: + return t('region-description-1AZ'); + case RegionTypeEnum['region-3-az']: + return t('region-description-3AZ'); + case RegionTypeEnum.localzone: + return t('region-description-localzone'); + default: + return ''; + } + }; + + if (!showPopover) { + return ; + } + + const config = getBadgeConfig(type); + const description = getDescription(type); + + return ( + + {config.label} + {description && ( + + + + + + {description} + {helpLink} + + + )} + + ); +}; diff --git a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/OrderPricing.component.tsx b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/OrderPricing.component.tsx index 1ff16eead837..45e218dec57f 100644 --- a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/OrderPricing.component.tsx +++ b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/OrderPricing.component.tsx @@ -5,6 +5,7 @@ import order from '@/types/Order'; const HOUR_IN_MONTH = 730; const MEGA_BYTES = 1024; + const OrderPricing = ({ pricings, }: { @@ -42,36 +43,6 @@ const OrderPricing = ({ )} - {pricings.replication && ( - - - {t('pricing_option_replication_label')} - - - - - - )} - - - {t('total_monthly_label')} - - - - - diff --git a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionDeploymentSelection.tsx b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionDeploymentSelection.tsx new file mode 100644 index 000000000000..a1ecf2e0e325 --- /dev/null +++ b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionDeploymentSelection.tsx @@ -0,0 +1,151 @@ +import { Card, CardHeader, Checkbox } from '@datatr-ux/uxlib'; +import { useTranslation } from 'react-i18next'; +import cloud from '@/types/Cloud'; +import { cn } from '@/lib/utils'; +import { RegionTypeBadge } from './RegionTypeBadge.component'; + +type TDeploymentMode = { + mode: cloud.RegionTypeEnum; + title: string; + description: string; + Image: () => JSX.Element; + isDefaultActive?: boolean; +}; + +type TDeploymentModeConfig = { + mode: cloud.RegionTypeEnum; + imagePath: string; + isDefaultActive: boolean; +}; + +export type TDeploymentModeSelectionProps = { + value: cloud.RegionTypeEnum[]; + onChange: (modes: cloud.RegionTypeEnum[]) => void; +}; + +const DEPLOYMENT_MODES_CONFIG: TDeploymentModeConfig[] = [ + { + mode: cloud.RegionTypeEnum['region-3-az'], + imagePath: 'assets/deploymentRegion/3AZ.svg', + isDefaultActive: true, + }, + { + mode: cloud.RegionTypeEnum.region, + imagePath: 'assets/deploymentRegion/1AZ.svg', + isDefaultActive: true, + }, + { + mode: cloud.RegionTypeEnum.localzone, + imagePath: 'assets/deploymentRegion/LZ.svg', + isDefaultActive: false, + }, +]; + +export const getDefaultDeploymentModes = (): cloud.RegionTypeEnum[] => { + return DEPLOYMENT_MODES_CONFIG.filter((config) => config.isDefaultActive).map( + (config) => config.mode, + ); +}; + +const Icon = ({ + imagePath, + width = 288, + height = 170, +}: { + imagePath: string; + width?: number; + height?: number; +}) => ( +
+); + +export const DeploymentModeSelection = ({ + value, + onChange, +}: TDeploymentModeSelectionProps) => { + const { t } = useTranslation('pci-object-storage/order-funnel'); + + const getTranslationKey = ( + mode: cloud.RegionTypeEnum, + type: 'title' | 'description', + ): string => { + const modeKey = mode; + const suffix = + type === 'title' ? 'deployment_mode' : 'deployment_mode_description'; + return `pci_instances_common_instance_${modeKey}_${suffix}`; + }; + + const deploymentModes: TDeploymentMode[] = DEPLOYMENT_MODES_CONFIG.map( + (config) => ({ + mode: config.mode, + title: t(getTranslationKey(config.mode, 'title')), + description: t(getTranslationKey(config.mode, 'description')), + Image: () => ( + + ), + isDefaultActive: config.isDefaultActive, + }), + ); + + const handleSelect = (selectedMode: cloud.RegionTypeEnum) => () => { + const currentValue = value || []; + const isSelected = value?.some((item) => item === selectedMode) || false; + + const selection = isSelected + ? currentValue.filter((mode) => mode !== selectedMode) + : [...currentValue, selectedMode]; + + onChange(selection); + }; + + return ( +
+
+ {deploymentModes.map((deploymentMode) => { + const { mode, title, description, Image } = deploymentMode; + const isSelected = value?.some((item) => item === mode); + + return ( + + ); + })} +
+
+ ); +}; diff --git a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionStep.component.tsx b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionStep.component.tsx index cd6768a829b9..11fe1a3f704b 100644 --- a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionStep.component.tsx +++ b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionStep.component.tsx @@ -1,27 +1,32 @@ -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import React, { useState } from 'react'; import { - Tabs, - TabsList, - TabsTrigger, Badge, - ScrollArea, - ScrollBar, - Popover, - PopoverTrigger, - PopoverContent, RadioGroup, RadioTile, RadioIndicator, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + ScrollArea, + Alert, + Checkbox, + Label, } from '@datatr-ux/uxlib'; -import { ExternalLink, HelpCircle } from 'lucide-react'; import { Region, RegionTypeEnum } from '@datatr-ux/ovhcloud-types/cloud/index'; -import useHorizontalScroll from '@/hooks/useHorizontalScroll.hook'; import Flag from '@/components/flag/Flag.component'; -import { cn } from '@/lib/utils'; -import A from '@/components/links/A.component'; import { useTranslatedMicroRegions } from '@/hooks/useTranslatedMicroRegions'; +import { + DeploymentModeSelection, + getDefaultDeploymentModes, +} from './RegionDeploymentSelection'; +import cloud from '@/types/Cloud'; +import OvhLink from '@/components/links/OvhLink.component'; +import usePciProject from '@/data/hooks/project/usePciProject.hook'; +import { RegionTypeBadgeWithPopover } from '@/components/region-type-badge/RegionTypeBadge.component'; interface RegionsSelectProps { regions: Region[]; @@ -31,14 +36,37 @@ interface RegionsSelectProps { export interface MappedRegions extends Region { label: string; continent: string; + type: cloud.RegionTypeEnum; } + +const QuotaOvhLink = ({ + projectId, + children, +}: { + projectId: string; + children?: React.ReactNode; +}) => ( + + {children} + +); + const RegionsStep = React.forwardRef( ({ regions, value, onChange }, ref) => { - const [selectedContinentIndex, setSelectedContinentIndex] = useState(0); + const [shouldFilterRegion, setShouldFilterRegion] = useState(false); + const [selectedContinent, setSelectedContinent] = useState(''); + const [selectedDeploymentModes, setSelectedDeploymentModes] = useState< + cloud.RegionTypeEnum[] + >(getDefaultDeploymentModes()); const { t } = useTranslation('regions'); const { t: tOrder } = useTranslation('pci-object-storage/order-funnel'); const RECOMENDED_REGION = 'EU-WEST-PAR'; - const scrollRef = useHorizontalScroll(); + const { data: project } = usePciProject(); const { translateContinentRegion, @@ -50,159 +78,173 @@ const RegionsStep = React.forwardRef( continent: translateContinentRegion(r.name), ...r, })); + + const regionsFilteredByDeployment = mappedRegions.filter((r) => { + return selectedDeploymentModes.length === 0 + ? true + : selectedDeploymentModes.includes(r.type); + }); + const continents = [ ...new Set([ t(`region_continent_all`), - ...mappedRegions.map((mr) => mr.continent).toSorted(), + ...regionsFilteredByDeployment.map((mr) => mr.continent).toSorted(), ]), ]; - const RegionTypeBadge = ({ region }: { region: MappedRegions }) => { - const helpLink = ( - - {t('help-link-more-info')} - - - ); - switch (region.type) { - case RegionTypeEnum.region: - return ( - - 1-AZ - - - - - - {t('region-description-1AZ')} - {helpLink} - - - - ); - case RegionTypeEnum['region-3-az']: - return ( - - 3-AZ - - - - - - {t('region-description-3AZ')} - {helpLink} - - - - ); - case RegionTypeEnum.localzone: - return ( - - LocalZone - - - - - - {t('region-description-localzone')} - {helpLink} - - - - ); - default: - return ?; + + const filteredRegions = mappedRegions.filter((r) => { + // Filter by continent + const matchesContinent = + !selectedContinent || selectedContinent === continents[0] + ? true + : r.continent === selectedContinent; + + // Filter by deployment modes - if no modes selected, show all regions + const matchesDeploymentMode = + selectedDeploymentModes.length === 0 + ? true + : selectedDeploymentModes.includes(r.type); + + return matchesContinent && matchesDeploymentMode; + }); + + const mappedRegionsFiltered = shouldFilterRegion + ? filteredRegions + : filteredRegions.slice(0, 12); + + const handleDeploymentModeChange = ( + newDeploymentModes: cloud.RegionTypeEnum[], + ) => { + setSelectedDeploymentModes(newDeploymentModes); + + const newRegionsFilteredByDeployment = mappedRegions.filter((r) => { + return newDeploymentModes.length === 0 + ? true + : newDeploymentModes.includes(r.type); + }); + + const newContinents = [ + ...new Set([ + t(`region_continent_all`), + ...newRegionsFilteredByDeployment + .map((mr) => mr.continent) + .toSorted(), + ]), + ]; + + if (selectedContinent && !newContinents.includes(selectedContinent)) { + setSelectedContinent(''); } }; + return (
- setSelectedContinentIndex(+v)} - > - - +
+
+
{tOrder('selectGeographicalZone')}
+ +
+
+ setShouldFilterRegion(!shouldFilterRegion)} + /> + +
+
+ + {mappedRegionsFiltered.length ? ( + +
+ + {mappedRegionsFiltered + .sort((a, b) => { + const priority: Record = { + [RegionTypeEnum['region-3-az']]: 0, + [RegionTypeEnum.region]: 1, + }; + + const aPriority = priority[a.type] ?? 2; + const bPriority = priority[b.type] ?? 2; + + if (aPriority !== bPriority) { + return aPriority - bPriority; + } + return a.name.localeCompare(b.name); + }) + .map((region) => ( + +
+
+ +
+ {region.countryCode && ( + + )} + {region.label} +
+
+
+
+ +
+ {region.name === RECOMENDED_REGION && ( + + {tOrder('regionRecommended')} + + )} +
+
+
+ ))} +
+
- - {mappedRegions - .filter((r) => - selectedContinentIndex === 0 - ? true - : r.continent === continents[selectedContinentIndex], - ) - .sort((a, b) => { - const priority: Record = { - [RegionTypeEnum['region-3-az']]: 0, - [RegionTypeEnum.region]: 1, - }; - - const aPriority = priority[a.type] ?? 2; - const bPriority = priority[b.type] ?? 2; - - if (aPriority !== bPriority) { - return aPriority - bPriority; - } - return a.name.localeCompare(b.name); - }) - .map((region) => ( - -
-
- -
- {region.countryCode && ( - - )} - {region.label} -
-
-
-
- -
- {region.name === RECOMENDED_REGION && ( - - {tOrder('regionRecommended')} - - )} -
-
-
- ))} -
-
+ ) : ( + + ]} + /> + + )} + {filteredRegions.length ? ( +
+ ]} + /> +
+ ) : null}
); }, diff --git a/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionTypeBadge.component.tsx b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionTypeBadge.component.tsx new file mode 100644 index 000000000000..9f44d5cc7273 --- /dev/null +++ b/packages/manager/apps/pci-object-storage/src/pages/object-storage/create/_components/steps/RegionTypeBadge.component.tsx @@ -0,0 +1,112 @@ +import { useTranslation } from 'react-i18next'; +import { + Badge, + Popover, + PopoverTrigger, + PopoverContent, +} from '@datatr-ux/uxlib'; +import { ExternalLink, HelpCircle } from 'lucide-react'; +import { RegionTypeEnum } from '@datatr-ux/ovhcloud-types/cloud/index'; + +import A from '@/components/links/A.component'; + +const getBadgeConfig = (type: RegionTypeEnum) => { + switch (type) { + case RegionTypeEnum.region: + return { + label: '1-AZ', + className: 'bg-primary-400 text-white', + }; + case RegionTypeEnum['region-3-az']: + return { + label: '3-AZ', + className: 'bg-primary-500 text-white', + }; + case RegionTypeEnum.localzone: + return { + label: 'LocalZone', + className: 'bg-primary-300 text-white', + }; + default: + return { + label: '?', + className: 'bg-neutral-100 text-text', + }; + } +}; + +interface RegionTypeBadgeProps { + type: RegionTypeEnum; + className?: string; +} + +export const RegionTypeBadge = ({ type, className }: RegionTypeBadgeProps) => { + const config = getBadgeConfig(type); + + return ( + + {config.label} + + ); +}; + +interface RegionTypeBadgeWithPopoverProps extends RegionTypeBadgeProps { + showPopover?: boolean; +} + +export const RegionTypeBadgeWithPopover = ({ + type, + className, + showPopover = true, +}: RegionTypeBadgeWithPopoverProps) => { + const { t } = useTranslation(['regions', 'pci-object-storage/order-funnel']); + + const helpLink = ( + + {t('help-link-more-info')} + + + ); + + const getDescription = (regionType: RegionTypeEnum) => { + switch (regionType) { + case RegionTypeEnum.region: + return t('region-description-1AZ'); + case RegionTypeEnum['region-3-az']: + return t('region-description-3AZ'); + case RegionTypeEnum.localzone: + return t('region-description-localzone'); + default: + return ''; + } + }; + + if (!showPopover) { + return ; + } + + const config = getBadgeConfig(type); + const description = getDescription(type); + + return ( + + {config.label} + {description && ( + + + + + + {description} + {helpLink} + + + )} + + ); +};