From c96459b88e134c29c3f1a64fdcdca3fce7b5b521 Mon Sep 17 00:00:00 2001 From: aderghamov Date: Thu, 30 Oct 2025 17:25:47 +0100 Subject: [PATCH] feat(ips): adding a modal to edit ip block information ref: #MANAGER-20142 Signed-off-by: aderghamov --- .../ip-block-information/Messages_fr_FR.json | 12 + .../translations/listing/Messages_fr_FR.json | 1 + .../apps/ips/src/data/api/get/index.ts | 1 + .../ips/src/data/api/get/ipRipeInformation.ts | 24 ++ .../api/postorput/changeIpOrganisation.ts | 21 ++ .../apps/ips/src/data/api/postorput/index.ts | 2 + .../api/postorput/upsertIpRipeInformation.ts | 20 ++ .../manager/apps/ips/src/data/hooks/index.ts | 1 + .../apps/ips/src/data/hooks/ip/index.ts | 2 + .../src/data/hooks/ip/useGetIpOrganisation.ts | 66 ++++++ .../data/hooks/ip/useGetIpRipeInformation.ts | 30 +++ .../upsertIpBlockInformation.page.tsx | 215 ++++++++++++++++++ .../IpActionsCell/IpActionsCell.tsx | 19 ++ .../OfferSelectionSection.component.tsx | 2 +- .../apps/ips/src/routes/routes.constant.ts | 2 + .../manager/apps/ips/src/routes/routes.tsx | 15 ++ .../ips/src/utils/translation.constants.ts | 1 + 17 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 packages/manager/apps/ips/public/translations/ip-block-information/Messages_fr_FR.json create mode 100644 packages/manager/apps/ips/src/data/api/get/ipRipeInformation.ts create mode 100644 packages/manager/apps/ips/src/data/api/postorput/changeIpOrganisation.ts create mode 100644 packages/manager/apps/ips/src/data/api/postorput/upsertIpRipeInformation.ts create mode 100644 packages/manager/apps/ips/src/data/hooks/ip/useGetIpOrganisation.ts create mode 100644 packages/manager/apps/ips/src/data/hooks/ip/useGetIpRipeInformation.ts create mode 100644 packages/manager/apps/ips/src/pages/actions/upsertIpBlockInformation/upsertIpBlockInformation.page.tsx diff --git a/packages/manager/apps/ips/public/translations/ip-block-information/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/ip-block-information/Messages_fr_FR.json new file mode 100644 index 000000000000..b0e49a0108ac --- /dev/null +++ b/packages/manager/apps/ips/public/translations/ip-block-information/Messages_fr_FR.json @@ -0,0 +1,12 @@ +{ + "ipBlockInformationTitle": "Informations du bloc d'IP", + "ipBlockInformationSubtitle": "Cette section vous permet de personnaliser les informations publiques sur votre bloc IP (informations présentes sur la base whois).", + "ipBlockInformationNetNameLabel": "Nom du réseau (Netname)", + "ipBlockInformationDescriptionLabel": "Description", + "ipBlockInformationOrganisationLabel": "Organisation", + "ipBlockInformationOrganisationOnGoingChange": "L'organisation du bloc d'IP est en cours de réaffectation. Veuillez patienter quelques minutes.", + "ipBlockInformationUpdateSuccessMessage": "Les informations du bloc d'IP {{ip}} ont été mises à jour.", + "ipBlockInformationUpdateErrorMessage": "Une erreur a eu lieu pendant la mise à jour des informations du bloc d'IP {{ip}}: {{error}}", + "ipBlockInformationOrgUpdateSuccessMessage": "Le bloc d'IP {{ip}} est en cours d'affectation à l'organisation {{organisation}}. Cette operation peut prendre quelques minutes", + "ipBlockInformationOrgUpdateErrorMessage": "Une erreur a eu lieu pendant l'affectation du bloc IP {{ip}} à l'organisation {{organisation}}: {{error}}" +} diff --git a/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json index 363ad2a0514f..3288a7c6c656 100644 --- a/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json +++ b/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json @@ -76,6 +76,7 @@ "listingActionEditDescription": "Editer la description", "listingActionDeleteVirtualMac": "Supprimer la MAC virtuelle", "listingActionByoipTerminate": "Résilier le service BYOIP", + "listingActionUpdateIpBlockInformation": "Voir / Modifier les informations sur le bloc IP", "listingActionSlice": "Segmenter", "listingActionAggregate": "Agréger", "listingActionUnblockHackedIP": "Déblocage anti-hack", diff --git a/packages/manager/apps/ips/src/data/api/get/index.ts b/packages/manager/apps/ips/src/data/api/get/index.ts index a79131a7c67a..fd4b1612b08c 100644 --- a/packages/manager/apps/ips/src/data/api/get/index.ts +++ b/packages/manager/apps/ips/src/data/api/get/index.ts @@ -14,5 +14,6 @@ export * from './organisationsList'; export * from './organisationsDetails'; export * from './byoip'; export * from './productServices'; +export * from './ipRipeInformation'; export * from './ipTask'; export * from './moveIp'; diff --git a/packages/manager/apps/ips/src/data/api/get/ipRipeInformation.ts b/packages/manager/apps/ips/src/data/api/get/ipRipeInformation.ts new file mode 100644 index 000000000000..2e8df2fe9bb6 --- /dev/null +++ b/packages/manager/apps/ips/src/data/api/get/ipRipeInformation.ts @@ -0,0 +1,24 @@ +import { ApiResponse, apiClient } from '@ovh-ux/manager-core-api'; + +export type IpRipeInformation = { + description: string; + netname: string; +}; + +export type GetIpRipeInformationParams = { + ip: string; +}; + +export const getIpRipeInformationQueryKey = ( + params: GetIpRipeInformationParams, +) => [`get/ip/${encodeURIComponent(params.ip)}/ripe`]; + +/** + * Get IP Ripe Information + */ +export const getIpRipeInformation = async ( + params: GetIpRipeInformationParams, +): Promise> => + apiClient.v6.get( + `/ip/${encodeURIComponent(params.ip)}/ripe`, + ); diff --git a/packages/manager/apps/ips/src/data/api/postorput/changeIpOrganisation.ts b/packages/manager/apps/ips/src/data/api/postorput/changeIpOrganisation.ts new file mode 100644 index 000000000000..b51dc97daa60 --- /dev/null +++ b/packages/manager/apps/ips/src/data/api/postorput/changeIpOrganisation.ts @@ -0,0 +1,21 @@ +import { ApiResponse, apiClient } from '@ovh-ux/manager-core-api'; + +export type ChangeIpOrganisationParams = { + ip: string; + organisation: string; +}; + +export const changeIpOrganisationQueryKey = ( + params: ChangeIpOrganisationParams, +) => [`post/ip/${encodeURIComponent(params.ip)}/changeOrg`]; + +export const changeIpOrganisation = async ( + params: ChangeIpOrganisationParams, +): Promise> => { + return apiClient.v6.post( + `/ip/${encodeURIComponent(params.ip)}/changeOrg`, + { + organisation: params.organisation, + }, + ); +}; diff --git a/packages/manager/apps/ips/src/data/api/postorput/index.ts b/packages/manager/apps/ips/src/data/api/postorput/index.ts index 78cdea141ea2..f5c60f49155f 100644 --- a/packages/manager/apps/ips/src/data/api/postorput/index.ts +++ b/packages/manager/apps/ips/src/data/api/postorput/index.ts @@ -7,3 +7,5 @@ export * from './addIpToVirtualMac'; export * from './postMoveIp'; export * from './unblockAntiHackIp'; export * from './unblockAntiSpamIp'; +export * from './upsertIpRipeInformation'; +export * from './changeIpOrganisation'; diff --git a/packages/manager/apps/ips/src/data/api/postorput/upsertIpRipeInformation.ts b/packages/manager/apps/ips/src/data/api/postorput/upsertIpRipeInformation.ts new file mode 100644 index 000000000000..fcd26d812c8c --- /dev/null +++ b/packages/manager/apps/ips/src/data/api/postorput/upsertIpRipeInformation.ts @@ -0,0 +1,20 @@ +import { ApiResponse, apiClient } from '@ovh-ux/manager-core-api'; + +export type UpsertIpRipeInformationParams = { + ip: string; + description: string; + netname: string; +}; + +export const upsertIpRipeInformationQueryKey = ( + params: UpsertIpRipeInformationParams, +) => [`put/ip/${encodeURIComponent(params.ip)}/ripe`]; + +export const upsertIpRipeInformation = async ( + params: UpsertIpRipeInformationParams, +): Promise> => { + return apiClient.v6.put(`/ip/${encodeURIComponent(params.ip)}/ripe`, { + description: params.description, + netname: params.netname, + }); +}; diff --git a/packages/manager/apps/ips/src/data/hooks/index.ts b/packages/manager/apps/ips/src/data/hooks/index.ts index 3b3af7031316..e3e874a21b53 100644 --- a/packages/manager/apps/ips/src/data/hooks/index.ts +++ b/packages/manager/apps/ips/src/data/hooks/index.ts @@ -1,4 +1,5 @@ export * from './ip'; +export * from './organisation'; export * from './useGetTokens'; export * from './useIpv6Availability'; export * from './useGetProductService'; diff --git a/packages/manager/apps/ips/src/data/hooks/ip/index.ts b/packages/manager/apps/ips/src/data/hooks/ip/index.ts index ab1ee98b4431..fb1da5fbfeee 100644 --- a/packages/manager/apps/ips/src/data/hooks/ip/index.ts +++ b/packages/manager/apps/ips/src/data/hooks/ip/index.ts @@ -6,6 +6,7 @@ export * from './useGetIpVmacDetails'; export * from './useGetIpVmacWithIp'; export * from './useGetIpMitigation'; export * from './useGetIpGameFirewall'; +export * from './useGetIpOrganisation'; export * from './useIpHasAlerts'; export * from './useGetIpAntihack'; export * from './useGetIpSpam'; @@ -24,3 +25,4 @@ export * from './useMoveIpService'; export * from './useGetDedicatedServerTasks'; export * from './useByoipActions'; export * from './edge-firewall'; +export * from './useGetIpRipeInformation'; diff --git a/packages/manager/apps/ips/src/data/hooks/ip/useGetIpOrganisation.ts b/packages/manager/apps/ips/src/data/hooks/ip/useGetIpOrganisation.ts new file mode 100644 index 000000000000..fb4fcda01aa2 --- /dev/null +++ b/packages/manager/apps/ips/src/data/hooks/ip/useGetIpOrganisation.ts @@ -0,0 +1,66 @@ +import { useQuery, useQueries } from '@tanstack/react-query'; +import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api'; +import { + IpDetails, + getIpDetailsQueryKey, + getIpDetails, + getIpTaskQueryKey, + getIpTaskList, +} from '@/data/api'; +import { IpTaskFunction, IpTaskStatus } from '@/types'; + +export type UseGetIpOrganisationParams = { + ip: string; + enabled?: boolean; +}; + +export const useGetIpOrganisation = ({ + ip, + enabled = true, +}: UseGetIpOrganisationParams) => { + const taskQueries = useQueries({ + queries: [IpTaskStatus.init, IpTaskStatus.todo, IpTaskStatus.doing].map( + (status) => ({ + queryKey: getIpTaskQueryKey({ + ip, + status, + fn: IpTaskFunction.changeRipeOrg, + }), + queryFn: () => + getIpTaskList({ + ip, + status, + fn: IpTaskFunction.changeRipeOrg, + }), + staleTime: 0, + }), + ), + }); + + const isTasksLoading = taskQueries.some((query) => query.isLoading); + const taskError = taskQueries.find((query) => query.isError)?.error; + const hasOnGoingChangeRipeOrgTask = taskQueries.some( + (query) => query.data?.data && query.data.data.length > 0, + ); + + const { data: ipDetailsResponse, isLoading, isError, error } = useQuery< + ApiResponse, + ApiError + >({ + queryKey: getIpDetailsQueryKey({ ip }), + queryFn: () => getIpDetails({ ip }), + enabled: + enabled && !isTasksLoading && !taskError && !hasOnGoingChangeRipeOrgTask, + staleTime: Number.POSITIVE_INFINITY, + retry: false, + }); + + return { + organisationId: ipDetailsResponse?.data.organisationId, + rirForOrganisation: ipDetailsResponse?.data.rir, + hasOnGoingChangeRipeOrgTask, + isLoading: isLoading || isTasksLoading, + isError: isError || !!taskError, + error: error ?? taskError, + }; +}; diff --git a/packages/manager/apps/ips/src/data/hooks/ip/useGetIpRipeInformation.ts b/packages/manager/apps/ips/src/data/hooks/ip/useGetIpRipeInformation.ts new file mode 100644 index 000000000000..bba5cce1ad33 --- /dev/null +++ b/packages/manager/apps/ips/src/data/hooks/ip/useGetIpRipeInformation.ts @@ -0,0 +1,30 @@ +import { useQuery } from '@tanstack/react-query'; +import { + getIpRipeInformation, + getIpRipeInformationQueryKey, +} from '@/data/api/get/ipRipeInformation'; + +export type UseGetIpRipeInformationParams = { + ip: string; +}; + +export type IpRipeInformation = { + description: string; + netname: string; +}; + +export const useGetIpRipeInformation = ({ + ip, +}: UseGetIpRipeInformationParams) => { + const { data: ipRipeInfo, isLoading, isError, error } = useQuery< + IpRipeInformation + >({ + queryKey: getIpRipeInformationQueryKey({ ip }), + queryFn: () => + getIpRipeInformation({ ip }).then((response) => response.data), + retry: false, + staleTime: Number.POSITIVE_INFINITY, + }); + + return { ipRipeInfo, isLoading, isError, error }; +}; diff --git a/packages/manager/apps/ips/src/pages/actions/upsertIpBlockInformation/upsertIpBlockInformation.page.tsx b/packages/manager/apps/ips/src/pages/actions/upsertIpBlockInformation/upsertIpBlockInformation.page.tsx new file mode 100644 index 000000000000..7608431ec139 --- /dev/null +++ b/packages/manager/apps/ips/src/pages/actions/upsertIpBlockInformation/upsertIpBlockInformation.page.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { Modal, useNotifications } from '@ovh-ux/manager-react-components'; +import { + OdsFormField, + OdsText, + OdsMessage, + OdsInput, + OdsTextarea, + OdsSelect, +} from '@ovhcloud/ods-components/react'; +import { ApiError } from '@ovh-ux/manager-core-api'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { ODS_MESSAGE_COLOR } from '@ovhcloud/ods-components'; +import { + useGetIpRipeInformation, + useGetOrganisationsList, + useGetIpOrganisation, +} from '@/data/hooks'; +import { fromIdToIp, ipFormatter, TRANSLATION_NAMESPACES } from '@/utils'; +import { + changeIpOrganisation, + getIpDetailsQueryKey, + getIpRipeInformationQueryKey, + upsertIpRipeInformation, +} from '@/data/api'; + +export default function UpsertIpBlockInformation() { + const navigate = useNavigate(); + const [search] = useSearchParams(); + const { id } = useParams(); + const { ipGroup: ip } = id + ? ipFormatter(fromIdToIp(id)) + : { ipGroup: undefined }; + const { ipRipeInfo, isLoading: isRipeLoading } = useGetIpRipeInformation({ + ip, + }); + const { organisations, isLoading: isOrgLoading } = useGetOrganisationsList(); + const { + organisationId, + rirForOrganisation, + hasOnGoingChangeRipeOrgTask, + isLoading: isOrganisationIdLoading, + } = useGetIpOrganisation({ ip }); + const { addSuccess, addError, clearNotifications } = useNotifications(); + const { t } = useTranslation([TRANSLATION_NAMESPACES.ipBlockInformation]); + const [netname, setNetname] = React.useState(ipRipeInfo?.netname || ''); + const [description, setDescription] = React.useState( + ipRipeInfo?.description || '', + ); + const [organisation, setOrganisation] = React.useState(organisationId || ''); + const queryClient = useQueryClient(); + + const closeModal = () => { + navigate(`..?${search.toString()}`); + }; + + const { mutate: upsertRipe, isPending: isRipePending } = useMutation({ + mutationFn: () => upsertIpRipeInformation({ ip, description, netname }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getIpRipeInformationQueryKey({ ip }), + }); + addSuccess(t('ipBlockInformationUpdateSuccessMessage', { ip })); + }, + onError: (error: ApiError) => { + addError(t('ipBlockInformationUpdateErrorMessage', { ip, error })); + }, + }); + + const { mutate: changeOrganisation, isPending: isOrgPending } = useMutation({ + mutationFn: () => changeIpOrganisation({ ip, organisation }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getIpDetailsQueryKey({ ip }), + }); + addSuccess( + t('ipBlockInformationOrgUpdateSuccessMessage', { ip, organisation }), + ); + }, + onError: (error: ApiError) => { + addError( + t('ipBlockInformationOrgUpdateErrorMessage', { + ip, + organisation, + error, + }), + ); + }, + }); + + const handleSubmit = () => { + const hasRipeChanges = + netname !== ipRipeInfo?.netname || + description !== ipRipeInfo?.description; + const hasOrgChanges = organisation !== organisationId; + + if (hasRipeChanges || hasOrgChanges) { + clearNotifications(); + } + + if (hasRipeChanges) upsertRipe(); + if (hasOrgChanges && !hasOnGoingChangeRipeOrgTask) changeOrganisation(); + + closeModal(); + }; + + useEffect(() => { + setNetname(ipRipeInfo?.netname || ''); + setDescription(ipRipeInfo?.description || ''); + setOrganisation(organisationId || ''); + }, [ipRipeInfo, organisationId]); + + const availableOrganisations = useMemo(() => { + if (rirForOrganisation === 'ARIN') { + return ( + organisations?.filter( + (orgId: string): boolean => + orgId?.toLowerCase().startsWith('arin_') ?? false, + ) ?? [] + ); + } + if (rirForOrganisation === 'RIPE') { + return ( + organisations?.filter( + (orgId: string): boolean => + orgId?.toLowerCase().startsWith('ripe_') ?? false, + ) ?? [] + ); + } + return organisations; + }, [organisations, rirForOrganisation]); + + return ( + +
+
+ + {t('ipBlockInformationSubtitle')} + +
+ +
+ + + setNetname(e.detail.value as string)} + isDisabled={isRipePending} + /> + +
+ +
+ + + setDescription(e.detail.value as string)} + isDisabled={isRipePending} + /> + +
+ +
+ + + {hasOnGoingChangeRipeOrgTask ? ( + + {t('ipBlockInformationOrganisationOnGoingChange')} + + ) : ( + setOrganisation(e.detail.value)} + isDisabled={isOrgPending} + > + {availableOrganisations?.map((org) => ( + + ))} + + )} + +
+
+
+ ); +} diff --git a/packages/manager/apps/ips/src/pages/listing/ipListing/components/DatagridCells/IpActionsCell/IpActionsCell.tsx b/packages/manager/apps/ips/src/pages/listing/ipListing/components/DatagridCells/IpActionsCell/IpActionsCell.tsx index ed89a60adef7..e0a18a82e0b8 100644 --- a/packages/manager/apps/ips/src/pages/listing/ipListing/components/DatagridCells/IpActionsCell/IpActionsCell.tsx +++ b/packages/manager/apps/ips/src/pages/listing/ipListing/components/DatagridCells/IpActionsCell/IpActionsCell.tsx @@ -389,6 +389,25 @@ export const IpActionsCell = ({ )}?${search.toString()}`, ), }, + isGroup && + ipaddr.IPv4.isIPv4(ipAddress) && + [ + IpTypeEnum.ADDITIONAL, + IpTypeEnum.PCC, + IpTypeEnum.VRACK, + IpTypeEnum.CLOUD, + IpTypeEnum.DEDICATED, + ].includes(ipDetails?.type) && { + id: 15, + label: t('listingActionUpdateIpBlockInformation'), + onClick: () => + navigate( + `${urls.ipBlockInformation.replace( + urlDynamicParts.id, + parentId, + )}?${search.toString()}`, + ), + }, ].filter(Boolean); return ( diff --git a/packages/manager/apps/ips/src/pages/order/sections/OfferSelectionSection.component.tsx b/packages/manager/apps/ips/src/pages/order/sections/OfferSelectionSection.component.tsx index a4459aff055b..a50c2f2e2a57 100644 --- a/packages/manager/apps/ips/src/pages/order/sections/OfferSelectionSection.component.tsx +++ b/packages/manager/apps/ips/src/pages/order/sections/OfferSelectionSection.component.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { OvhSubsidiary } from '@ovh-ux/manager-react-components'; -import { getPrice, getPriceTextFormatted } from '@/components/price'; import { OdsSelect, OdsText, @@ -9,6 +8,7 @@ import { } from '@ovhcloud/ods-components/react'; import { ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { getPrice, getPriceTextFormatted } from '@/components/price'; import { DEFAULT_PRICING_MODE, IpOffer, diff --git a/packages/manager/apps/ips/src/routes/routes.constant.ts b/packages/manager/apps/ips/src/routes/routes.constant.ts index c3489a976fc5..39ae8b9dd539 100644 --- a/packages/manager/apps/ips/src/routes/routes.constant.ts +++ b/packages/manager/apps/ips/src/routes/routes.constant.ts @@ -24,6 +24,7 @@ export const subRoutes = { exportIpToCsv: 'export-ip-to-csv', slice: 'slice', aggregate: 'aggregate', + ipBlockInformation: 'update-ip-information', }; export const urlDynamicParts = { @@ -59,5 +60,6 @@ export const urls = { listingConfigureReverseDns: `${subRoutes.root}/${subRoutes.configureReverseDns}/${urlDynamicParts.parentId}/${urlDynamicParts.optionalId}`, byoipOrderModal: `${subRoutes.root}/${subRoutes.byoip}/${subRoutes.byoipOrder}`, slice: `${subRoutes.root}/${subRoutes.slice}/${urlDynamicParts.parentId}`, + ipBlockInformation: `${subRoutes.root}/${subRoutes.ipBlockInformation}/${urlDynamicParts.id}`, aggregate: `${subRoutes.root}/${subRoutes.aggregate}/${urlDynamicParts.parentId}`, }; diff --git a/packages/manager/apps/ips/src/routes/routes.tsx b/packages/manager/apps/ips/src/routes/routes.tsx index b5db2dfc9573..617c04510c54 100644 --- a/packages/manager/apps/ips/src/routes/routes.tsx +++ b/packages/manager/apps/ips/src/routes/routes.tsx @@ -134,6 +134,21 @@ export const Routes: RouteObject[] = [ }, }, }, + { + id: subRoutes.ipBlockInformation, + path: urls.ipBlockInformation, + ...lazyRouteConfig(() => + import( + '@/pages/actions/upsertIpBlockInformation/upsertIpBlockInformation.page' + ), + ), + handle: { + tracking: { + pageName: 'listing-view-ip-block-information', + pageType: PageType.popup, + }, + }, + }, { id: subRoutes.deleteVirtualMac, path: urls.deleteVirtualMac, diff --git a/packages/manager/apps/ips/src/utils/translation.constants.ts b/packages/manager/apps/ips/src/utils/translation.constants.ts index 12a8a4f6c445..4ae2af434a3a 100644 --- a/packages/manager/apps/ips/src/utils/translation.constants.ts +++ b/packages/manager/apps/ips/src/utils/translation.constants.ts @@ -7,6 +7,7 @@ export const TRANSLATION_NAMESPACES = { edgeNetworkFirewall: 'edge-network-firewall', ips: 'ips', listing: 'listing', + ipBlockInformation: 'ip-block-information', managerOrganisations: 'manage-organisations', onboarding: 'onboarding', order: 'order',