diff --git a/packages/manager/apps/web-domains/public/translations/domain/Messages_fr_FR.json b/packages/manager/apps/web-domains/public/translations/domain/Messages_fr_FR.json index e947093173fa..2c36d262da1d 100644 --- a/packages/manager/apps/web-domains/public/translations/domain/Messages_fr_FR.json +++ b/packages/manager/apps/web-domains/public/translations/domain/Messages_fr_FR.json @@ -247,6 +247,13 @@ "domain_tab_general_information_data_drawer_contact_field_phone": "Téléphone", "domain_tab_general_information_data_drawer_contact_field_province": "Province", "domain_tab_general_information_data_drawer_contact_field_zip": "Code postal", + "domain_tab_general_information_dns_title": "Serveur DNS", + "domain_tab_general_information_dns_standard": "Standard", + "domain_tab_general_information_note_anycast": "Mettez à niveau vers une offre Anycast pour de meilleures performances.", + "domain_tab_general_information_dns_anycast": "Anycast", + "domain_tab_general_information_dns_personnalised": "Personnalisé", + "domain_tab_general_information_dns_dedicated": "Dédié", + "domain_tab_general_information_dns_empty": "Aucun", "domain_tab_DNS_modification_form_dns_number": "Veuillez renseigner entre {{min}} et {{max}} DNS.", "domain_tab_DNS_modification_form_server_field": "Serveur DNS", "domain_tab_DNS_modification_form_ip_field": "IP associée", diff --git a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.spec.tsx b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.spec.tsx index 9946c96230de..b6f2327dbb46 100644 --- a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.spec.tsx +++ b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.spec.tsx @@ -7,8 +7,10 @@ import { wrapper } from '@/common/utils/test.provider'; import ConfigurationCards from './ConfigurationCards'; import { useGetDnssecStatus, + useGetDomainAnycastOption, useGetDomainAuthInfo, useGetDomainResource, + useTerminateAnycastMutation, useTransferTag, useUpdateDnssecService, useUpdateDomainResource, @@ -22,7 +24,6 @@ import { ResourceStatusEnum } from '@/domain/enum/resourceStatus.enum'; import { TDomainResource } from '@/domain/types/domainResource'; import { StatusEnum } from '@/domain/enum/Status.enum'; -vi.mock('@/domain/hooks/data/query'); vi.mock('@ovh-ux/manager-react-components', async () => { const actual = await vi.importActual('@ovh-ux/manager-react-components'); return { @@ -31,6 +32,17 @@ vi.mock('@ovh-ux/manager-react-components', async () => { }; }); +vi.mock('@/domain/hooks/data/query', () => ({ + useGetDomainAnycastOption: vi.fn(), + useGetDomainResource: vi.fn(), + useGetDomainAuthInfo: vi.fn(), + useGetDnssecStatus: vi.fn(), + useUpdateDnssecService: vi.fn(), + useUpdateDomainResource: vi.fn(), + useTransferTag: vi.fn(), + useTerminateAnycastMutation: vi.fn(), +})); + describe('ConfigurationCards component', () => { const mockUpdateServiceDnssec = vi.fn(); const mockUpdateDomain = vi.fn(); @@ -115,6 +127,14 @@ describe('ConfigurationCards component', () => { dnssecStatus: mockDnssecStatus, isDnssecStatusLoading: false, }); + (useGetDomainAnycastOption as jest.Mock).mockReturnValue({ + anycastOption: null, + isFetchingAnycastOption: false, + }); + (useTerminateAnycastMutation as jest.Mock).mockReturnValue({ + terminateAnycast: vi.fn(), + isTerminateAnycastPending: false, + }); (useUpdateDnssecService as jest.Mock).mockReturnValue({ updateServiceDnssec: mockUpdateServiceDnssec, isUpdateIsPending: false, @@ -145,8 +165,12 @@ describe('ConfigurationCards component', () => { it('renders DNS server badge', () => { render(, { wrapper }); - expect(screen.getByText('Serveur DNS')).toBeInTheDocument(); - expect(screen.getByText('Enregistré')).toBeInTheDocument(); + expect( + screen.getByText('domain_tab_general_information_dns_title'), + ).toBeInTheDocument(); + expect( + screen.getByText('domain_tab_general_information_dns_standard'), + ).toBeInTheDocument(); }); it('renders DnssecToggleStatus component', () => { diff --git a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.tsx b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.tsx index 214fc05f480f..2321c18ccfd6 100644 --- a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.tsx +++ b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/ConfigurationCards.tsx @@ -4,6 +4,7 @@ import { ManagerTile } from '@ovh-ux/manager-react-components'; import { Badge, BADGE_COLOR } from '@ovhcloud/ods-react'; import { useGetDnssecStatus, + useGetDomainAnycastOption, useGetDomainAuthInfo, useGetDomainResource, useTransferTag, @@ -25,6 +26,8 @@ import { DisclosureConfigurationEnum, TContactsConfigurationAPI, } from '@/domain/types/domainResource'; +import DnsState from './DnsState'; +import AnycastTerminateModal from '../AnycastOrder/AnycastTerminateModal'; interface ConfigurationCardsProps { readonly serviceName: string; @@ -36,6 +39,9 @@ export default function ConfigurationCards({ const { t } = useTranslation(['domain']); const { domainResource } = useGetDomainResource(serviceName); const { authInfo, isAuthInfoLoading } = useGetDomainAuthInfo(serviceName); + const { anycastOption, isFetchingAnycastOption } = useGetDomainAnycastOption( + serviceName, + ); const [dnssecModalOpened, setDnssecModalOpened] = React.useState( false, ); @@ -48,6 +54,11 @@ export default function ConfigurationCards({ setDataProtectionDrawerOpened, ] = React.useState(false); const [tag, setTag] = React.useState(''); + const [ + anycastTerminateModalOpen, + setAnycastTerminateModalOpen, + ] = React.useState(false); + const [restoreAnycast, setRestoreAnycast] = React.useState(false); const [ transferAuthInfoModalOpened, setTransferAuthInfoModalOpened, @@ -213,93 +224,96 @@ export default function ConfigurationCards({ } return ( - <> - {dataProtectionDrawerOpened && ( -
setDataProtectionDrawerOpened(false)} - /> - )} - - - {t('domain_tab_general_information_configuration')} - - - - Serveur DNS - - - Enregistré - - - - - - - - - - setDnssecModalOpened(!dnssecModalOpened)} - /> - handleUpdateProtectionState()} - onClose={() => setTransferModalOpened(!transferModalOpened)} - /> - - setTransferAuthInfoModalOpened(!transferAuthInfoModalOpened) - } - open={transferAuthInfoModalOpened} - /> - setTagModalOpened(!tagModalOpened)} - /> - setDataProtectionDrawerOpened(false)} - domainResource={domainResource} - visibleContacts={visibleContacts} - selectedContacts={selectedContacts} - onCheckboxChange={handleCheckboxChange} - onClick={() => { - handleUpdateDataProtection(); - }} - /> - - + + + {t('domain_tab_general_information_configuration')} + + + + + + + + setDnssecModalOpened(!dnssecModalOpened)} + /> + + + handleUpdateProtectionState()} + onClose={() => setTransferModalOpened(!transferModalOpened)} + /> + + setTransferAuthInfoModalOpened(!transferAuthInfoModalOpened) + } + open={transferAuthInfoModalOpened} + /> + setTagModalOpened(!tagModalOpened)} + /> + setDataProtectionDrawerOpened(false)} + domainResource={domainResource} + visibleContacts={visibleContacts} + selectedContacts={selectedContacts} + onCheckboxChange={handleCheckboxChange} + onClick={() => { + handleUpdateDataProtection(); + }} + /> + + setAnycastTerminateModalOpen(!anycastTerminateModalOpen) + } + /> + ); } diff --git a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DataProtection.spec.tsx b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DataProtection.spec.tsx index 82da0beb6336..abae9051cb3e 100644 --- a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DataProtection.spec.tsx +++ b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DataProtection.spec.tsx @@ -1,6 +1,6 @@ import '@/common/setupTests'; import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; import { wrapper } from '@/common/utils/test.provider'; @@ -8,7 +8,6 @@ import DataProtection from './DataProtection'; import { TDomainResource, DisclosureConfigurationEnum, - DataProtectionStatus, } from '@/domain/types/domainResource'; import { DnsConfigurationTypeEnum } from '@/domain/enum/dnsConfigurationType.enum'; import { DomainStateEnum } from '@/domain/enum/domainState.enum'; diff --git a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.spec.tsx b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.spec.tsx new file mode 100644 index 000000000000..14d4bf8a01b8 --- /dev/null +++ b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.spec.tsx @@ -0,0 +1,184 @@ +import '@/common/setupTests'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; +import { wrapper } from '@/common/utils/test.provider'; +import DnsState from './DnsState'; +import { OptionStateEnum } from '@/domain/enum/optionState.enum'; +import { TDomainResource } from '@/domain/types/domainResource'; +import { DnsConfigurationTypeEnum } from '@/domain/enum/dnsConfigurationType.enum'; +import { DomainStateEnum } from '@/domain/enum/domainState.enum'; +import { ProtectionStateEnum } from '@/domain/enum/protectionState.enum'; +import { SuspensionStateEnum } from '@/domain/enum/suspensionState.enum'; +import { ResourceStatusEnum } from '@/domain/enum/resourceStatus.enum'; +import { OptionEnum } from '@/common/enum/option.enum'; + +describe('DnsState component', () => { + const mockDomainResource: TDomainResource = { + checksum: 'checksum-value', + currentState: { + additionalStates: [], + dnsConfiguration: { + configurationType: DnsConfigurationTypeEnum.HOSTING, + glueRecordIPv6Supported: true, + hostSupported: true, + maxDNS: 10, + minDNS: 2, + nameServers: [ + { + ipv4: '192.0.2.1', + ipv6: '2001:db8::1', + nameServer: 'ns1.example.com', + nameServerType: DnsConfigurationTypeEnum.HOSTING, + }, + { + ipv4: '192.0.2.2', + ipv6: '2001:db8::2', + nameServer: 'ns2.example.com', + nameServerType: DnsConfigurationTypeEnum.HOSTING, + }, + ], + dnssecSupported: true, + }, + hostsConfiguration: { + ipv4Supported: true, + ipv6Supported: true, + multipleIPsSupported: true, + hostSupported: true, + hosts: [], + }, + contactsConfiguration: { + contactAdministrator: { + id: 'contact-admin-id', + }, + contactBilling: { + id: 'contact-billing-id', + }, + contactTechnical: { + id: 'contact-tech-id', + }, + contactOwner: { + id: 'contact-owner-id', + }, + }, + extension: '.com', + mainState: DomainStateEnum.OK, + name: 'example.com', + protectionState: ProtectionStateEnum.UNPROTECTED, + suspensionState: SuspensionStateEnum.NOT_SUSPENDED, + authInfoManagedByOVHcloud: true, + authInfoSupported: true, + createdAt: '2024-01-01T00:00:00Z', + }, + currentTasks: [], + iam: null, + id: 'domain-id', + resourceStatus: ResourceStatusEnum.READY, + }; + + const mockAnycastOption = { + state: OptionStateEnum.SUBSCRIBED, + expirationDate: new Date('2025-12-31T23:59:59Z').toString(), + }; + + it('renders DNS state label and notes', () => { + render( + , + { wrapper }, + ); + + expect( + screen.getByText('domain_tab_general_information_dns_standard'), + ).toBeInTheDocument(); + expect( + screen.getByText('domain_tab_general_information_note_anycast'), + ).toBeInTheDocument(); + }); + + it('renders tooltip with anycast label when anycast is subscribed', () => { + const { container } = render( + , + { wrapper }, + ); + + const button = container.querySelector( + 'ods-button[label*="domain_dns_tab_button_cancel_terminate_anycast"]', + ); + expect(button).toBeInTheDocument(); + }); + + it('renders tooltip with anycast label when anycast is released', () => { + const releasedAnycastOption = { + ...mockAnycastOption, + state: OptionStateEnum.RELEASED, + }; + + const { container } = render( + , + { wrapper }, + ); + + const button = container.querySelector( + 'ods-button[label*="domain_dns_tab_button_cancel_terminate_anycast"]', + ); + expect(button).toBeInTheDocument(); + }); + + it('renders tooltip with anycast label when anycast is not set', () => { + const { container } = render( + , + { wrapper }, + ); + + const button = container.querySelector( + 'ods-button[label="domain_tab_DNS_anycast_order"]', + ); + expect(button).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.tsx b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.tsx new file mode 100644 index 000000000000..5668af6cfe00 --- /dev/null +++ b/packages/manager/apps/web-domains/src/domain/components/ConfigurationCards/DnsState.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { + ActionMenu, + ManagerTile, + useFormatDate, +} from '@ovh-ux/manager-react-components'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { getDnsStateDetails } from '@/domain/utils/dnsUtils'; +import { OptionStateEnum } from '@/domain/enum/optionState.enum'; +import { useGenerateUrl } from '@/domain/hooks/generateUrl/useGenerateUrl'; +import { urls } from '@/domain/routes/routes.constant'; +import { TDomainOption, TDomainResource } from '@/domain/types/domainResource'; + +interface DnsStateProps { + readonly serviceName: string; + readonly domainResource: TDomainResource; + readonly anycastOption: TDomainOption; + readonly isFetchingAnycastOption: boolean; + readonly anycastTerminateModalOpen: boolean; + readonly setAnycastTerminateModalOpen: React.Dispatch< + React.SetStateAction + >; + readonly restoreAnycast: boolean; + readonly setRestoreAnycast: React.Dispatch>; +} +export default function DnsState({ + serviceName, + domainResource, + anycastOption, + anycastTerminateModalOpen, + isFetchingAnycastOption, + setAnycastTerminateModalOpen, + restoreAnycast, + setRestoreAnycast, +}: DnsStateProps) { + const { t } = useTranslation(['domain', NAMESPACES.ACTIONS]); + + const formatDate = useFormatDate(); + + const navigate = useNavigate(); + + React.useEffect(() => { + if (anycastOption) { + switch (anycastOption.state) { + case OptionStateEnum.RELEASED.toLowerCase(): { + setRestoreAnycast(true); + break; + } + case OptionStateEnum.SUBSCRIBED.toLowerCase(): { + setRestoreAnycast(false); + break; + } + default: + setRestoreAnycast(false); + break; + } + } + }, [anycastOption]); + + const dnsState = getDnsStateDetails( + domainResource.currentState.dnsConfiguration.configurationType, + ); + + const handleAnycastLabel = () => { + if (!anycastOption) { + return t('domain_tab_DNS_anycast_order'); + } + + if (anycastOption && restoreAnycast) { + return t('domain_dns_tab_button_cancel_terminate_anycast', { + action: t(`${NAMESPACES.ACTIONS}:${'restore'}`), + }); + } + + return t('domain_dns_tab_button_cancel_terminate_anycast', { + action: t(`${NAMESPACES.ACTIONS}:${'terminate'}`), + }); + }; + + const handleBtnClick = () => { + if (anycastOption) { + setAnycastTerminateModalOpen(!anycastTerminateModalOpen); + } else { + navigate( + useGenerateUrl(urls.domainTabOrderAnycast, 'path', { + serviceName, + }), + { replace: true }, + ); + } + }; + + return ( + + + {t('domain_tab_general_information_dns_title')} + + +
+ {t(dnsState.label)} + {dnsState.anycastSupported && ( + handleBtnClick(), + }, + ]} + /> + )} +
+ + {restoreAnycast ? ( + }} + /> + ) : ( + t(dnsState.notes) + )} + +
+
+ ); +} diff --git a/packages/manager/apps/web-domains/src/domain/constants/configuration.card.ts b/packages/manager/apps/web-domains/src/domain/constants/configuration.card.ts index 3e4420a59138..4dfb625dfc80 100644 --- a/packages/manager/apps/web-domains/src/domain/constants/configuration.card.ts +++ b/packages/manager/apps/web-domains/src/domain/constants/configuration.card.ts @@ -2,6 +2,7 @@ import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { BADGE_COLOR } from '@ovhcloud/ods-react'; import { ProtectionStateEnum } from '@/domain/enum/protectionState.enum'; import { DataProtectionStatus } from '../types/domainResource'; +import { DnsConfigurationTypeEnum } from '../enum/dnsConfigurationType.enum'; export const ConfigurationDnssecBadgeColorAndContent = { not_supported: { @@ -121,3 +122,52 @@ export const ConfigurationDataProtectionBadgeColorAndContent = { i18nkeySubContent: 'domain_tab_general_information_data_protection_partial', }, }; +export const ConfigurationDnsStateAndContent = [ + { + dnsTypes: [ + DnsConfigurationTypeEnum.HOSTING, + DnsConfigurationTypeEnum.HOLD, + DnsConfigurationTypeEnum.PARKING, + ], + result: { + label: 'domain_tab_general_information_dns_standard', + notes: 'domain_tab_general_information_note_anycast', + anycastSupported: true, + }, + }, + { + dnsTypes: [DnsConfigurationTypeEnum.ANYCAST], + result: { + label: 'domain_tab_general_information_dns_anycast', + notes: '', + anycastSupported: true, + }, + }, + { + dnsTypes: [ + DnsConfigurationTypeEnum.EXTERNAL, + DnsConfigurationTypeEnum.MIXED, + ], + result: { + label: 'domain_tab_general_information_dns_personnalised', + notes: '', + anycastSupported: false, + }, + }, + { + dnsTypes: [DnsConfigurationTypeEnum.DEDICATED], + result: { + label: 'domain_tab_general_information_dns_dedicated', + notes: '', + anycastSupported: false, + }, + }, + { + dnsTypes: [DnsConfigurationTypeEnum.EMPTY], + result: { + label: 'domain_tab_general_information_dns_empty', + notes: '', + anycastSupported: false, + }, + }, +]; diff --git a/packages/manager/apps/web-domains/src/domain/utils/dnsUtils.ts b/packages/manager/apps/web-domains/src/domain/utils/dnsUtils.ts index 715c7f5cf576..093e7a26a154 100644 --- a/packages/manager/apps/web-domains/src/domain/utils/dnsUtils.ts +++ b/packages/manager/apps/web-domains/src/domain/utils/dnsUtils.ts @@ -12,6 +12,7 @@ import { DnsConfigurationTypeEnum, } from '@/domain/enum/dnsConfigurationType.enum'; import { TDomainZone } from '@/domain/types/domainZone'; +import { ConfigurationDnsStateAndContent } from '../constants/configuration.card'; const INTERNAL_DNS_PATTERN: Record = { EU: /^d?ns(?:\d{1,3})?\.ovh\.net/i, @@ -198,3 +199,10 @@ export function canSaveNewDnsConfig( areArraysDifferent() ); } + +export function getDnsStateDetails(dnsConfiguration: DnsConfigurationTypeEnum) { + const values = ConfigurationDnsStateAndContent.find((dns) => { + return dns.dnsTypes.includes(dnsConfiguration); + }); + return values.result; +}