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 prix0>.",
- "orderButton": "Commander",
+ "pricingDisclaimer": " Pour plus d'informations sur les tarifs, <0>voir la page des prix0>.",
+ "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égion0>.",
+ "regionsNoMatch": "Aucune région ne correspond à vos filtres. Ajustez vos critères ou <0>activez une région0>.",
+ "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}
+
+
+ )}
+
+ );
+};