diff --git a/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.spec.ts b/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.spec.ts new file mode 100644 index 000000000000..ab37790b9b90 --- /dev/null +++ b/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.spec.ts @@ -0,0 +1,15 @@ +import { getRirFromPlanCode } from './useGetCatalog'; + +describe('getRirFromPlanCode', () => { + it.each([ + { planCode: 'byoip-failover-v4-arin', expected: 'ARIN' }, + { planCode: 'byoip-failover-v4-ripe', expected: 'RIPE' }, + { planCode: 'byoip-failover-v4-apnic', expected: 'APNIC' }, + { planCode: 'byoip-failover-v4', expected: '' }, + { planCode: 'unrelated-plan-code', expected: '' }, + { planCode: '', expected: '' }, + { planCode: undefined as unknown as string, expected: '' }, + ])('$planCode => $expected', ({ planCode, expected }) => { + expect(getRirFromPlanCode(planCode)).toBe(expected); + }); +}); diff --git a/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.ts b/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.ts index fba018032b9c..2e8cc1201523 100644 --- a/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.ts +++ b/packages/manager/apps/ips/src/data/hooks/catalog/useGetCatalog.ts @@ -17,6 +17,13 @@ export type Campus = { planCode: string; }; +// Extract the RIR from a byoip plan code, e.g. "byoip-failover-v4-arin" -> "ARIN". +// Returns an empty string when the plan code cannot be decoded. +export const getRirFromPlanCode = (planCode: string): string => { + if (!planCode?.startsWith(`${BYOIP_FAILOVER_V4}-`)) return ''; + return planCode.slice(BYOIP_FAILOVER_V4.length + 1).toUpperCase(); +}; + export type ProductConfiguration = { name: string; values: string[] | Campus[]; @@ -131,6 +138,37 @@ export const useGetCatalog = () => { if (index !== -1 && plan?.details?.product?.configurations?.[index]) { plan.details.product.configurations[index].values = campusList; } + + // On some subsidiaries (e.g. US) the catalog does not expose an ipRir + // configuration. In that case we derive the available RIRs from the + // campus plan codes so the RIR selection step is still usable. + const ipRirIndex = plan.details.product.configurations.findIndex( + ({ name }) => name === CONFIG_NAME.IPRIR, + ); + const derivedIpRirValues = Array.from( + new Set( + campusList + .map(({ planCode }) => getRirFromPlanCode(planCode)) + .filter(Boolean), + ), + ); + + if (ipRirIndex === -1) { + if (derivedIpRirValues.length > 0) { + plan.details.product.configurations.push({ + name: CONFIG_NAME.IPRIR, + values: derivedIpRirValues, + }); + } + } else { + const existingValues = + (plan.details.product.configurations[ipRirIndex] + .values as string[]) || []; + if (existingValues.length === 0) { + plan.details.product.configurations[ipRirIndex].values = + derivedIpRirValues; + } + } } return plan as unknown as Plan; diff --git a/packages/manager/apps/ips/src/pages/byoip/Byoip.utils.ts b/packages/manager/apps/ips/src/pages/byoip/Byoip.utils.ts index 279e0b7dd50e..bc8239d77c9e 100644 --- a/packages/manager/apps/ips/src/pages/byoip/Byoip.utils.ts +++ b/packages/manager/apps/ips/src/pages/byoip/Byoip.utils.ts @@ -47,9 +47,12 @@ export type ConfigItem = { /** * Returns the express order settings */ -export const getByoipProductSettings = (config: ConfigItem[]) => +export const getByoipProductSettings = ( + config: ConfigItem[], + planCode: string = BYOIP_FAILOVER_V4, +) => JSURL.stringify({ - planCode: BYOIP_FAILOVER_V4, + planCode, configuration: [...config].filter(Boolean), option: [], quantity: 1, diff --git a/packages/manager/apps/ips/src/pages/byoip/ByoipOrderModal/ByoipOrderModal.page.tsx b/packages/manager/apps/ips/src/pages/byoip/ByoipOrderModal/ByoipOrderModal.page.tsx index 710531688f78..c647968a3fc2 100644 --- a/packages/manager/apps/ips/src/pages/byoip/ByoipOrderModal/ByoipOrderModal.page.tsx +++ b/packages/manager/apps/ips/src/pages/byoip/ByoipOrderModal/ByoipOrderModal.page.tsx @@ -26,7 +26,12 @@ import { useOvhTracking, } from '@ovh-ux/manager-react-shell-client'; -import { BYOIP_FAILOVER_V4, CONFIG_NAME } from '@/data/hooks/catalog'; +import { + BYOIP_FAILOVER_V4, + Campus, + CONFIG_NAME, + useGetCatalog, +} from '@/data/hooks/catalog'; import { urls } from '@/routes/routes.constant'; import { ByoipContext } from '../Byoip.context'; @@ -34,6 +39,7 @@ import { ByoipPayloadParams, ConfigItem, getByoipProductSettings, + getConfigValues, } from '../Byoip.utils'; interface DeclarationItem { @@ -47,6 +53,17 @@ export const ByoipOrderModal: React.FC = () => { const { trackClick } = useOvhTracking(); const { ipRir, selectedRegion, ipRange, asOwnRirType, asOwnNumberType } = React.useContext(ByoipContext); + const { data: catalog } = useGetCatalog(); + + const campusValues = getConfigValues( + catalog?.details?.product.configurations, + CONFIG_NAME.CAMPUS, + ) as Campus[]; + + const selectedCampus = campusValues.find( + (campus) => campus.name === selectedRegion, + ); + const selectedPlanCode = selectedCampus?.planCode ?? BYOIP_FAILOVER_V4; const orderBaseUrl = useOrderURL('express_review_base'); @@ -165,7 +182,7 @@ export const ByoipOrderModal: React.FC = () => { ipRir, campus: { name: selectedRegion, - planCode: BYOIP_FAILOVER_V4, + planCode: selectedPlanCode, }, ip: ipRange, ...(asOwnRirType && { asRir: asOwnRirType }), @@ -195,7 +212,10 @@ export const ByoipOrderModal: React.FC = () => { if (campus && updateConfig[campusId]) { updateConfig[campusId].values = [campus.name]; } - const settings = getByoipProductSettings(updateConfig); + const settings = getByoipProductSettings( + updateConfig, + selectedPlanCode, + ); window.open( `${orderBaseUrl}?products=~(${settings})`, '_blank', diff --git a/packages/manager/apps/ips/src/pages/byoip/sections/RegionSelectionSection.component.tsx b/packages/manager/apps/ips/src/pages/byoip/sections/RegionSelectionSection.component.tsx index 5f461079c4ef..fc8d24a6ea93 100644 --- a/packages/manager/apps/ips/src/pages/byoip/sections/RegionSelectionSection.component.tsx +++ b/packages/manager/apps/ips/src/pages/byoip/sections/RegionSelectionSection.component.tsx @@ -13,7 +13,11 @@ import { import { OrderSection } from '@/components/OrderSection/OrderSection.component'; import { RegionCard } from '@/components/RegionCard/RegionCard.component'; import { DATACENTER_TO_REGION } from '@/data/hooks/catalog'; -import { CONFIG_NAME, useGetCatalog } from '@/data/hooks/catalog/useGetCatalog'; +import { + CONFIG_NAME, + getRirFromPlanCode, + useGetCatalog, +} from '@/data/hooks/catalog/useGetCatalog'; import { TRANSLATION_NAMESPACES } from '@/utils'; import { ByoipContext } from '../Byoip.context'; @@ -27,14 +31,25 @@ type CampusType = { export const RegionSelectionSection: React.FC = () => { const { t } = useTranslation([TRANSLATION_NAMESPACES.byoip]); const { data: catalog, isLoading } = useGetCatalog(); - const { selectedRegion, setSelectedRegion } = React.useContext(ByoipContext); + const { ipRir, selectedRegion, setSelectedRegion } = + React.useContext(ByoipContext); const { trackClick } = useOvhTracking(); - const campusValues = getConfigValues( + const allCampusValues = getConfigValues( catalog?.details?.product.configurations, CONFIG_NAME.CAMPUS, ) as CampusType[]; + // When the catalog exposes multiple RIR-specific plan codes, keep only the + // campuses attached to the selected RIR. Otherwise fall back to the full list. + const campusValues = ipRir + ? allCampusValues.filter( + (campus) => getRirFromPlanCode(campus.planCode) === ipRir.toUpperCase(), + ) + : allCampusValues; + const campusValuesToDisplay = + campusValues.length > 0 ? campusValues : allCampusValues; + return ( { > }>
- {campusValues.map((value) => { + {campusValuesToDisplay.map((value) => { const region = DATACENTER_TO_REGION[value.name]; return ( result.data?.regions.includes(region)) + .filter((result) => result.data?.regions?.includes(region) ?? false) .map(({ data }) => data ?? defaultIpDetailValue), ipv6List: ipv6Results - .filter((result) => result.data?.regions.includes(region)) + .filter((result) => result.data?.regions?.includes(region) ?? false) .map(({ data }) => data ?? defaultIpDetailValue), isComplete: data?.status !== 'pending', isLoading: