From 03b1533a6f12777a5ae44dfc8e6453cd4df29e7c Mon Sep 17 00:00:00 2001 From: JacquesLarique <134954692+JacquesLarique@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:35:32 +0200 Subject: [PATCH] feat(hub): rewrote hub in react (#13689) ref: MANAGER-14935 Signed-off-by: Jacques Larique Co-authored-by: Omar ALKABOUSS MOUSSANA --- packages/manager/apps/hub-react/README.md | 3 + packages/manager/apps/hub-react/cucumber.js | 20 + packages/manager/apps/hub-react/index.html | 22 + packages/manager/apps/hub-react/package.json | 68 + .../apps/hub-react/playwright.config.ts | 20 + .../manager/apps/hub-react/postcss.config.js | 6 + .../billing/actions/Messages_de_DE.json | 24 + .../billing/actions/Messages_en_GB.json | 24 + .../billing/actions/Messages_es_ES.json | 24 + .../billing/actions/Messages_fr_CA.json | 24 + .../billing/actions/Messages_fr_FR.json | 24 + .../billing/actions/Messages_it_IT.json | 24 + .../billing/actions/Messages_pl_PL.json | 24 + .../billing/actions/Messages_pt_PT.json | 24 + .../billing/status/Messages_de_DE.json | 12 + .../billing/status/Messages_en_GB.json | 12 + .../billing/status/Messages_es_ES.json | 12 + .../billing/status/Messages_fr_CA.json | 12 + .../billing/status/Messages_fr_FR.json | 12 + .../billing/status/Messages_it_IT.json | 12 + .../billing/status/Messages_pl_PL.json | 12 + .../billing/status/Messages_pt_PT.json | 12 + .../translations/hub/Messages_de_DE.json | 7 + .../translations/hub/Messages_en_GB.json | 7 + .../translations/hub/Messages_es_ES.json | 7 + .../translations/hub/Messages_fr_CA.json | 7 + .../translations/hub/Messages_fr_FR.json | 7 + .../translations/hub/Messages_it_IT.json | 7 + .../translations/hub/Messages_pl_PL.json | 7 + .../translations/hub/Messages_pt_PT.json | 7 + .../hub/billing/Messages_de_DE.json | 15 + .../hub/billing/Messages_en_GB.json | 15 + .../hub/billing/Messages_es_ES.json | 15 + .../hub/billing/Messages_fr_CA.json | 15 + .../hub/billing/Messages_fr_FR.json | 15 + .../hub/billing/Messages_it_IT.json | 15 + .../hub/billing/Messages_pl_PL.json | 15 + .../hub/billing/Messages_pt_PT.json | 15 + .../hub/catalog/Messages_de_DE.json | 5 + .../hub/catalog/Messages_en_GB.json | 5 + .../hub/catalog/Messages_es_ES.json | 5 + .../hub/catalog/Messages_fr_CA.json | 5 + .../hub/catalog/Messages_fr_FR.json | 5 + .../hub/catalog/Messages_it_IT.json | 5 + .../hub/catalog/Messages_pl_PL.json | 5 + .../hub/catalog/Messages_pt_PT.json | 5 + .../hub/error/Messages_de_DE.json | 4 + .../hub/error/Messages_en_GB.json | 4 + .../hub/error/Messages_es_ES.json | 4 + .../hub/error/Messages_fr_CA.json | 4 + .../hub/error/Messages_fr_FR.json | 4 + .../hub/error/Messages_it_IT.json | 4 + .../hub/error/Messages_pl_PL.json | 4 + .../hub/error/Messages_pt_PT.json | 4 + .../translations/hub/kyc/Messages_de_DE.json | 9 + .../translations/hub/kyc/Messages_en_GB.json | 9 + .../translations/hub/kyc/Messages_es_ES.json | 9 + .../translations/hub/kyc/Messages_fr_CA.json | 9 + .../translations/hub/kyc/Messages_fr_FR.json | 9 + .../translations/hub/kyc/Messages_it_IT.json | 9 + .../translations/hub/kyc/Messages_pl_PL.json | 9 + .../translations/hub/kyc/Messages_pt_PT.json | 9 + .../hub/order/Messages_de_DE.json | 21 + .../hub/order/Messages_en_GB.json | 21 + .../hub/order/Messages_es_ES.json | 21 + .../hub/order/Messages_fr_CA.json | 21 + .../hub/order/Messages_fr_FR.json | 21 + .../hub/order/Messages_it_IT.json | 21 + .../hub/order/Messages_pl_PL.json | 21 + .../hub/order/Messages_pt_PT.json | 21 + .../hub/payment-status/Messages_de_DE.json | 9 + .../hub/payment-status/Messages_en_GB.json | 9 + .../hub/payment-status/Messages_es_ES.json | 9 + .../hub/payment-status/Messages_fr_CA.json | 9 + .../hub/payment-status/Messages_fr_FR.json | 9 + .../hub/payment-status/Messages_it_IT.json | 9 + .../hub/payment-status/Messages_pl_PL.json | 9 + .../hub/payment-status/Messages_pt_PT.json | 9 + .../hub/products/Messages_de_DE.json | 71 + .../hub/products/Messages_en_GB.json | 71 + .../hub/products/Messages_es_ES.json | 71 + .../hub/products/Messages_fr_CA.json | 71 + .../hub/products/Messages_fr_FR.json | 71 + .../hub/products/Messages_it_IT.json | 71 + .../hub/products/Messages_pl_PL.json | 71 + .../hub/products/Messages_pt_PT.json | 71 + .../hub/siret/Messages_de_DE.json | 9 + .../hub/siret/Messages_en_GB.json | 9 + .../hub/siret/Messages_es_ES.json | 9 + .../hub/siret/Messages_fr_CA.json | 9 + .../hub/siret/Messages_fr_FR.json | 9 + .../hub/siret/Messages_it_IT.json | 9 + .../hub/siret/Messages_pl_PL.json | 9 + .../hub/siret/Messages_pt_PT.json | 9 + .../hub/support/Messages_de_DE.json | 12 + .../hub/support/Messages_en_GB.json | 12 + .../hub/support/Messages_es_ES.json | 12 + .../hub/support/Messages_fr_CA.json | 12 + .../hub/support/Messages_fr_FR.json | 12 + .../hub/support/Messages_it_IT.json | 12 + .../hub/support/Messages_pl_PL.json | 12 + .../hub/support/Messages_pt_PT.json | 12 + packages/manager/apps/hub-react/src/App.tsx | 22 + .../hub-react/src/_mock_/billingServices.ts | 150 ++ .../apps/hub-react/src/_mock_/catalog.ts | 894 ++++++++ .../apps/hub-react/src/_mock_/products.ts | 1983 +++++++++++++++++ .../apps/hub-react/src/billing.constants.ts | 41 + .../BillingStatus.component.tsx | 72 + .../billing-status/BillingStatus.constants.ts | 15 + .../ServicesActions.component.tsx | 108 + .../src/billing/hooks/useServiceActions.ts | 223 ++ .../src/billing/hooks/useServiceLinks.tsx | 187 ++ .../src/billing/types/billingServices.type.ts | 332 +++ .../src/billing/types/service-links.type.ts | 18 + .../components/banner/Banner.component.tsx | 61 + .../src/components/banner/Banner.spec.tsx | 104 + .../HubOrderTracking.component.tsx | 227 ++ .../HubOrderTracking.spec.tsx | 133 ++ .../hub-support/HubSupport.component.tsx | 129 ++ .../hub-support/HubSupport.constants.ts | 8 + .../hub-support/HubSupport.spec.tsx | 149 ++ .../hub-support/assets/assistance.png | Bin 0 -> 22390 bytes .../HubSupportHelp.component.tsx | 59 + .../hub-support-help/HubSupportHelp.spec.tsx | 53 + .../HubSupportTable.component.tsx | 28 + .../HubSupportTable.spec.tsx | 49 + .../HubSupportTableItem.component.tsx | 113 + .../HubSupportTableItem.spec.tsx | 116 + .../products/Products.component.tsx | 221 ++ .../components/products/Products.constants.ts | 192 ++ .../src/components/products/Products.spec.tsx | 162 ++ .../components/products/Products.style.scss | 11 + .../skeletons/Skeletons.component.tsx | 11 + .../components/skeletons/Skeletons.spec.tsx | 36 + .../tile-error/TileError.component.spec.tsx | 36 + .../tile-error/TileError.component.tsx | 75 + .../TileGridSkeleton.component.tsx | 23 + .../TileGridSkeleton.spec.tsx | 15 + .../tile-skeleton/TileSkeleton.component.tsx | 27 + .../tile-skeleton/TileSkeleton.spec.tsx | 17 + .../components/welcome/Welcome.component.tsx | 56 + .../src/components/welcome/Welcome.spec.tsx | 60 + .../hub-react/src/data/api/apiHubSupport.ts | 16 + .../data/api/apiOrder/apiOrder.constants.ts | 13 + .../src/data/api/apiOrder/apiOrder.ts | 85 + .../apps/hub-react/src/data/api/banner.ts | 9 + .../hub-react/src/data/api/billingServices.ts | 29 + .../apps/hub-react/src/data/api/bills.ts | 17 + .../apps/hub-react/src/data/api/catalog.ts | 35 + .../apps/hub-react/src/data/api/debt.ts | 8 + .../apps/hub-react/src/data/api/kyc.ts | 9 + .../apps/hub-react/src/data/api/lastOrder.ts | 7 + .../hub-react/src/data/api/notifications.ts | 16 + .../apps/hub-react/src/data/api/services.ts | 10 + .../apiHubSupport/useHubSupport.spec.tsx | 69 + .../hooks/apiHubSupport/useHubSupport.tsx | 31 + .../data/hooks/apiOrder/useLastOrder.spec.tsx | 47 + .../src/data/hooks/apiOrder/useLastOrder.tsx | 10 + .../src/data/hooks/banner/useBanner.spec.tsx | 62 + .../src/data/hooks/banner/useBanner.tsx | 11 + .../useBillingServices.spec.tsx | 34 + .../billingServices/useBillingServices.tsx | 11 + .../src/data/hooks/bills/useBills.spec.tsx | 52 + .../src/data/hooks/bills/useBills.tsx | 11 + .../data/hooks/catalog/useCatalog.spec.tsx | 46 + .../src/data/hooks/catalog/useCatalog.tsx | 11 + .../src/data/hooks/debt/useDebt.spec.tsx | 66 + .../hub-react/src/data/hooks/debt/useDebt.tsx | 11 + .../src/data/hooks/kyc/useKyc.spec.tsx | 39 + .../hub-react/src/data/hooks/kyc/useKyc.tsx | 17 + .../hooks/lastOrder/useLastOrder.spec.tsx | 77 + .../src/data/hooks/lastOrder/useLastOrder.tsx | 10 + .../notifications/useNotifications.spec.tsx | 42 + .../hooks/notifications/useNotifications.tsx | 11 + .../data/hooks/services/useServices.spec.tsx | 38 + .../src/data/hooks/services/useServices.tsx | 12 + .../hooks/dateFormat/useDateFormat.spec.tsx | 52 + .../src/hooks/dateFormat/useDateFormat.tsx | 16 + .../guideUtils/useGuideUtils.constants.ts | 30 + .../hooks/guideUtils/useGuideUtils.spec.tsx | 50 + .../src/hooks/guideUtils/useGuideUtils.tsx | 41 + .../hooks/periodFilter/usePeriodFilter.tsx | 14 + .../src/hooks/priceFormat/usePriceFormat.tsx | 14 + .../src/hooks/products/useProducts.spec.tsx | 117 + .../src/hooks/products/useProducts.ts | 47 + .../manager/apps/hub-react/src/hub.config.ts | 3 + .../apps/hub-react/src/hub.constants.ts | 12 + .../manager/apps/hub-react/src/index.scss | 102 + packages/manager/apps/hub-react/src/index.tsx | 73 + .../manager/apps/hub-react/src/pages/404.tsx | 28 + .../apps/hub-react/src/pages/layout.test.tsx | 60 + .../apps/hub-react/src/pages/layout.tsx | 29 + .../pages/layout/BillingSummary.component.tsx | 223 ++ .../pages/layout/BillingSummary.style.scss | 46 + .../src/pages/layout/Catalog.component.tsx | 102 + .../EnterpriseBillingSummary.component.tsx | 74 + .../pages/layout/KycFraudBanner.component.tsx | 120 + .../pages/layout/KycIndiaBanner.component.tsx | 110 + .../NotificationsCarousel.component.tsx | 138 ++ .../pages/layout/PaymentStatus.component.tsx | 304 +++ .../pages/layout/SiretBanner.component.tsx | 111 + .../src/pages/layout/SiretModal.component.tsx | 115 + .../layout/assets/billing-background.svg | 29 + .../src/pages/layout/layout.constants.ts | 30 + .../src/pages/layout/layout.test.tsx | 1056 +++++++++ .../hub-react/src/pages/layout/layout.tsx | 327 +++ .../manager/apps/hub-react/src/queryClient.ts | 12 + .../hub-react/src/routes/routes.constant.ts | 7 + .../apps/hub-react/src/routes/routes.tsx | 31 + .../manager/apps/hub-react/src/setupTests.ts | 12 + .../apps/hub-react/src/tracking.constant.ts | 20 + .../hub-react/src/types/apiEnvelope.type.ts | 9 + .../apps/hub-react/src/types/banner.type.ts | 17 + .../apps/hub-react/src/types/bills.type.ts | 24 + .../apps/hub-react/src/types/catalog.ts | 20 + .../apps/hub-react/src/types/debt.type.ts | 13 + .../apps/hub-react/src/types/kyc.type.ts | 15 + .../hub-react/src/types/lastOrder.type.ts | 28 + .../hub-react/src/types/notifications.type.ts | 22 + .../apps/hub-react/src/types/order.type.ts | 70 + .../apps/hub-react/src/types/services.type.ts | 42 + .../apps/hub-react/src/types/support.type.ts | 17 + .../manager/apps/hub-react/src/vite-hmr.ts | 5 + .../manager/apps/hub-react/tailwind.config.js | 14 + packages/manager/apps/hub-react/tsconfig.json | 27 + .../manager/apps/hub-react/tsconfig.test.json | 6 + .../manager/apps/hub-react/vite.config.mjs | 8 + .../manager/apps/hub-react/vitest.config.js | 30 + .../hub/src/translations/Messages_pt_PT.json | 2 +- yarn.lock | 7 +- 230 files changed, 12840 insertions(+), 2 deletions(-) create mode 100644 packages/manager/apps/hub-react/README.md create mode 100644 packages/manager/apps/hub-react/cucumber.js create mode 100644 packages/manager/apps/hub-react/index.html create mode 100644 packages/manager/apps/hub-react/package.json create mode 100644 packages/manager/apps/hub-react/playwright.config.ts create mode 100644 packages/manager/apps/hub-react/postcss.config.js create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/billing/status/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/error/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/order/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/products/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_de_DE.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_en_GB.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_es_ES.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_CA.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_FR.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_it_IT.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_pl_PL.json create mode 100644 packages/manager/apps/hub-react/public/translations/hub/support/Messages_pt_PT.json create mode 100644 packages/manager/apps/hub-react/src/App.tsx create mode 100644 packages/manager/apps/hub-react/src/_mock_/billingServices.ts create mode 100644 packages/manager/apps/hub-react/src/_mock_/catalog.ts create mode 100644 packages/manager/apps/hub-react/src/_mock_/products.ts create mode 100644 packages/manager/apps/hub-react/src/billing.constants.ts create mode 100644 packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.component.tsx create mode 100644 packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.constants.ts create mode 100644 packages/manager/apps/hub-react/src/billing/components/services-actions/ServicesActions.component.tsx create mode 100644 packages/manager/apps/hub-react/src/billing/hooks/useServiceActions.ts create mode 100644 packages/manager/apps/hub-react/src/billing/hooks/useServiceLinks.tsx create mode 100644 packages/manager/apps/hub-react/src/billing/types/billingServices.type.ts create mode 100644 packages/manager/apps/hub-react/src/billing/types/service-links.type.ts create mode 100644 packages/manager/apps/hub-react/src/components/banner/Banner.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/banner/Banner.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/HubSupport.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/HubSupport.constants.ts create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/HubSupport.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/assets/assistance.png create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/products/Products.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/products/Products.constants.ts create mode 100644 packages/manager/apps/hub-react/src/components/products/Products.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/products/Products.style.scss create mode 100644 packages/manager/apps/hub-react/src/components/skeletons/Skeletons.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/skeletons/Skeletons.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-error/TileError.component.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-error/TileError.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/components/welcome/Welcome.component.tsx create mode 100644 packages/manager/apps/hub-react/src/components/welcome/Welcome.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/api/apiHubSupport.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.constants.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/banner.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/billingServices.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/bills.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/catalog.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/debt.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/kyc.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/lastOrder.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/notifications.ts create mode 100644 packages/manager/apps/hub-react/src/data/api/services.ts create mode 100644 packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/bills/useBills.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/bills/useBills.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/services/useServices.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/data/hooks/services/useServices.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.constants.ts create mode 100644 packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/periodFilter/usePeriodFilter.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/priceFormat/usePriceFormat.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/products/useProducts.spec.tsx create mode 100644 packages/manager/apps/hub-react/src/hooks/products/useProducts.ts create mode 100644 packages/manager/apps/hub-react/src/hub.config.ts create mode 100644 packages/manager/apps/hub-react/src/hub.constants.ts create mode 100644 packages/manager/apps/hub-react/src/index.scss create mode 100644 packages/manager/apps/hub-react/src/index.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/404.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout.test.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/BillingSummary.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/BillingSummary.style.scss create mode 100644 packages/manager/apps/hub-react/src/pages/layout/Catalog.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/EnterpriseBillingSummary.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/KycFraudBanner.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/KycIndiaBanner.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/NotificationsCarousel.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/PaymentStatus.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/SiretBanner.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/SiretModal.component.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/assets/billing-background.svg create mode 100644 packages/manager/apps/hub-react/src/pages/layout/layout.constants.ts create mode 100644 packages/manager/apps/hub-react/src/pages/layout/layout.test.tsx create mode 100644 packages/manager/apps/hub-react/src/pages/layout/layout.tsx create mode 100644 packages/manager/apps/hub-react/src/queryClient.ts create mode 100644 packages/manager/apps/hub-react/src/routes/routes.constant.ts create mode 100644 packages/manager/apps/hub-react/src/routes/routes.tsx create mode 100644 packages/manager/apps/hub-react/src/setupTests.ts create mode 100644 packages/manager/apps/hub-react/src/tracking.constant.ts create mode 100644 packages/manager/apps/hub-react/src/types/apiEnvelope.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/banner.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/bills.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/catalog.ts create mode 100644 packages/manager/apps/hub-react/src/types/debt.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/kyc.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/lastOrder.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/notifications.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/order.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/services.type.ts create mode 100644 packages/manager/apps/hub-react/src/types/support.type.ts create mode 100644 packages/manager/apps/hub-react/src/vite-hmr.ts create mode 100644 packages/manager/apps/hub-react/tailwind.config.js create mode 100644 packages/manager/apps/hub-react/tsconfig.json create mode 100644 packages/manager/apps/hub-react/tsconfig.test.json create mode 100644 packages/manager/apps/hub-react/vite.config.mjs create mode 100644 packages/manager/apps/hub-react/vitest.config.js diff --git a/packages/manager/apps/hub-react/README.md b/packages/manager/apps/hub-react/README.md new file mode 100644 index 000000000000..b16dc6e422b4 --- /dev/null +++ b/packages/manager/apps/hub-react/README.md @@ -0,0 +1,3 @@ +# @ovh-ux/manager-hub-app + +> OVHcloud Dashboard control panel. diff --git a/packages/manager/apps/hub-react/cucumber.js b/packages/manager/apps/hub-react/cucumber.js new file mode 100644 index 000000000000..8e6abbfbca8f --- /dev/null +++ b/packages/manager/apps/hub-react/cucumber.js @@ -0,0 +1,20 @@ +const isCI = process.env.CI; + +module.exports = { + default: { + paths: ['e2e/features/**/*.feature'], + require: [ + '../../../../playwright-helpers/bdd-setup.ts', + 'e2e/**/*.step.ts', + ], + requireModule: ['ts-node/register'], + format: [ + 'summary', + isCI ? 'progress' : 'progress-bar', + !isCI && ['html', 'e2e/reports/cucumber-results-report.html'], + !isCI && ['usage-json', 'e2e/reports/cucumber-usage-report.json'], + ].filter(Boolean), + formatOptions: { snippetInterface: 'async-await' }, + retry: 1, + }, +}; diff --git a/packages/manager/apps/hub-react/index.html b/packages/manager/apps/hub-react/index.html new file mode 100644 index 000000000000..9b49e88f39c5 --- /dev/null +++ b/packages/manager/apps/hub-react/index.html @@ -0,0 +1,22 @@ + + + + + + + + OVHcloud + + + + + +
+ + + diff --git a/packages/manager/apps/hub-react/package.json b/packages/manager/apps/hub-react/package.json new file mode 100644 index 000000000000..de75a867c53d --- /dev/null +++ b/packages/manager/apps/hub-react/package.json @@ -0,0 +1,68 @@ +{ + "name": "@ovh-ux/manager-hub-react-app", + "version": "0.0.0", + "private": true, + "description": "OVHcloud Dashboard control panel.", + "repository": { + "type": "git", + "url": "git+https://github.com/ovh/manager.git", + "directory": "packages/manager/apps/hub-react" + }, + "license": "BSD-3-Clause", + "author": "OVH SAS", + "scripts": { + "build": "tsc && vite build", + "dev": "tsc && vite", + "start": "lerna exec --stream --scope='@ovh-ux/manager-hub-react-app' --include-dependencies -- npm run build --if-present", + "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-hub-react-app' --include-dependencies -- npm run dev --if-present", + "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-hub-react-app' --include-dependencies -- npm run dev:watch --if-present", + "test": "vitest run", + "test:e2e": "tsc && node ../../../../scripts/run-playwright-bdd.js", + "test:e2e:ci": "tsc && node ../../../../scripts/run-playwright-bdd.js --ci", + "test:watch": "vitest watch" + }, + "dependencies": { + "@ovh-ux/manager-config": "^7.3.3", + "@ovh-ux/manager-core-api": "^0.8.0", + "@ovh-ux/manager-models": "^1.14.13", + "@ovh-ux/manager-react-components": "^1.31.0", + "@ovh-ux/manager-react-shell-client": "^0.7.0", + "@ovh-ux/manager-tailwind-config": "^0.2.0", + "@ovh-ux/request-tagger": "^0.3.0", + "@ovhcloud/ods-common-core": "17.2.1", + "@ovhcloud/ods-common-theming": "17.2.1", + "@ovhcloud/ods-components": "17.2.1", + "@ovhcloud/ods-theme-blue-jeans": "17.2.1", + "@tanstack/react-query": "^5.51.21", + "axios": "^1.1.2", + "clsx": "^1.2.1", + "i18next": "^23.8.2", + "i18next-http-backend": "^2.4.2", + "punycode": "^2.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.5", + "react-router-dom": "^6.3.0" + }, + "devDependencies": { + "@cucumber/cucumber": "^10.3.1", + "@ovh-ux/manager-vite-config": "^0.8.0", + "@playwright/test": "^1.41.2", + "@tanstack/react-query-devtools": "^5.51.21", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^14.1.2", + "@types/punycode": "^2.1.4", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.2.0", + "element-internals-polyfill": "^1.3.10", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^5.2.13", + "vitest": "^1.2.0" + }, + "regions": [ + "CA", + "EU", + "US" + ] +} diff --git a/packages/manager/apps/hub-react/playwright.config.ts b/packages/manager/apps/hub-react/playwright.config.ts new file mode 100644 index 000000000000..feb249bcbe3f --- /dev/null +++ b/packages/manager/apps/hub-react/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + workers: 3, + fullyParallel: false, + timeout: 30 * 1000, + reporter: [['html', { open: 'on-failure' }]], + expect: { + timeout: 20000, + }, + use: { + // Collect trace when retrying the failed test. + trace: 'retain-on-failure', + }, + testMatch: '**/*.e2e.ts', + webServer: { + command: 'yarn run dev', + url: 'http://localhost:9000/', + }, +}); diff --git a/packages/manager/apps/hub-react/postcss.config.js b/packages/manager/apps/hub-react/postcss.config.js new file mode 100644 index 000000000000..12a703d900da --- /dev/null +++ b/packages/manager/apps/hub-react/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_de_DE.json new file mode 100644 index 000000000000..3c55c7700cf0 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_de_DE.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Mehr Aktionen für diesen Dienst", + "billing_autorenew_service_enable_autorenew": "Automatische Zahlung aktivieren", + "billing_services_actions_menu_pay_bill": "Meine Rechnung begleichen", + "billing_services_actions_menu_manage_renew": "Verlängerung konfigurieren", + "billing_services_actions_menu_exchange_update_accounts": "Verlängerung der Accounts konfigurieren", + "billing_services_actions_menu_anticipate_renew": "Vorauszahlen", + "billing_services_actions_menu_resiliate": "Meine Vertragsbindung kündigen", + "billing_services_actions_menu_resiliate_my_engagement": "Meine Vertragsbindung kündigen", + "billing_services_actions_menu_renew_label": "Dienst verlängern: {{ serviceName }} (Neues Fenster)", + "billing_services_actions_menu_renew": "Dienst verlängern", + "billing_services_actions_menu_exchange_update": "Abrechnung bearbeiten", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "MX Plan sofort löschen", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Enterprise Cloud Databases sofort löschen", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Hosting sofort löschen", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Mein SQL Private Hosting kündigen", + "billing_services_actions_menu_resiliate_WEBCOACH": "WebCoach löschen", + "billing_services_actions_menu_sms_credit": "Guthaben hinzufügen", + "billing_services_actions_menu_sms_renew": "Automatische Aufladung konfigurieren", + "billing_services_actions_menu_resiliate_cancel": "Kündigung der Dienstes stornieren", + "billing_services_actions_menu_see_dashboard": "Dienstdetails anzeigen", + "billing_services_actions_menu_commit": "Meine Abonnementlaufzeit verwalten", + "billing_services_actions_menu_commit_cancel": "Abonnementbestellung mit fester Laufzeit stornieren" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_en_GB.json new file mode 100644 index 000000000000..24a7ff523083 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_en_GB.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Further actions on this service ", + "billing_autorenew_service_enable_autorenew": "Enable automatic payment", + "billing_services_actions_menu_pay_bill": "Pay my bill", + "billing_services_actions_menu_manage_renew": "Configure renewal", + "billing_services_actions_menu_exchange_update_accounts": "Configure renewal for accounts", + "billing_services_actions_menu_anticipate_renew": "Bring forward payment", + "billing_services_actions_menu_resiliate": "Cancel subscription", + "billing_services_actions_menu_resiliate_my_engagement": "Cancel subscription", + "billing_services_actions_menu_renew_label": "Renew the following service: {{ serviceName }} (New window)", + "billing_services_actions_menu_renew": "Renew service", + "billing_services_actions_menu_exchange_update": "Modify billing", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Delete MX Plan immediately", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Delete Enterprise Cloud Database immediately", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Delete web hosting plan immediately", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Delete my Private SQL hosting service", + "billing_services_actions_menu_resiliate_WEBCOACH": "Delete my Web Coach", + "billing_services_actions_menu_sms_credit": "Add credits", + "billing_services_actions_menu_sms_renew": "Configure automatic reloading", + "billing_services_actions_menu_resiliate_cancel": "Stop cancellation of service", + "billing_services_actions_menu_see_dashboard": "View service details", + "billing_services_actions_menu_commit": "Manage my commitment", + "billing_services_actions_menu_commit_cancel": "Cancel subscription request" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_es_ES.json new file mode 100644 index 000000000000..a3844f6d0fb9 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_es_ES.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Más acciones sobre este servicio", + "billing_autorenew_service_enable_autorenew": "Activar el pago automático", + "billing_services_actions_menu_pay_bill": "Abonar la factura", + "billing_services_actions_menu_manage_renew": "Configurar la renovación", + "billing_services_actions_menu_exchange_update_accounts": "Configurar la renovación de las cuentas", + "billing_services_actions_menu_anticipate_renew": "Adelantar el pago", + "billing_services_actions_menu_resiliate": "Cancelar mi compromiso", + "billing_services_actions_menu_resiliate_my_engagement": "Cancelar mi compromiso", + "billing_services_actions_menu_renew_label": "Renovar el servicio: {{ serviceName }} (nueva ventana)", + "billing_services_actions_menu_renew": "Renovar el servicio", + "billing_services_actions_menu_exchange_update": "Modificar la facturación", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Eliminar inmediatamente el MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Eliminar inmediatamente Enterprise Cloud Databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Eliminar inmediatamente el alojamiento", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Eliminar mi alojamiento SQL Privado", + "billing_services_actions_menu_resiliate_WEBCOACH": "Eliminar mi WebCoach", + "billing_services_actions_menu_sms_credit": "Añadir crédito", + "billing_services_actions_menu_sms_renew": "Configurar la recarga automática", + "billing_services_actions_menu_resiliate_cancel": "Cancelar la baja del servicio", + "billing_services_actions_menu_see_dashboard": "Ver el detalle del servicio", + "billing_services_actions_menu_commit": "Gestionar mi compromiso de permanencia", + "billing_services_actions_menu_commit_cancel": "Cancelar la solicitud de contratación con compromiso de permanencia" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_CA.json new file mode 100644 index 000000000000..219001192e19 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_CA.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Plus d'actions sur ce service", + "billing_autorenew_service_enable_autorenew": "Activer le paiement automatique", + "billing_services_actions_menu_pay_bill": "Régler ma facture", + "billing_services_actions_menu_manage_renew": "Configurer le renouvellement", + "billing_services_actions_menu_exchange_update_accounts": "Configurer le renouvellement des comptes", + "billing_services_actions_menu_anticipate_renew": "Anticiper le paiement", + "billing_services_actions_menu_resiliate": "Résilier", + "billing_services_actions_menu_resiliate_my_engagement": "Résilier mon engagement", + "billing_services_actions_menu_renew_label": "Renouveler le service : {{ serviceName }} (Nouvelle fenêtre)", + "billing_services_actions_menu_renew": "Renouveler le service", + "billing_services_actions_menu_exchange_update": "Modifier la facturation", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Supprimer immédiatement le MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Supprimer immédiatement l'enterprise cloud databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Supprimer immédiatement l'hébergement", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Supprimer mon hébergement SQL privé", + "billing_services_actions_menu_resiliate_WEBCOACH": "Supprimer mon WebCoach", + "billing_services_actions_menu_sms_credit": "Ajouter des crédits", + "billing_services_actions_menu_sms_renew": "Configurer la recharge automatique", + "billing_services_actions_menu_resiliate_cancel": "Annuler la résiliation du service", + "billing_services_actions_menu_see_dashboard": "Voir le détail du service", + "billing_services_actions_menu_commit": "Gérer mon engagement", + "billing_services_actions_menu_commit_cancel": "Annuler la demande d'engagement" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_FR.json new file mode 100644 index 000000000000..219001192e19 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_fr_FR.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Plus d'actions sur ce service", + "billing_autorenew_service_enable_autorenew": "Activer le paiement automatique", + "billing_services_actions_menu_pay_bill": "Régler ma facture", + "billing_services_actions_menu_manage_renew": "Configurer le renouvellement", + "billing_services_actions_menu_exchange_update_accounts": "Configurer le renouvellement des comptes", + "billing_services_actions_menu_anticipate_renew": "Anticiper le paiement", + "billing_services_actions_menu_resiliate": "Résilier", + "billing_services_actions_menu_resiliate_my_engagement": "Résilier mon engagement", + "billing_services_actions_menu_renew_label": "Renouveler le service : {{ serviceName }} (Nouvelle fenêtre)", + "billing_services_actions_menu_renew": "Renouveler le service", + "billing_services_actions_menu_exchange_update": "Modifier la facturation", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Supprimer immédiatement le MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Supprimer immédiatement l'enterprise cloud databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Supprimer immédiatement l'hébergement", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Supprimer mon hébergement SQL privé", + "billing_services_actions_menu_resiliate_WEBCOACH": "Supprimer mon WebCoach", + "billing_services_actions_menu_sms_credit": "Ajouter des crédits", + "billing_services_actions_menu_sms_renew": "Configurer la recharge automatique", + "billing_services_actions_menu_resiliate_cancel": "Annuler la résiliation du service", + "billing_services_actions_menu_see_dashboard": "Voir le détail du service", + "billing_services_actions_menu_commit": "Gérer mon engagement", + "billing_services_actions_menu_commit_cancel": "Annuler la demande d'engagement" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_it_IT.json new file mode 100644 index 000000000000..923942a47ea1 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_it_IT.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Più azioni per questo servizio", + "billing_autorenew_service_enable_autorenew": "Attiva il pagamento automatico", + "billing_services_actions_menu_pay_bill": "Paga la tua fattura", + "billing_services_actions_menu_manage_renew": "Configura il rinnovo", + "billing_services_actions_menu_exchange_update_accounts": "Configura il rinnovo degli account", + "billing_services_actions_menu_anticipate_renew": "Anticipa il pagamento", + "billing_services_actions_menu_resiliate": "Rescindi l’impegno contrattuale", + "billing_services_actions_menu_resiliate_my_engagement": "Rescindi l’impegno contrattuale", + "billing_services_actions_menu_renew_label": "Rinnova il servizio: {{ serviceName }} (Nuova finestra)", + "billing_services_actions_menu_renew": "Rinnova il servizio", + "billing_services_actions_menu_exchange_update": "Modifica la fatturazione", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Elimina subito MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Elimina subito Enterprise Cloud Databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Elimina subito l'hosting", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Elimina l'hosting SQL Privato", + "billing_services_actions_menu_resiliate_WEBCOACH": "Elimina il tuo WebCoach", + "billing_services_actions_menu_sms_credit": "Aggiungi credito", + "billing_services_actions_menu_sms_renew": "Imposta la ricarica automatica", + "billing_services_actions_menu_resiliate_cancel": "Annulla la disattivazione del servizio", + "billing_services_actions_menu_see_dashboard": "Mostra i dettagli del server", + "billing_services_actions_menu_commit": "Gestisci il tuo impegno contrattuale", + "billing_services_actions_menu_commit_cancel": "Annulla la richiesta di sottoscrizione di un impegno contrattuale" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pl_PL.json new file mode 100644 index 000000000000..c030874fcf1f --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pl_PL.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Więcej operacji dla tej usługi", + "billing_autorenew_service_enable_autorenew": "Włącz płatność automatyczną", + "billing_services_actions_menu_pay_bill": "Ureguluj należności", + "billing_services_actions_menu_manage_renew": "Skonfiguruj odnowienie", + "billing_services_actions_menu_exchange_update_accounts": "Skonfiguruj odnowienie kont", + "billing_services_actions_menu_anticipate_renew": "Prognoza płatności", + "billing_services_actions_menu_resiliate": "Rezygnacja z umowy terminowej", + "billing_services_actions_menu_resiliate_my_engagement": "Rezygnacja z umowy terminowej", + "billing_services_actions_menu_renew_label": "Odnowienie usługi: {{serviceName}} (Nowe okno)", + "billing_services_actions_menu_renew": "Odnów usługę", + "billing_services_actions_menu_exchange_update": "Zmień płatności", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Usuń natychmiast MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Usuń natychmiast Enterprise Cloud Databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Usuń natychmiast hosting", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Usuń serwer Private SQL", + "billing_services_actions_menu_resiliate_WEBCOACH": "Usuń moje narzędzie WebCoach", + "billing_services_actions_menu_sms_credit": "Zasil konto", + "billing_services_actions_menu_sms_renew": "Skonfiguruj automatyczne doładowanie", + "billing_services_actions_menu_resiliate_cancel": "Anuluj rezygnację z usługi", + "billing_services_actions_menu_see_dashboard": "Wyświetl szczegółowe informacje o usłudze", + "billing_services_actions_menu_commit": "Zarządzanie usługą z opcją umowy terminowej", + "billing_services_actions_menu_commit_cancel": "Anuluj zamówienie usługi z opcją umowy terminowej" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pt_PT.json new file mode 100644 index 000000000000..8d0d71c6667c --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/actions/Messages_pt_PT.json @@ -0,0 +1,24 @@ +{ + "billing_services_actions_menu_label": "Mais ações neste serviço", + "billing_autorenew_service_enable_autorenew": "Ativar pagamento automático", + "billing_services_actions_menu_pay_bill": "Pagar a minha fatura", + "billing_services_actions_menu_manage_renew": "Configurar a renovação", + "billing_services_actions_menu_exchange_update_accounts": "Configurar a renovação das contas", + "billing_services_actions_menu_anticipate_renew": "Antecipar o pagamento", + "billing_services_actions_menu_resiliate": "Rescindir o meu compromisso", + "billing_services_actions_menu_resiliate_my_engagement": "Rescindir o meu compromisso", + "billing_services_actions_menu_renew_label": "Renovar o serviço: {{ serviceName }} (nova janela)", + "billing_services_actions_menu_renew": "Renovar o serviço", + "billing_services_actions_menu_exchange_update": "Alterar a faturação", + "billing_services_actions_menu_resiliate_EMAIL_DOMAIN": "Eliminar imediatamente o MX Plan", + "billing_services_actions_menu_resiliate_ENTERPRISE_CLOUD_DATABASE": "Eliminar imediatamente o Enterprise Cloud Databases", + "billing_services_actions_menu_resiliate_HOSTING_WEB": "Eliminar imediatamente o alojamento", + "billing_services_actions_menu_resiliate_HOSTING_PRIVATE_DATABASE": "Eliminar o meu alojamento SQL Privado", + "billing_services_actions_menu_resiliate_WEBCOACH": "Eliminar o meu WebCoach", + "billing_services_actions_menu_sms_credit": "Adicionar créditos", + "billing_services_actions_menu_sms_renew": "Configurar a recarga automática", + "billing_services_actions_menu_resiliate_cancel": "Anular a rescisão do serviço", + "billing_services_actions_menu_see_dashboard": "Ver os detalhes do serviço", + "billing_services_actions_menu_commit": "Gerir o meu compromisso", + "billing_services_actions_menu_commit_cancel": "Anular o pedido de compromisso" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_de_DE.json new file mode 100644 index 000000000000..48d986147d75 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_de_DE.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Status", + "manager_billing_service_status_auto": "Automatische Verlängerung", + "manager_billing_service_status_automatic": "Automatische Verlängerung", + "manager_billing_service_status_manual": "Manuelle Verlängerung", + "manager_billing_service_status_manualPayment": "Manuelle Verlängerung", + "manager_billing_service_status_pending_debt": "Ausstehende Rechnung", + "manager_billing_service_status_delete_at_expiration": "Kündigung angefordert", + "manager_billing_service_status_expired": "Gekündigt", + "manager_billing_service_status_billing_suspended": "Rechnungsstellung verschoben", + "manager_billing_service_status_forced_manual": "Manuelle Verlängerung" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_en_GB.json new file mode 100644 index 000000000000..6ae6fbe0a3ce --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_en_GB.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Status", + "manager_billing_service_status_auto": "Automatic renewal", + "manager_billing_service_status_automatic": "Automatic renewal", + "manager_billing_service_status_manual": "Manual renewal", + "manager_billing_service_status_manualPayment": "Manual renewal", + "manager_billing_service_status_pending_debt": "Bill to pay", + "manager_billing_service_status_delete_at_expiration": "Cancellation requested", + "manager_billing_service_status_expired": "Cancelled", + "manager_billing_service_status_billing_suspended": "Deferred billing", + "manager_billing_service_status_forced_manual": "Manual renewal" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_es_ES.json new file mode 100644 index 000000000000..08ca797d3286 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_es_ES.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Estado", + "manager_billing_service_status_auto": "Renovación automática", + "manager_billing_service_status_automatic": "Renovación automática", + "manager_billing_service_status_manual": "Renovación manual", + "manager_billing_service_status_manualPayment": "Renovación manual", + "manager_billing_service_status_pending_debt": "Factura pendiente", + "manager_billing_service_status_delete_at_expiration": "Baja solicitada", + "manager_billing_service_status_expired": "Dado de baja", + "manager_billing_service_status_billing_suspended": "Facturación aplazada", + "manager_billing_service_status_forced_manual": "Renovación manual" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_CA.json new file mode 100644 index 000000000000..9a411d802751 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_CA.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Statut", + "manager_billing_service_status_auto": "Renouvellement automatique", + "manager_billing_service_status_automatic": "Renouvellement automatique", + "manager_billing_service_status_manual": "Renouvellement manuel", + "manager_billing_service_status_manualPayment": "Renouvellement manuel", + "manager_billing_service_status_pending_debt": "Facture à régler", + "manager_billing_service_status_delete_at_expiration": "Résiliation demandée", + "manager_billing_service_status_expired": "Résilié", + "manager_billing_service_status_billing_suspended": "Facturation reportée", + "manager_billing_service_status_forced_manual": "Renouvellement manuel forcé" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_FR.json new file mode 100644 index 000000000000..9a411d802751 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_fr_FR.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Statut", + "manager_billing_service_status_auto": "Renouvellement automatique", + "manager_billing_service_status_automatic": "Renouvellement automatique", + "manager_billing_service_status_manual": "Renouvellement manuel", + "manager_billing_service_status_manualPayment": "Renouvellement manuel", + "manager_billing_service_status_pending_debt": "Facture à régler", + "manager_billing_service_status_delete_at_expiration": "Résiliation demandée", + "manager_billing_service_status_expired": "Résilié", + "manager_billing_service_status_billing_suspended": "Facturation reportée", + "manager_billing_service_status_forced_manual": "Renouvellement manuel forcé" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_it_IT.json new file mode 100644 index 000000000000..fba42c7f59e7 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_it_IT.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Stato", + "manager_billing_service_status_auto": "Rinnovo automatico", + "manager_billing_service_status_automatic": "Rinnovo automatico", + "manager_billing_service_status_manual": "Rinnovo manuale", + "manager_billing_service_status_manualPayment": "Rinnovo manuale", + "manager_billing_service_status_pending_debt": "Fatture da pagare", + "manager_billing_service_status_delete_at_expiration": "Disattivazione richiesta", + "manager_billing_service_status_expired": "Disattivato", + "manager_billing_service_status_billing_suspended": "Fatturazione rinviata", + "manager_billing_service_status_forced_manual": "Rinnovo manuale" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pl_PL.json new file mode 100644 index 000000000000..8e49f10ef2c5 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pl_PL.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Status", + "manager_billing_service_status_auto": "Automatyczne odnowienie", + "manager_billing_service_status_automatic": "Automatyczne odnowienie", + "manager_billing_service_status_manual": "Odnowienie ręczne", + "manager_billing_service_status_manualPayment": "Odnowienie ręczne", + "manager_billing_service_status_pending_debt": "Płatność do uregulowania", + "manager_billing_service_status_delete_at_expiration": "Złożona rezygnacja", + "manager_billing_service_status_expired": "Anulowany", + "manager_billing_service_status_billing_suspended": "Faktura odroczona", + "manager_billing_service_status_forced_manual": "Odnowienie ręczne" +} diff --git a/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pt_PT.json new file mode 100644 index 000000000000..71abe76bc2d2 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/billing/status/Messages_pt_PT.json @@ -0,0 +1,12 @@ +{ + "manager_billing_service_status": "Estado", + "manager_billing_service_status_auto": "Renovação automática", + "manager_billing_service_status_automatic": "Renovação automática", + "manager_billing_service_status_manual": "Renovação manual", + "manager_billing_service_status_manualPayment": "Renovação manual", + "manager_billing_service_status_pending_debt": "Fatura por pagar", + "manager_billing_service_status_delete_at_expiration": "Rescisão solicitada", + "manager_billing_service_status_expired": "Rescindido", + "manager_billing_service_status_billing_suspended": "Faturação diferida", + "manager_billing_service_status_forced_manual": "Renovação manual" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_de_DE.json new file mode 100644 index 000000000000..2bd68b18a76e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_de_DE.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Herzlich Willkommen, {{ name }} !", + "ovh_trusted_nic_label": "Vertrauenswürdige Zone", + "manager_hub_skip_to_main_content": "Zum Hauptinhalt", + "manager_hub_dashboard_overview": "Überblick über meine Aktivitäten", + "hub_support_see_more": "Alles anzeigen" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_en_GB.json new file mode 100644 index 000000000000..4dab68e2cf87 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_en_GB.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Welcome {{ name }}!", + "ovh_trusted_nic_label": "Trusted Zone", + "manager_hub_skip_to_main_content": "Skip to main content", + "manager_hub_dashboard_overview": "View all my activity", + "hub_support_see_more": "See all" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_es_ES.json new file mode 100644 index 000000000000..3452abb86aa8 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_es_ES.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "¡Bienvenido/a, {{ name }}!", + "ovh_trusted_nic_label": "Trusted Zone", + "manager_hub_skip_to_main_content": "Ir al contenido principal", + "manager_hub_dashboard_overview": "Vista general de mi actividad", + "hub_support_see_more": "Ver todo" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_CA.json new file mode 100644 index 000000000000..316d8a8ada5e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_CA.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Bienvenue {{ name }} !", + "ovh_trusted_nic_label": "Zone de confiance", + "manager_hub_skip_to_main_content": "Aller au contenu principal", + "manager_hub_dashboard_overview": "Vue d'ensemble de mon activité", + "hub_support_see_more": "Voir tout" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_FR.json new file mode 100644 index 000000000000..316d8a8ada5e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_fr_FR.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Bienvenue {{ name }} !", + "ovh_trusted_nic_label": "Zone de confiance", + "manager_hub_skip_to_main_content": "Aller au contenu principal", + "manager_hub_dashboard_overview": "Vue d'ensemble de mon activité", + "hub_support_see_more": "Voir tout" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_it_IT.json new file mode 100644 index 000000000000..0cca7b398aab --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_it_IT.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Benvenuto {{ name }}!", + "ovh_trusted_nic_label": "Trusted Zone", + "manager_hub_skip_to_main_content": "Vai al contenuto principale", + "manager_hub_dashboard_overview": "Visione d’insieme della tua attività", + "hub_support_see_more": "Visualizza tutto" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_pl_PL.json new file mode 100644 index 000000000000..ed513cde1585 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_pl_PL.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Witamy!", + "ovh_trusted_nic_label": "Trusted Zone", + "manager_hub_skip_to_main_content": "Przejdź do głównej sekcji", + "manager_hub_dashboard_overview": "Przegląd moich projektów", + "hub_support_see_more": "Zobacz wszystko" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/Messages_pt_PT.json new file mode 100644 index 000000000000..6942db735861 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/Messages_pt_PT.json @@ -0,0 +1,7 @@ +{ + "manager_hub_dashboard_welcome": "Bem-vindo/a {{ name }}!", + "ovh_trusted_nic_label": "Trusted Zone", + "manager_hub_skip_to_main_content": "Aceder ao conteúdo principal", + "manager_hub_dashboard_overview": "Visão geral da minha atividade", + "hub_support_see_more": "Ver tudo" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_de_DE.json new file mode 100644 index 000000000000..d22807ec160e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_de_DE.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Gesamtbetrag der Rechnungen", + "hub_billing_summary_period_1": "Im letzten Monat", + "hub_billing_summary_period_3": "In den letzten 3 Monaten", + "hub_billing_summary_period_6": "In den letzten 6 Monaten", + "hub_billing_summary_debt": "Es sind noch {{ debt }} zu begleichen.", + "hub_billing_summary_debt_pay": "Ausstehende Zahlungen tätigen", + "hub_billing_summary_debt_null": "Bezahlt", + "hub_billing_summary_debt_no_bills": "Für diesen Zeitraum existiert keine Rechnung.", + "hub_billing_summary_display_bills": "Die Rechnungen anzeigen", + "hub_billing_summary_display_bills_error": "Gesamtbetrag nicht verfügbar", + "hub_enterprise_billing_summary_title": "Meine Rechnungen", + "hub_enterprise_billing_summary_description": "Um Ihre Rechnungen einzusehen, verbinden Sie sich bitte über Ihr dediziertes Interface.", + "hub_enterprise_billing_summary_goto": "Anmelden" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_en_GB.json new file mode 100644 index 000000000000..10c19e0e6044 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_en_GB.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Total amount of bills", + "hub_billing_summary_period_1": "The last month ", + "hub_billing_summary_period_3": "The last 3 months", + "hub_billing_summary_period_6": "The last 6 months", + "hub_billing_summary_debt": "{{ debt }} pending payment", + "hub_billing_summary_debt_pay": "Settle outstanding amount", + "hub_billing_summary_debt_null": "Paid", + "hub_billing_summary_debt_no_bills": "No bill for this period", + "hub_billing_summary_display_bills": "View bills", + "hub_billing_summary_display_bills_error": "Unable to retrieve total amount", + "hub_enterprise_billing_summary_title": "My bills", + "hub_enterprise_billing_summary_description": "To consult your invoices, please connect to your dedicated interface.", + "hub_enterprise_billing_summary_goto": "Log in" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_es_ES.json new file mode 100644 index 000000000000..65ea87bccc25 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_es_ES.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Importe total de las facturas", + "hub_billing_summary_period_1": "El mes pasado", + "hub_billing_summary_period_3": "Los últimos 3 meses", + "hub_billing_summary_period_6": "Los últimos 6 meses", + "hub_billing_summary_debt": "Importe pendiente: {{ debt }}", + "hub_billing_summary_debt_pay": "Abonar mi deuda", + "hub_billing_summary_debt_null": "Pagado", + "hub_billing_summary_debt_no_bills": "No hay facturas para el período seleccionado.", + "hub_billing_summary_display_bills": "Ver las facturas", + "hub_billing_summary_display_bills_error": "No es posible cargar el importe total.", + "hub_enterprise_billing_summary_title": "Mis facturas", + "hub_enterprise_billing_summary_description": "Conéctese a la interfaz dedicada para consultar sus facturas.", + "hub_enterprise_billing_summary_goto": "Conectarme" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_CA.json new file mode 100644 index 000000000000..e531088a3246 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_CA.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Montant total des factures", + "hub_billing_summary_period_1": "Le mois dernier", + "hub_billing_summary_period_3": "Les 3 derniers mois", + "hub_billing_summary_period_6": "Les 6 derniers mois", + "hub_billing_summary_debt": "Il vous reste {{ debt }} à régler", + "hub_billing_summary_debt_pay": "Solder ma dette", + "hub_billing_summary_debt_null": "Payé", + "hub_billing_summary_debt_no_bills": "Pas de facture pour cette période", + "hub_billing_summary_display_bills": "Voir les factures", + "hub_billing_summary_display_bills_error": "Impossible de récupérer le montant total", + "hub_enterprise_billing_summary_title": "Mes factures", + "hub_enterprise_billing_summary_description": "Pour consulter vos factures, merci de vous connecter sur votre interface dédiée", + "hub_enterprise_billing_summary_goto": "Se connecter" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_FR.json new file mode 100644 index 000000000000..e531088a3246 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_fr_FR.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Montant total des factures", + "hub_billing_summary_period_1": "Le mois dernier", + "hub_billing_summary_period_3": "Les 3 derniers mois", + "hub_billing_summary_period_6": "Les 6 derniers mois", + "hub_billing_summary_debt": "Il vous reste {{ debt }} à régler", + "hub_billing_summary_debt_pay": "Solder ma dette", + "hub_billing_summary_debt_null": "Payé", + "hub_billing_summary_debt_no_bills": "Pas de facture pour cette période", + "hub_billing_summary_display_bills": "Voir les factures", + "hub_billing_summary_display_bills_error": "Impossible de récupérer le montant total", + "hub_enterprise_billing_summary_title": "Mes factures", + "hub_enterprise_billing_summary_description": "Pour consulter vos factures, merci de vous connecter sur votre interface dédiée", + "hub_enterprise_billing_summary_goto": "Se connecter" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_it_IT.json new file mode 100644 index 000000000000..b10cb974a851 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_it_IT.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Importo totale delle fatture", + "hub_billing_summary_period_1": "Mese scorso", + "hub_billing_summary_period_3": "Ultimi 3 mesi", + "hub_billing_summary_period_6": "Ultimi 6 mesi", + "hub_billing_summary_debt": "Restano ancora {{ debt }} da pagare", + "hub_billing_summary_debt_pay": "Salda il debito", + "hub_billing_summary_debt_null": "Pagato", + "hub_billing_summary_debt_no_bills": "Nessuna fattura per questo periodo", + "hub_billing_summary_display_bills": "Visualizza le fatture", + "hub_billing_summary_display_bills_error": "Impossibile recuperare l’importo totale", + "hub_enterprise_billing_summary_title": "Le tue fatture", + "hub_enterprise_billing_summary_description": "Per consultare le tue fatture, accedi all’interfaccia dedicata.", + "hub_enterprise_billing_summary_goto": "Accedi" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pl_PL.json new file mode 100644 index 000000000000..75fb81705956 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pl_PL.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Łączna kwota faktur", + "hub_billing_summary_period_1": "Ostatni miesiąc", + "hub_billing_summary_period_3": "3 ostatnie miesiące", + "hub_billing_summary_period_6": "6 ostatnich miesięcy", + "hub_billing_summary_debt": "Pozostało do zapłaty: {{debt}} ", + "hub_billing_summary_debt_pay": "Ureguluj należności", + "hub_billing_summary_debt_null": "Opłacone", + "hub_billing_summary_debt_no_bills": "Brak faktur za ten okres", + "hub_billing_summary_display_bills": "Wyświetl faktury", + "hub_billing_summary_display_bills_error": "Nie można pobrać łącznej kwoty", + "hub_enterprise_billing_summary_title": "Faktury", + "hub_enterprise_billing_summary_description": "Aby sprawdzić faktury, zaloguj się do Panelu klienta", + "hub_enterprise_billing_summary_goto": "Zaloguj się" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pt_PT.json new file mode 100644 index 000000000000..7c1168dd7b19 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/billing/Messages_pt_PT.json @@ -0,0 +1,15 @@ +{ + "hub_billing_summary_title": "Montante total das faturas", + "hub_billing_summary_period_1": "Mês passado", + "hub_billing_summary_period_3": "Últimos 3 meses", + "hub_billing_summary_period_6": "Últimos 6 meses", + "hub_billing_summary_debt": "Montante de {{ debt }} por pagar", + "hub_billing_summary_debt_pay": "Saldar dívida", + "hub_billing_summary_debt_null": "Pago", + "hub_billing_summary_debt_no_bills": "Nenhuma fatura para este período", + "hub_billing_summary_display_bills": "Ver faturas", + "hub_billing_summary_display_bills_error": "Não é possível recuperar o montante total", + "hub_enterprise_billing_summary_title": "Faturas", + "hub_enterprise_billing_summary_description": "Para consultar as suas faturas, aceda à sua interface dedicada", + "hub_enterprise_billing_summary_goto": "Iniciar sessão" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_de_DE.json new file mode 100644 index 000000000000..684af6318eab --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_de_DE.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Bestellen", + "manager_hub_catalog_title": "Unsere Produkte", + "manager_hub_catalog_description": "Beginnen Sie Ihr OVHcloud Abenteuer und bestellen Sie Ihr erstes Produkt." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_en_GB.json new file mode 100644 index 000000000000..6450a210cee1 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_en_GB.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Order", + "manager_hub_catalog_title": "Explore our products", + "manager_hub_catalog_description": "Start the OVHcloud adventure and order your first product" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_es_ES.json new file mode 100644 index 000000000000..62ad6daf8c91 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_es_ES.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Contratar", + "manager_hub_catalog_title": "Descubra nuestros productos", + "manager_hub_catalog_description": "¡Comience la aventura OVHcloud y contrate su primer producto!" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_CA.json new file mode 100644 index 000000000000..2b3a9c1cbc98 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_CA.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_title": "Découvrez nos produits", + "manager_hub_catalog_description": "Démarrez l'aventure OVHcloud et commandez votre premier produit", + "manager_hub_catalog_items_order": "Commander" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_FR.json new file mode 100644 index 000000000000..2b3a9c1cbc98 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_fr_FR.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_title": "Découvrez nos produits", + "manager_hub_catalog_description": "Démarrez l'aventure OVHcloud et commandez votre premier produit", + "manager_hub_catalog_items_order": "Commander" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_it_IT.json new file mode 100644 index 000000000000..f838462f2266 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_it_IT.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Ordina", + "manager_hub_catalog_title": "Scopri i nostri prodotti", + "manager_hub_catalog_description": "Inizia l’avventura OVHcloud ordinando il tuo primo servizio!" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pl_PL.json new file mode 100644 index 000000000000..9f144947aff3 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pl_PL.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Zamów", + "manager_hub_catalog_title": "Poznaj nasze produkty", + "manager_hub_catalog_description": "Rozpocznij przygodę z OVHcloud i zamów pierwszy produkt" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pt_PT.json new file mode 100644 index 000000000000..eb38e5da696a --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/catalog/Messages_pt_PT.json @@ -0,0 +1,5 @@ +{ + "manager_hub_catalog_items_order": "Encomendar", + "manager_hub_catalog_title": "Descubra os nossos produtos", + "manager_hub_catalog_description": "Comece a aventura OVHcloud e encomende o seu primeiro produto" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_de_DE.json new file mode 100644 index 000000000000..7346d97c5de8 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_de_DE.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Hoppla!", + "manager_hub_error_tile_retry": "Neu laden" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_en_GB.json new file mode 100644 index 000000000000..4fe697a0f73b --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_en_GB.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Oops!", + "manager_hub_error_tile_retry": "Reload" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_es_ES.json new file mode 100644 index 000000000000..1d7954fe3174 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_es_ES.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "¡Vaya!", + "manager_hub_error_tile_retry": "Actualizar" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_CA.json new file mode 100644 index 000000000000..e007944903b5 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_CA.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Oups ...!", + "manager_hub_error_tile_retry": "Recharger" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_FR.json new file mode 100644 index 000000000000..26f93b60a2ff --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_fr_FR.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Oops …!", + "manager_hub_error_tile_retry": "Recharger" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_it_IT.json new file mode 100644 index 000000000000..0a9019a5e886 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_it_IT.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Ops!", + "manager_hub_error_tile_retry": "Ricarica" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pl_PL.json new file mode 100644 index 000000000000..7166a69ea81e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pl_PL.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Ojej!", + "manager_hub_error_tile_retry": "Przeładuj" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pt_PT.json new file mode 100644 index 000000000000..743ec9cb23b8 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/error/Messages_pt_PT.json @@ -0,0 +1,4 @@ +{ + "manager_hub_error_tile_oops": "Ups!", + "manager_hub_error_tile_retry": "Atualizar" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_de_DE.json new file mode 100644 index 000000000000..229012146da3 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_de_DE.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Sie möchten ein OVHcloud Produkt bestellen: Aufgrund der geltenden Vorschriften muss Ihre Identität überprüft werden. Bitte fügen Sie Ihre Ausweisdokumente hinzu.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Um Ihre Identität zu überprüfen, bevor Sie eine Bestellung aufgeben, lesen Sie bitte die E-Mail unseres Teams und führen Sie die angegebenen Schritte aus.", + "manager_hub_dashboard_kyc_banner_link": "Meine Dokumente", + "kyc_fraud_required_banner_text": "Es ist eine Identitätsüberprüfung erforderlich. Fügen Sie Ihre Belge hinzu, um die laufende Bestellung abzuschließen.", + "kyc_fraud_required_banner_link": "Meine Dokumente", + "kyc_fraud_open_banner_text": "Die Verarbeitung Ihrer Dokumente wird derzeit von unserem Team analysiert. Weitere Informationen finden Sie in", + "kyc_fraud_open_banner_link": "Meine Support-Anfragen." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_en_GB.json new file mode 100644 index 000000000000..91594274178b --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_en_GB.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Before you order an OVHcloud product, current regulations require verification of your identity. Please add your identity documents.", + "manager_hub_dashboard_kyc_banner_description_waiting": "To place an order, please read the email you’ve received from our team and follow the instructions to verify your identity.", + "manager_hub_dashboard_kyc_banner_link": "My documents", + "kyc_fraud_required_banner_text": "You need to verify your identity. To complete your current order, please add your supporting documents.", + "kyc_fraud_required_banner_link": "My documents", + "kyc_fraud_open_banner_text": "Your documents are currently being processed by our team. For more information, please refer to", + "kyc_fraud_open_banner_link": "My support tickets" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_es_ES.json new file mode 100644 index 000000000000..8fdbf84364b7 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_es_ES.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Según la normativa vigente, es necesario verificar su identidad para contratar los servicios de OVHcloud. Por favor, añada sus documentos de identidad.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Por favor, lea el mensaje de correo electrónico que le hemos enviado y realice la acción solicitada para verificar su identidad antes de realizar un pedido.", + "manager_hub_dashboard_kyc_banner_link": "Mis documentos", + "kyc_fraud_required_banner_text": "Es necesario comprobar su identidad. Para finalizar el pedido en curso, añada los documentos justificantes.", + "kyc_fraud_required_banner_link": "Mis documentos", + "kyc_fraud_open_banner_text": "Nuestro equipo está analizando el tratamiento de sus documentos. Para más información, puede consultar", + "kyc_fraud_open_banner_link": "Mis solicitudes de asistencia" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_CA.json new file mode 100644 index 000000000000..1af6ff932877 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_CA.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Vous souhaitez commander un produit OVHcloud : la réglementation en vigueur nécessite une vérification de votre identité. Veuillez ajouter vos documents d'identité.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Pour la vérification de votre identité avant de réaliser une commande, veuillez prendre connaissance de l'email envoyé par notre équipe et procéder à l'action demandée.", + "manager_hub_dashboard_kyc_banner_link": "Mes documents", + "kyc_fraud_required_banner_text": "Une vérification de votre identité est nécessaire. Afin de finaliser votre commande en cours, veuillez ajouter vos justificatifs.", + "kyc_fraud_required_banner_link": "Mes documents", + "kyc_fraud_open_banner_text": "Le traitement de vos documents est actuellement en cours d'analyse par notre équipe. Pour plus d'information, vous pouvez consulter", + "kyc_fraud_open_banner_link": "Mes demandes d'assistance." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_FR.json new file mode 100644 index 000000000000..1af6ff932877 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_fr_FR.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Vous souhaitez commander un produit OVHcloud : la réglementation en vigueur nécessite une vérification de votre identité. Veuillez ajouter vos documents d'identité.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Pour la vérification de votre identité avant de réaliser une commande, veuillez prendre connaissance de l'email envoyé par notre équipe et procéder à l'action demandée.", + "manager_hub_dashboard_kyc_banner_link": "Mes documents", + "kyc_fraud_required_banner_text": "Une vérification de votre identité est nécessaire. Afin de finaliser votre commande en cours, veuillez ajouter vos justificatifs.", + "kyc_fraud_required_banner_link": "Mes documents", + "kyc_fraud_open_banner_text": "Le traitement de vos documents est actuellement en cours d'analyse par notre équipe. Pour plus d'information, vous pouvez consulter", + "kyc_fraud_open_banner_link": "Mes demandes d'assistance." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_it_IT.json new file mode 100644 index 000000000000..ab7c5ff5128c --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_it_IT.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Vuoi ordinare un servizio OVHcloud: la normativa in vigore richiede una verifica dell'identità. Aggiungi i tuoi documenti d'identità.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Per verificare la tua identità prima di effettuare un ordine, leggi l'email inviata dal nostro team e procedi con l'azione richiesta.", + "manager_hub_dashboard_kyc_banner_link": "I miei documenti", + "kyc_fraud_required_banner_text": "È necessario verificare la tua identità. Per completare l'ordine in corso, aggiungi i documenti giustificativi.", + "kyc_fraud_required_banner_link": "I miei documenti", + "kyc_fraud_open_banner_text": "Il trattamento dei documenti è attualmente in fase di analisi da parte dei nostri team. Per maggiori informazioni, consulta", + "kyc_fraud_open_banner_link": "Le mie richieste di assistenza" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pl_PL.json new file mode 100644 index 000000000000..affa300b8d27 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pl_PL.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Chcesz zamówić produkt OVHcloud: obowiązujące przepisy wymagają sprawdzenia Twojej tożsamości. Dodaj dokumenty tożsamości.", + "manager_hub_dashboard_kyc_banner_description_waiting": "W celu potwierdzenia Twojej tożsamości, przed złożeniem zamówienia zapoznaj się z e-mailem wysłanym przez nasz zespół i podejmij wymagane działania.", + "manager_hub_dashboard_kyc_banner_link": "Moje dokumenty", + "kyc_fraud_required_banner_text": "Konieczne jest potwierdzenie Twojej tożsamości. Aby sfinalizować bieżące zamówienie, dodaj dokumenty potwierdzające tożsamość.", + "kyc_fraud_required_banner_link": "Moje dokumenty", + "kyc_fraud_open_banner_text": "Twoje dokumenty są obecnie przetwarzane przez nasz zespół. Aby uzyskać więcej informacji, sprawdź", + "kyc_fraud_open_banner_link": "Moje zgłoszenia" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pt_PT.json new file mode 100644 index 000000000000..1063fd7ac527 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/kyc/Messages_pt_PT.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_kyc_banner_description": "Deseja encomendar um produto OVHcloud: a regulamentação em vigor requer uma verificação da sua identidade. Adicione os seus documentos de identificação.", + "manager_hub_dashboard_kyc_banner_description_waiting": "Para a verificação da sua identidade antes de realizar uma encomenda, consulte por favor o e-mail enviado pela nossa equipa e proceda à ação solicitada.", + "manager_hub_dashboard_kyc_banner_link": "Os meus documentos", + "kyc_fraud_required_banner_text": "É necessária uma verificação da sua identidade. Para finalizar a encomenda em curso, adicione os comprovativos.", + "kyc_fraud_required_banner_link": "Os meus documentos", + "kyc_fraud_open_banner_text": "O tratamento dos seus documentos está a ser analisado pela nossa equipa. Para mais informações, pode consultar", + "kyc_fraud_open_banner_link": "Os meus pedidos de assistência" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_de_DE.json new file mode 100644 index 000000000000..9390c85b6ec9 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_de_DE.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Letzte Bestellung", + "hub_order_tracking_see_all": "Meine Bestellungen anzeigen", + "hub_order_tracking_error": "Nachverfolgung der Bestellung nicht verfügbar", + "hub_order_tracking_order_id": "Nr. {{orderId}}", + "order_tracking_history_DELIVERY": "Vorbereitung Ihrer Bestellung", + "order_tracking_history_FRAUD_CHECK": "Bestellung wird bestätigt...", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "Warten auf Belege", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Überprüfen Ihrer Bestellung", + "order_tracking_history_FRAUD_REFUSED": "Bestellung abgelehnt", + "order_tracking_history_INVOICE_IN_PROGRESS": "Ihre Bestellung ist verfügbar", + "order_tracking_history_INVOICE_SENT": "Ihre Rechnung ist verfügbar", + "order_tracking_history_ORDER_ACCEPTED": "Bestellung angenommen", + "order_tracking_history_ORDER_STARTED": "Ihre Bestellung wird erstellt...", + "order_tracking_history_PAYMENT_CONFIRMED": "Zahlung bestätigt", + "order_tracking_history_PAYMENT_INITIATED": "Zahlung wird bestätigt", + "order_tracking_history_PAYMENT_RECEIVED": "Zahlung erhalten", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Zahlung wird bestätigt", + "order_tracking_history_custom_payment_waiting": "Unbezahlt", + "order_tracking_history_custom_creation": "Erstellung Ihrer Bestellung" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_en_GB.json new file mode 100644 index 000000000000..780f12a8912a --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_en_GB.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Most recent order", + "hub_order_tracking_see_all": "View my orders", + "hub_order_tracking_error": "Unable to retrieve order tracking", + "hub_order_tracking_order_id": "No. {{orderId}}", + "order_tracking_history_DELIVERY": "Preparing your order", + "order_tracking_history_FRAUD_CHECK": "Your order is being confirmed", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "Awaiting supporting documents ", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Checking your order", + "order_tracking_history_FRAUD_REFUSED": "Order declined", + "order_tracking_history_INVOICE_IN_PROGRESS": "Your order is available", + "order_tracking_history_INVOICE_SENT": "Your bill is available", + "order_tracking_history_ORDER_ACCEPTED": "Order accepted", + "order_tracking_history_ORDER_STARTED": "Your order is being processed", + "order_tracking_history_PAYMENT_CONFIRMED": "Payment validated", + "order_tracking_history_PAYMENT_INITIATED": "Your payment is being validated", + "order_tracking_history_PAYMENT_RECEIVED": "Payment received", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Your payment is being validated", + "order_tracking_history_custom_payment_waiting": "Payment not received", + "order_tracking_history_custom_creation": "Create your order" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_es_ES.json new file mode 100644 index 000000000000..9db4d67cd288 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_es_ES.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Último pedido", + "hub_order_tracking_see_all": "Ver mis pedidos", + "hub_order_tracking_error": "No es posible cargar el seguimiento de pedidos.", + "hub_order_tracking_order_id": "N.º {{orderId}}", + "order_tracking_history_DELIVERY": "Preparación del pedido", + "order_tracking_history_FRAUD_CHECK": "Validando el pedido...", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "Pendiente de documentos justificantes", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Comprobación del pedido", + "order_tracking_history_FRAUD_REFUSED": "Pedido rechazado", + "order_tracking_history_INVOICE_IN_PROGRESS": "El pedido ya está disponible.", + "order_tracking_history_INVOICE_SENT": "La factura ya está disponible.", + "order_tracking_history_ORDER_ACCEPTED": "Pedido aceptado", + "order_tracking_history_ORDER_STARTED": "Registro del pedido", + "order_tracking_history_PAYMENT_CONFIRMED": "Pago validado", + "order_tracking_history_PAYMENT_INITIATED": "Validando el pago...", + "order_tracking_history_PAYMENT_RECEIVED": "Pago recibido", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Validando el pago...", + "order_tracking_history_custom_payment_waiting": "Pago no recibido", + "order_tracking_history_custom_creation": "Creación del pedido" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_CA.json new file mode 100644 index 000000000000..cd419fd51b16 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_CA.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Dernière commande", + "hub_order_tracking_see_all": "Voir mes commandes", + "hub_order_tracking_error": "Impossible de récupérer le suivi de commande", + "hub_order_tracking_order_id": "N° {{orderId}}", + "order_tracking_history_DELIVERY": "Préparation de votre commande", + "order_tracking_history_FRAUD_CHECK": "Commande en cours de validation", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "En attente de documents justificatifs", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Vérification de votre commande", + "order_tracking_history_FRAUD_REFUSED": "Commande refusée", + "order_tracking_history_INVOICE_IN_PROGRESS": "Votre commande est disponible", + "order_tracking_history_INVOICE_SENT": "Votre facture est disponible", + "order_tracking_history_ORDER_ACCEPTED": "Commande acceptée", + "order_tracking_history_ORDER_STARTED": "Prise en compte de votre commande", + "order_tracking_history_PAYMENT_CONFIRMED": "Paiement validé", + "order_tracking_history_PAYMENT_INITIATED": "Paiement en cours de validation", + "order_tracking_history_PAYMENT_RECEIVED": "Paiement reçu", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Paiement en cours de validation", + "order_tracking_history_custom_payment_waiting": "Paiement non reçu", + "order_tracking_history_custom_creation": "Création de votre commande" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_FR.json new file mode 100644 index 000000000000..cd419fd51b16 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_fr_FR.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Dernière commande", + "hub_order_tracking_see_all": "Voir mes commandes", + "hub_order_tracking_error": "Impossible de récupérer le suivi de commande", + "hub_order_tracking_order_id": "N° {{orderId}}", + "order_tracking_history_DELIVERY": "Préparation de votre commande", + "order_tracking_history_FRAUD_CHECK": "Commande en cours de validation", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "En attente de documents justificatifs", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Vérification de votre commande", + "order_tracking_history_FRAUD_REFUSED": "Commande refusée", + "order_tracking_history_INVOICE_IN_PROGRESS": "Votre commande est disponible", + "order_tracking_history_INVOICE_SENT": "Votre facture est disponible", + "order_tracking_history_ORDER_ACCEPTED": "Commande acceptée", + "order_tracking_history_ORDER_STARTED": "Prise en compte de votre commande", + "order_tracking_history_PAYMENT_CONFIRMED": "Paiement validé", + "order_tracking_history_PAYMENT_INITIATED": "Paiement en cours de validation", + "order_tracking_history_PAYMENT_RECEIVED": "Paiement reçu", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Paiement en cours de validation", + "order_tracking_history_custom_payment_waiting": "Paiement non reçu", + "order_tracking_history_custom_creation": "Création de votre commande" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_it_IT.json new file mode 100644 index 000000000000..7494690dfc8b --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_it_IT.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Ultimo ordine", + "hub_order_tracking_see_all": "Visualizza i tuoi ordini", + "hub_order_tracking_error": "Impossibile recuperare lo stato dell’ordine", + "hub_order_tracking_order_id": "N° {{orderId}}", + "order_tracking_history_DELIVERY": "Ordine in corso di preparazione", + "order_tracking_history_FRAUD_CHECK": "Ordine in corso di convalida", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "In attesa dei documenti giustificativi", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Verifica dell'ordine", + "order_tracking_history_FRAUD_REFUSED": "Ordine rifiutato", + "order_tracking_history_INVOICE_IN_PROGRESS": "Il tuo ordine è disponibile", + "order_tracking_history_INVOICE_SENT": "La tua fattura è disponibile", + "order_tracking_history_ORDER_ACCEPTED": "Ordine accettato", + "order_tracking_history_ORDER_STARTED": "Presa in carico dell’ordine...", + "order_tracking_history_PAYMENT_CONFIRMED": "Pagamento convalidato", + "order_tracking_history_PAYMENT_INITIATED": "Pagamento in corso di convalida", + "order_tracking_history_PAYMENT_RECEIVED": "Pagamento ricevuto", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Pagamento in corso di convalida", + "order_tracking_history_custom_payment_waiting": "Pagamento non ricevuto", + "order_tracking_history_custom_creation": "Creazione dell’ordine" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pl_PL.json new file mode 100644 index 000000000000..41f785c01c01 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pl_PL.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Ostatnie zamówienie ", + "hub_order_tracking_see_all": "Wyświetl moje zamówienia", + "hub_order_tracking_error": "Nie można pobrać informacji o zamówieniu", + "hub_order_tracking_order_id": "Nr {{orderId}}", + "order_tracking_history_DELIVERY": "Trwa przygotowywanie Twojego zamówienia.", + "order_tracking_history_FRAUD_CHECK": "Trwa potwierdzanie zamówienia.", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "W oczekiwaniu na dokumenty potwierdzające tożsamość", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Trwa weryfikacja Twojego zamówienia.", + "order_tracking_history_FRAUD_REFUSED": "Zamówienie zostało odrzucone.", + "order_tracking_history_INVOICE_IN_PROGRESS": "Twoje zamówienie jest dostępne.", + "order_tracking_history_INVOICE_SENT": "Faktura jest dostępna.", + "order_tracking_history_ORDER_ACCEPTED": "Zamówienie przyjęte", + "order_tracking_history_ORDER_STARTED": "Trwa zapisywanie Twojego zamówienia.", + "order_tracking_history_PAYMENT_CONFIRMED": "Płatność zatwierdzona.", + "order_tracking_history_PAYMENT_INITIATED": "Trwa potwierdzanie płatności.", + "order_tracking_history_PAYMENT_RECEIVED": "Płatność otrzymana", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Trwa potwierdzanie płatności.", + "order_tracking_history_custom_payment_waiting": "Brak płatności", + "order_tracking_history_custom_creation": "Trwa tworzenie zamówienia." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pt_PT.json new file mode 100644 index 000000000000..1c1ac8d5b154 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/order/Messages_pt_PT.json @@ -0,0 +1,21 @@ +{ + "hub_order_tracking_title": "Última encomenda", + "hub_order_tracking_see_all": "Ver as minhas encomendas", + "hub_order_tracking_error": "Não é possível recuperar o acompanhamento da encomenda", + "hub_order_tracking_order_id": "N.º {{orderId}}", + "order_tracking_history_DELIVERY": "Preparação da sua encomenda", + "order_tracking_history_FRAUD_CHECK": "Encomenda em curso de validação", + "order_tracking_history_FRAUD_DOCS_REQUESTED": "A aguardar documentos comprovativos", + "order_tracking_history_FRAUD_MANUAL_REVIEW": "Verificação da sua encomenda", + "order_tracking_history_FRAUD_REFUSED": "Encomenda recusada", + "order_tracking_history_INVOICE_IN_PROGRESS": "A sua encomenda está disponível", + "order_tracking_history_INVOICE_SENT": "A sua fatura está disponível", + "order_tracking_history_ORDER_ACCEPTED": "Encomenda aceite", + "order_tracking_history_ORDER_STARTED": "A sua encomenda foi registada", + "order_tracking_history_PAYMENT_CONFIRMED": "Pagamento validado", + "order_tracking_history_PAYMENT_INITIATED": "Pagamento em curso de validação", + "order_tracking_history_PAYMENT_RECEIVED": "Pagamento recebido", + "order_tracking_history_REGISTERED_PAYMENT_INITIATED": "Pagamento em curso de validação", + "order_tracking_history_custom_payment_waiting": "Sem pagamento", + "order_tracking_history_custom_creation": "Criação da sua nota de encomenda" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_de_DE.json new file mode 100644 index 000000000000..11a97ef4cbd5 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_de_DE.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Letzter Status der Zahlungen für Dienstleistungen", + "ovh_manager_hub_payment_status_tile_see_all": "Alles anzeigen", + "ovh_manager_hub_payment_status_tile_now": "Sofort", + "ovh_manager_hub_payment_status_tile_before": "vor dem {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "seit dem {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Aufstellung der Dienste nicht verfügbar", + "ovh_manager_hub_payment_status_tile_no_services": "Keine Dienste" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_en_GB.json new file mode 100644 index 000000000000..879ce20faaa4 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_en_GB.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Latest payment statuses for services", + "ovh_manager_hub_payment_status_tile_see_all": "See all", + "ovh_manager_hub_payment_status_tile_now": "Immediately", + "ovh_manager_hub_payment_status_tile_before": "Before {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "Since {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Unable to retrieve services", + "ovh_manager_hub_payment_status_tile_no_services": "No services" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_es_ES.json new file mode 100644 index 000000000000..8dc1675765c2 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_es_ES.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Últimos estados del pago de los servicios", + "ovh_manager_hub_payment_status_tile_see_all": "Ver todo", + "ovh_manager_hub_payment_status_tile_now": "De inmediato", + "ovh_manager_hub_payment_status_tile_before": "antes del {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "desde el {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "No es posible cargar los servicios.", + "ovh_manager_hub_payment_status_tile_no_services": "No hay servicios" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_CA.json new file mode 100644 index 000000000000..5636d8ff972e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_CA.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Derniers statuts de paiement des services", + "ovh_manager_hub_payment_status_tile_see_all": "Voir tout", + "ovh_manager_hub_payment_status_tile_now": "Immédiatement", + "ovh_manager_hub_payment_status_tile_before": "avant le {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "depuis le {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Impossible de récupérer les services", + "ovh_manager_hub_payment_status_tile_no_services": "Aucun service" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_FR.json new file mode 100644 index 000000000000..5636d8ff972e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_fr_FR.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Derniers statuts de paiement des services", + "ovh_manager_hub_payment_status_tile_see_all": "Voir tout", + "ovh_manager_hub_payment_status_tile_now": "Immédiatement", + "ovh_manager_hub_payment_status_tile_before": "avant le {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "depuis le {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Impossible de récupérer les services", + "ovh_manager_hub_payment_status_tile_no_services": "Aucun service" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_it_IT.json new file mode 100644 index 000000000000..f194c2a6cadb --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_it_IT.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Ultimi stati di pagamento dei servizi", + "ovh_manager_hub_payment_status_tile_see_all": "Visualizza tutto", + "ovh_manager_hub_payment_status_tile_now": "Immediatamente", + "ovh_manager_hub_payment_status_tile_before": "Prima del {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "dal {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Impossibile recuperare i servizi", + "ovh_manager_hub_payment_status_tile_no_services": "Nessun servizio" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pl_PL.json new file mode 100644 index 000000000000..618819264011 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pl_PL.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Ostatnie statusy płatności za usługi", + "ovh_manager_hub_payment_status_tile_see_all": "Zobacz wszystko", + "ovh_manager_hub_payment_status_tile_now": "Natychmiast", + "ovh_manager_hub_payment_status_tile_before": "przed {{date}}", + "ovh_manager_hub_payment_status_tile_renew": "począwszy od {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Nie można pobrać informacji o usługach", + "ovh_manager_hub_payment_status_tile_no_services": "Brak usług" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pt_PT.json new file mode 100644 index 000000000000..6d7323b5b9c0 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/payment-status/Messages_pt_PT.json @@ -0,0 +1,9 @@ +{ + "ovh_manager_hub_payment_status_tile_title": "Últimos estados de pagamento dos serviços", + "ovh_manager_hub_payment_status_tile_see_all": "Ver tudo", + "ovh_manager_hub_payment_status_tile_now": "Imediatamente", + "ovh_manager_hub_payment_status_tile_before": "antes de {{ date }}", + "ovh_manager_hub_payment_status_tile_renew": "a partir de {{ date }}", + "ovh_manager_hub_payment_status_tile_error": "Não é possível recuperar os serviços", + "ovh_manager_hub_payment_status_tile_no_services": "Nenhum serviço" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_de_DE.json new file mode 100644 index 000000000000..4ab52eacec97 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_de_DE.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Meine Produkte und Dienstleistungen", + "manager_hub_products_ALL_DOM": "AllDom-Paket", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "PCI Projekte", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Database", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DbaaS Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Server-Housing", + "manager_hub_products_DEDICATED_NASHA": "HA-NAS", + "manager_hub_products_DEDICATED_SERVER": "Dedicated Server", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Domainnamen", + "manager_hub_products_DOMAIN_ZONE": "DNS-Zone", + "manager_hub_products_EMAIL_DOMAIN": "E-Mails", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange ", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange ", + "manager_hub_products_EMAIL_PRO": "E-Mail für Profis", + "manager_hub_products_EXCHANGE": "Microsoft Exchange ", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Webhosting", + "manager_hub_products_IP_LOADBALANCER": "IP-Loadbalancer", + "manager_hub_products_IP_LOADBALANCING": "IP-Loadbalancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Lizenzen", + "manager_hub_products_LICENSE_WINDOWS": "Windows Lizenzen", + "manager_hub_products_LICENSE_OFFICE": "Office 365 Lizenzen", + "manager_hub_products_LICENSE_CLOUD_LINUX": "CloudLinux Lizenzen", + "manager_hub_products_LICENSE_CPANEL": "cPanel Lizenzen", + "manager_hub_products_LICENSE_PLESK": "Plesk Lizenzen", + "manager_hub_products_LICENSE_DIRECTADMIN": "DirectAdmin Lizenzen", + "manager_hub_products_LICENSE_VIRTUOZZO": "Virtuozzo Lizenzen", + "manager_hub_products_LICENSE_WORKLIGHT": "Worklight Lizenzen", + "manager_hub_products_LICENSE_SQLSERVER": "SQL Server Lizenzen", + "manager_hub_products_LICENCE_WINDOWS": "Windows Lizenzen", + "manager_hub_products_LICENCE_OFFICE": "Office 365 Lizenzen", + "manager_hub_products_LICENCE_CLOUD_LINUX": "CloudLinux Lizenzen", + "manager_hub_products_LICENCE_CPANEL": "cPanel Lizenzen", + "manager_hub_products_LICENCE_PLESK": "Plesk Lizenzen", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "DirectAdmin Lizenzen", + "manager_hub_products_LICENCE_VIRTUOZZO": "Virtuozzo Lizenzen", + "manager_hub_products_LICENCE_WORKLIGHT": "Worklight Lizenzen", + "manager_hub_products_LICENCE_SQLSERVER": "SQL Server Lizenzen", + "manager_hub_products_LICENCE_SQL_SERVER": "SQL Server Lizenzen", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft Sharepoint ", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "xDSL-Pakete", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Telefonie", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Virtual Private Server", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Weniger Dienste anzeigen", + "manager_hub_products_see_more": "Mehr Dienste anzeigen" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_en_GB.json new file mode 100644 index 000000000000..7a721fb1cf8d --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_en_GB.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "My products and services", + "manager_hub_products_ALL_DOM": "AllDom Pack", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "PCI projects", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Database", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DBaaS Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "Hosted Private Cloud", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Housing server ", + "manager_hub_products_DEDICATED_NASHA": "HA-NAS", + "manager_hub_products_DEDICATED_SERVER": "Dedicated servers", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Domain names", + "manager_hub_products_DOMAIN_ZONE": "DNS zones", + "manager_hub_products_EMAIL_DOMAIN": "Emails", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "Professional emails", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Web Hosting", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licenses", + "manager_hub_products_LICENSE_WINDOWS": "Windows licences", + "manager_hub_products_LICENSE_OFFICE": "Office 365 licences", + "manager_hub_products_LICENSE_CLOUD_LINUX": "CloudLinux licences", + "manager_hub_products_LICENSE_CPANEL": "cPanel licences ", + "manager_hub_products_LICENSE_PLESK": "Plesk licences", + "manager_hub_products_LICENSE_DIRECTADMIN": "DirectAdmin licences", + "manager_hub_products_LICENSE_VIRTUOZZO": "Virtuozzo licences ", + "manager_hub_products_LICENSE_WORKLIGHT": "Worklight licences", + "manager_hub_products_LICENSE_SQLSERVER": "SQL Server licences ", + "manager_hub_products_LICENCE_WINDOWS": "Windows licences", + "manager_hub_products_LICENCE_OFFICE": "Office 365 licences", + "manager_hub_products_LICENCE_CLOUD_LINUX": "CloudLinux licences", + "manager_hub_products_LICENCE_CPANEL": "cPanel licences ", + "manager_hub_products_LICENCE_PLESK": "Plesk licences", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "DirectAdmin licences", + "manager_hub_products_LICENCE_VIRTUOZZO": "Virtuozzo licences ", + "manager_hub_products_LICENCE_WORKLIGHT": "Worklight licences", + "manager_hub_products_LICENCE_SQLSERVER": "SQL Server licences ", + "manager_hub_products_LICENCE_SQL_SERVER": "SQL Server licences ", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "xDSL packs", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Telephony", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Virtual private servers", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "See less services", + "manager_hub_products_see_more": "See all products and services" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_es_ES.json new file mode 100644 index 000000000000..dcd1434a0e10 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_es_ES.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Mis productos y servicios", + "manager_hub_products_ALL_DOM": "Pack AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Proyectos PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Databases", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DBaaS Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Servidor en housing", + "manager_hub_products_DEDICATED_NASHA": "NAS-HA", + "manager_hub_products_DEDICATED_SERVER": "Servidores dedicados", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Dominios", + "manager_hub_products_DOMAIN_ZONE": "Zonas DNS", + "manager_hub_products_EMAIL_DOMAIN": "Direcciones de correo", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "Emails Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Alojamiento web", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licencias", + "manager_hub_products_LICENSE_WINDOWS": "Licencias Windows", + "manager_hub_products_LICENSE_OFFICE": "Licencias Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licencias CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licencias cPanel", + "manager_hub_products_LICENSE_PLESK": "Licencias Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licencias DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licencias Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licencias Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licencias SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licencias Windows", + "manager_hub_products_LICENCE_OFFICE": "Licencias Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licencias CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licencias cPanel", + "manager_hub_products_LICENCE_PLESK": "Licencias Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licencias DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licencias Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licencias Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licencias SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licencias SQL Server", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Packs xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Telefonía", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterpise", + "manager_hub_products_VPS": "Servidores privados virtuales", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Ver menos servicios", + "manager_hub_products_see_more": "Ver más servicios" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_CA.json new file mode 100644 index 000000000000..4f01d6d04a3d --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_CA.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Mes produits et services", + "manager_hub_products_ALL_DOM": "Pack AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Projets PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Database", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "Dbaas Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Serveur Housing", + "manager_hub_products_DEDICATED_NASHA": "NASHA", + "manager_hub_products_DEDICATED_SERVER": "Serveurs Dédiés", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Noms de domaine", + "manager_hub_products_DOMAIN_ZONE": "Zones DNS", + "manager_hub_products_EMAIL_DOMAIN": "E-mails", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "E-mails Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Hébergement Web", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licences", + "manager_hub_products_LICENSE_WINDOWS": "Licences Windows", + "manager_hub_products_LICENSE_OFFICE": "Licences Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licences CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licences cPanel", + "manager_hub_products_LICENSE_PLESK": "Licences Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licences DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licences Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licences Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licences SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licences Windows", + "manager_hub_products_LICENCE_OFFICE": "Licences Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licences CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licences cPanel", + "manager_hub_products_LICENCE_PLESK": "Licences Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licences DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licences Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licences Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licences SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licences SQL Server", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Packs xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Téléphonie", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Serveurs privés virtuels", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Voir moins de services", + "manager_hub_products_see_more": "Voir tous les produits et services" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_FR.json new file mode 100644 index 000000000000..4f01d6d04a3d --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_fr_FR.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Mes produits et services", + "manager_hub_products_ALL_DOM": "Pack AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Projets PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Database", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "Dbaas Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Serveur Housing", + "manager_hub_products_DEDICATED_NASHA": "NASHA", + "manager_hub_products_DEDICATED_SERVER": "Serveurs Dédiés", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Noms de domaine", + "manager_hub_products_DOMAIN_ZONE": "Zones DNS", + "manager_hub_products_EMAIL_DOMAIN": "E-mails", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "E-mails Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Hébergement Web", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licences", + "manager_hub_products_LICENSE_WINDOWS": "Licences Windows", + "manager_hub_products_LICENSE_OFFICE": "Licences Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licences CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licences cPanel", + "manager_hub_products_LICENSE_PLESK": "Licences Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licences DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licences Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licences Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licences SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licences Windows", + "manager_hub_products_LICENCE_OFFICE": "Licences Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licences CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licences cPanel", + "manager_hub_products_LICENCE_PLESK": "Licences Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licences DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licences Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licences Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licences SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licences SQL Server", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Packs xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Téléphonie", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Serveurs privés virtuels", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Voir moins de services", + "manager_hub_products_see_more": "Voir tous les produits et services" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_it_IT.json new file mode 100644 index 000000000000..3674ff1cdb97 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_it_IT.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "I tuoi prodotti e servizi", + "manager_hub_products_ALL_DOM": "Pack AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Progetti PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Databases", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DBaaS Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Server in housing", + "manager_hub_products_DEDICATED_NASHA": "NASHA", + "manager_hub_products_DEDICATED_SERVER": "Server dedicati", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Domini", + "manager_hub_products_DOMAIN_ZONE": "Zone DNS", + "manager_hub_products_EMAIL_DOMAIN": "Email", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "Email Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Hosting Web", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licenze", + "manager_hub_products_LICENSE_WINDOWS": "Licenze Windows", + "manager_hub_products_LICENSE_OFFICE": "Licenze Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licenze CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licenze cPanel", + "manager_hub_products_LICENSE_PLESK": "Licenze Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licenze DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licenze Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licenze Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licenze SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licenze Windows", + "manager_hub_products_LICENCE_OFFICE": "Licenze Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licenze CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licenze cPanel", + "manager_hub_products_LICENCE_PLESK": "Licenze Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licenze DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licenze Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licenze Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licenze SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licenze SQL Server", + "manager_hub_products_METRICS": "Metrics", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Pack xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Telefonia", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Server Privati Virtuali", + "manager_hub_products_VRACK": "vRack", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Visualizza meno servizi", + "manager_hub_products_see_more": "Visualizza più servizi" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pl_PL.json new file mode 100644 index 000000000000..722a944810a0 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pl_PL.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Produkty i usługi", + "manager_hub_products_ALL_DOM": "Pakiet AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Projekty PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Databases", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DBaaS Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Housing", + "manager_hub_products_DEDICATED_NASHA": "NASHA", + "manager_hub_products_DEDICATED_SERVER": "Serwery dedykowane", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Domeny", + "manager_hub_products_DOMAIN_ZONE": "Strefy DNS", + "manager_hub_products_EMAIL_DOMAIN": "E-maile", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "E-maile Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Faks", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Hosting WWW", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "Adresy IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licencje", + "manager_hub_products_LICENSE_WINDOWS": "Licencje Windows", + "manager_hub_products_LICENSE_OFFICE": "Licencje Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licencje CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licencje cPanel", + "manager_hub_products_LICENSE_PLESK": "Licencje Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licencje DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licencje Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licencje Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licencje SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licencje Windows", + "manager_hub_products_LICENCE_OFFICE": "Licencje Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licencje CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licencje cPanel", + "manager_hub_products_LICENCE_PLESK": "Licencje Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licencje DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licencje Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licencje Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licencje SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licencje SQL Server", + "manager_hub_products_METRICS": "Metryki", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft Sharepoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHCloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Pakiety xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Telefonia", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VPS": "Prywatne serwery wirtualne", + "manager_hub_products_VRACK": "vRack", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Wyświetl mniej usług", + "manager_hub_products_see_more": "Wyświetl więcej usług" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pt_PT.json new file mode 100644 index 000000000000..f0eeb51a89f2 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/products/Messages_pt_PT.json @@ -0,0 +1,71 @@ +{ + "manager_hub_dashboard_services": "Os meus produtos e serviços", + "manager_hub_products_ALL_DOM": "Pack AllDom", + "manager_hub_products_CDN_DEDICATED": "CDN", + "manager_hub_products_CLOUD_PROJECT": "Projetos PCI", + "manager_hub_products_CLOUD_DB_ENTERPRISE_CLUSTER": "Enterprise Cloud Databases", + "manager_hub_products_DBAAS_LOGS": "Logs Data Platform", + "manager_hub_products_DBAAS_QUEUE": "DBaas Queue", + "manager_hub_products_DEDICATED_CEPH": "Cloud Disk Array", + "manager_hub_products_DEDICATED_CLOUD": "VMware", + "manager_hub_products_ESSENTIALS": "Managed Bare Metal", + "manager_hub_products_DEDICATED_HOUSING": "Servidor housing", + "manager_hub_products_DEDICATED_NASHA": "NASHA", + "manager_hub_products_DEDICATED_SERVER": "Servidores dedicados", + "manager_hub_products_DESKAAS": "Cloud Desktop", + "manager_hub_products_DOMAIN": "Nomes de domínio", + "manager_hub_products_DOMAIN_ZONE": "Zonas DNS", + "manager_hub_products_EMAIL_DOMAIN": "E-mails", + "manager_hub_products_EMAIL_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_EMAIL_EXCHANGE_SERVICE": "Microsoft Exchange", + "manager_hub_products_EMAIL_PRO": "E-mails Pro", + "manager_hub_products_EXCHANGE": "Microsoft Exchange", + "manager_hub_products_FREEFAX": "Fax", + "manager_hub_products_HOSTING_PRIVATE_DATABASE": "Web Cloud Databases", + "manager_hub_products_HOSTING_WEB": "Alojamento web", + "manager_hub_products_IP_LOADBALANCER": "IP Load Balancer", + "manager_hub_products_IP_LOADBALANCING": "IP Load Balancer", + "manager_hub_products_IP_SERVICE": "IP", + "manager_hub_products_KEY_MANAGEMENT_SERVICE": "Key Management Service", + "manager_hub_products_LICENCE": "Licenças", + "manager_hub_products_LICENSE_WINDOWS": "Licenças Windows", + "manager_hub_products_LICENSE_OFFICE": "Licenças Office 365", + "manager_hub_products_LICENSE_CLOUD_LINUX": "Licenças CloudLinux", + "manager_hub_products_LICENSE_CPANEL": "Licenças cPanel", + "manager_hub_products_LICENSE_PLESK": "Licenças Plesk", + "manager_hub_products_LICENSE_DIRECTADMIN": "Licenças DirectAdmin", + "manager_hub_products_LICENSE_VIRTUOZZO": "Licenças Virtuozzo", + "manager_hub_products_LICENSE_WORKLIGHT": "Licenças Worklight", + "manager_hub_products_LICENSE_SQLSERVER": "Licenças SQL Server", + "manager_hub_products_LICENCE_WINDOWS": "Licenças Windows", + "manager_hub_products_LICENCE_OFFICE": "Licenças Office 365", + "manager_hub_products_LICENCE_CLOUD_LINUX": "Licenças CloudLinux", + "manager_hub_products_LICENCE_CPANEL": "Licenças cPanel", + "manager_hub_products_LICENCE_PLESK": "Licenças Plesk", + "manager_hub_products_LICENCE_DIRECT_ADMIN": "Licenças DirectAdmin", + "manager_hub_products_LICENCE_VIRTUOZZO": "Licenças Virtuozzo", + "manager_hub_products_LICENCE_WORKLIGHT": "Licenças Worklight", + "manager_hub_products_LICENCE_SQLSERVER": "Licenças SQL Server", + "manager_hub_products_LICENCE_SQL_SERVER": "Licenças SQL Server", + "manager_hub_products_METRICS": "Estatísticas", + "manager_hub_products_MS_SERVICES_SHAREPOINT": "Microsoft SharePoint", + "manager_hub_products_STORAGE_NETAPP": "Enterprise File Storage", + "manager_hub_products_NUTANIX": "Nutanix", + "manager_hub_products_NETAPP": "NetApp", + "manager_hub_products_OVH_CLOUD_CONNECT": "OVHcloud Connect", + "manager_hub_products_OVER_THE_BOX": "OverTheBox", + "manager_hub_products_PACK_XDSL": "Packs xDSL", + "manager_hub_products_SMS": "SMS", + "manager_hub_products_SSL_GATEWAY": "SSL Gateway", + "manager_hub_products_TELEPHONY": "Serviço Telefónico", + "manager_hub_products_VEEAM_CLOUD_CONNECT": "Veeam Cloud Connect", + "manager_hub_products_VEEAM_ENTERPRISE": "Veeam Enterprise", + "manager_hub_products_VEEAM_VEEAM_ENTERPRISE": "Veeam Enterpise", + "manager_hub_products_VPS": "Servidores privados virtuais", + "manager_hub_products_VRACK": "vRacks", + "manager_hub_products_VRACK_SERVICES": "vRack Services", + "manager_hub_products_XDSL": "xDSL", + "manager_hub_products_WEB_PAA_S_SUBSCRIPTION": "Web PaaS", + "manager_hub_products_see_less": "Ver menos serviços", + "manager_hub_products_see_more": "Ver mais serviços" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_de_DE.json new file mode 100644 index 000000000000..66297da1f6a3 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_de_DE.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "Klicken Sie hier für das Update.", + "manager_hub_dashboard_banner_siret": "Um die Qualität Ihrer Daten zu verbessern, müssen wir die SIRET-Nummer Ihres Unternehmens aktualisieren. Diese Aktualisierung dauert nur wenige Sekunden. Bitte nehmen Sie Ihren KBIS zur Hand. ", + "manager_hub_dashboard_modal_title": "Aktualisierung Ihrer Kontaktdaten", + "manager_hub_dashboard_modal_siret_part_1": "Um die Qualität Ihrer Daten zu verbessern, müssen wir die SIRET-Nummer Ihres Unternehmens aktualisieren.", + "manager_hub_dashboard_modal_siret_part_2": "Mit Ihrem KBIS wird diese Aktualisierung nur wenige Sekunden dauern.", + "manager_hub_dashboard_modal_siret_link": "Aktualisieren", + "manager_hub_dashboard_modal_siret_cancel": "Nicht jetzt" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_en_GB.json new file mode 100644 index 000000000000..6ca5e2298747 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_en_GB.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "click here for the update.", + "manager_hub_dashboard_banner_siret": "To improve the quality of your data, we need to update your company’s SIRET number. This update will only take a few seconds. Please take your KBIS and ", + "manager_hub_dashboard_modal_title": "Update your contact details", + "manager_hub_dashboard_modal_siret_part_1": "To improve the quality of your data, we need to update your company’s SIRET number.", + "manager_hub_dashboard_modal_siret_part_2": "With your KBIS, this update will only take a few seconds.", + "manager_hub_dashboard_modal_siret_link": "Update", + "manager_hub_dashboard_modal_siret_cancel": "Not now" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_es_ES.json new file mode 100644 index 000000000000..44e5e399ec62 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_es_ES.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "Haga clic aquí para lanzar la actualización.", + "manager_hub_dashboard_banner_siret": "Con el objetivo de mejorar la calidad de sus datos, necesitamos actualizar el número SIRET de su empresa. Esta actualización se realizará en solo unos segundos. Tenga en cuenta que necesitará su extracto KBIS. ", + "manager_hub_dashboard_modal_title": "Actualización de sus datos", + "manager_hub_dashboard_modal_siret_part_1": "Con el objetivo de mejorar la calidad de sus datos, necesitamos actualizar el número SIRET de su empresa.", + "manager_hub_dashboard_modal_siret_part_2": "Esta actualización se realizará en tan solo unos segundos. Tenga en cuenta que necesitará su extracto KBIS.", + "manager_hub_dashboard_modal_siret_link": "Actualizar ahora", + "manager_hub_dashboard_modal_siret_cancel": "En otro momento" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_CA.json new file mode 100644 index 000000000000..94c6213ff043 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_CA.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "cliquez-ici pour la mise à jour.", + "manager_hub_dashboard_banner_siret": "Dans le cadre de l’amélioration de la qualité de vos données, nous avons besoin de mettre à jour le numéro SIRET de votre entreprise. Cette mise à jour ne prendra que quelques secondes, munissez-vous de votre KBIS et ", + "manager_hub_dashboard_modal_title": "Mise à jour de vos coordonnées", + "manager_hub_dashboard_modal_siret_part_1": "Dans le cadre de l’amélioration de la qualité de vos données, nous avons besoin de mettre à jour le numéro SIRET de votre entreprise.", + "manager_hub_dashboard_modal_siret_part_2": "Avec l'aide de votre KBIS, cette mise à jour ne prendra que quelques secondes.", + "manager_hub_dashboard_modal_siret_link": "Mettre à jour", + "manager_hub_dashboard_modal_siret_cancel": "Pas maintenant" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_FR.json new file mode 100644 index 000000000000..5fa388cf388e --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_fr_FR.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret": "Dans le cadre de l’amélioration de la qualité de vos données, nous avons besoin de mettre à jour le numéro SIRET de votre entreprise. Cette mise à jour ne prendra que quelques secondes, munissez-vous de votre KBIS et ", + "manager_hub_dashboard_banner_siret_link": "cliquez-ici pour la mise à jour.", + "manager_hub_dashboard_modal_title": "Mise à jour de vos coordonnées", + "manager_hub_dashboard_modal_siret_part_1": "Dans le cadre de l’amélioration de la qualité de vos données, nous avons besoin de mettre à jour le numéro SIRET de votre entreprise.", + "manager_hub_dashboard_modal_siret_part_2": "Avec l'aide de votre KBIS, cette mise à jour ne prendra que quelques secondes.", + "manager_hub_dashboard_modal_siret_link": "Mettre à jour", + "manager_hub_dashboard_modal_siret_cancel": "Pas maintenant" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_it_IT.json new file mode 100644 index 000000000000..14a8ad890319 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_it_IT.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "clicca qui per l'aggiornamento.", + "manager_hub_dashboard_banner_siret": "Per migliorare la qualità dei tuoi dati, è necessario aggiornare il numero SIRET della tua azienda. Questo aggiornamento richiederà solo alcuni secondi, assicurati di avere a disposizione l’estratto KBIS e ", + "manager_hub_dashboard_modal_title": "Aggiornamento dei tuoi dati", + "manager_hub_dashboard_modal_siret_part_1": "Per migliorare la qualità dei tuoi dati, è necessario aggiornare il numero SIRET della tua azienda.", + "manager_hub_dashboard_modal_siret_part_2": "Utilizzando il tuo estratto KBIS, l'aggiornamento richiederà solo alcuni secondi.", + "manager_hub_dashboard_modal_siret_link": "Aggiorna", + "manager_hub_dashboard_modal_siret_cancel": "Non adesso" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pl_PL.json new file mode 100644 index 000000000000..2c01793bc1c7 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pl_PL.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "kliknij tutaj, aby przeprowadzić aktualizację.", + "manager_hub_dashboard_banner_siret": "Prosimy o aktualizację numeru REGON Twojej firmy. Aktualizacja danych zajmie tylko kilka sekund. Sprawdź dane w wyciągu z rejestru handlowego i ", + "manager_hub_dashboard_modal_title": "Aktualizacja danych", + "manager_hub_dashboard_modal_siret_part_1": "Prosimy o aktualizację numeru REGON Twojej firmy.", + "manager_hub_dashboard_modal_siret_part_2": "Aktualizacja danych zajmie tylko kilka sekund. Dane znajdziesz w wyciągu z rejestru handlowego.", + "manager_hub_dashboard_modal_siret_link": "Aktualizuj", + "manager_hub_dashboard_modal_siret_cancel": "Nie teraz" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pt_PT.json new file mode 100644 index 000000000000..e89c9a05ed36 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/siret/Messages_pt_PT.json @@ -0,0 +1,9 @@ +{ + "manager_hub_dashboard_banner_siret_link": "clique aqui para efetuar a atualização.", + "manager_hub_dashboard_banner_siret": "Para melhorar a qualidade dos seus dados, é necessário atualizar o NIF da sua empresa. Esta atualização demorará apenas alguns segundos. Tenha consigo a sua certidão de registo comercial e ", + "manager_hub_dashboard_modal_title": "Atualização dos seus dados", + "manager_hub_dashboard_modal_siret_part_1": "Para melhorar a qualidade dos seus dados, é necessário atualizar o NIF da sua empresa.", + "manager_hub_dashboard_modal_siret_part_2": "Se tiver a sua certidão de registo comercial consigo, esta atualização demorará apenas alguns segundos.", + "manager_hub_dashboard_modal_siret_link": "Atualizar", + "manager_hub_dashboard_modal_siret_cancel": "Agora não" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_de_DE.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_de_DE.json new file mode 100644 index 000000000000..15560381c073 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_de_DE.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Kürzliche Support-Anfragen", + "hub_support_read": "Lesen", + "hub_support_state_open": "Offen", + "hub_support_state_closed": "Geschlossen", + "hub_support_state_unknown": "Unbekannt", + "hub_support_account_management": "Accountverwaltung", + "hub_support_need_help": "Sie benötigen Hilfe?", + "hub_support_need_help_more": "Gibt es Probleme mit Ihren Produkten und Dienstleistungen? Holen Sie sich in unseren Anleitungen Rat.", + "hub_support_help": "Ihre Dienstleistungen von OVHcloud verstehen und nutzen", + "hub_support_error": "Ihre Support-Anfragen können nicht abgerufen werden." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_en_GB.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_en_GB.json new file mode 100644 index 000000000000..522fa8e08e2b --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_en_GB.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Recent support tickets", + "hub_support_read": "Read", + "hub_support_state_open": "Open", + "hub_support_state_closed": "Closed", + "hub_support_state_unknown": "Unknown", + "hub_support_account_management": "Account management", + "hub_support_need_help": "Need help?", + "hub_support_need_help_more": "Need support using our products and services? Browse our guides.", + "hub_support_help": "Understanding and using OVHcloud solutions", + "hub_support_error": "Unable to retrieve support tickets" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_es_ES.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_es_ES.json new file mode 100644 index 000000000000..e710ced58116 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_es_ES.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Últimas solicitudes de asistencia", + "hub_support_read": "Leer", + "hub_support_state_open": "Abierta", + "hub_support_state_closed": "Cerrada", + "hub_support_state_unknown": "Desconocida", + "hub_support_account_management": "Gestión de cuenta", + "hub_support_need_help": "¿Necesita ayuda?", + "hub_support_need_help_more": "¿Tiene problemas para utilizar sus productos y servicios? Consulte nuestras guías.", + "hub_support_help": "Entender y utilizar los servicios de OVHcloud", + "hub_support_error": "No es posible cargar las solicitudes de asistencia." +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_CA.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_CA.json new file mode 100644 index 000000000000..713e1c37fa74 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_CA.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Dernières demandes d'assistance", + "hub_support_read": "Lire", + "hub_support_state_open": "Ouvert", + "hub_support_state_closed": "Fermé", + "hub_support_state_unknown": "Inconnu", + "hub_support_account_management": "Gestion du compte", + "hub_support_need_help": "Besoin d'aide ?", + "hub_support_need_help_more": "Des difficultés pour utiliser vos produits et services ? Consultez dès maintenant nos guides.", + "hub_support_help": "Comprendre et utiliser vos services OVHcloud", + "hub_support_error": "Impossible de récupérer vos demandes d'assistance" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_FR.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_FR.json new file mode 100644 index 000000000000..e91a4672af93 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_fr_FR.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Dernières demandes d'assistance", + "hub_support_read": "Lire", + "hub_support_state_open": "Ouvert", + "hub_support_state_closed": "Fermé", + "hub_support_state_unknown": "Inconnu", + "hub_support_account_management": "Gestion du compte", + "hub_support_need_help": "Besoin d'aide ?", + "hub_support_need_help_more": "Des difficultés pour utiliser vos produits et services ? Consultez dès maintenant nos guides.", + "hub_support_help": "Comprendre et utiliser vos services OVHcloud", + "hub_support_error": "Impossible de récupérer vos demandes d'assistance" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_it_IT.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_it_IT.json new file mode 100644 index 000000000000..9daf28a6bfd3 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_it_IT.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Ultime richieste di assistenza", + "hub_support_read": "Leggi", + "hub_support_state_open": "Aperto", + "hub_support_state_closed": "Chiuso", + "hub_support_state_unknown": "Sconosciuto", + "hub_support_account_management": "Gestione dell’account", + "hub_support_need_help": "Bisogno di aiuto?", + "hub_support_need_help_more": "Difficoltà nell’utilizzo di prodotti e servizi? Consulta le guide disponibili.", + "hub_support_help": "Comprendere e utilizzare le soluzioni OVHcloud", + "hub_support_error": "Impossibile recuperare le richieste di assistenza" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pl_PL.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pl_PL.json new file mode 100644 index 000000000000..36bfa6f8a1e7 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pl_PL.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Ostatnie zgłoszenia", + "hub_support_read": "Przeczytaj", + "hub_support_state_open": "Otwarte", + "hub_support_state_closed": "Zamknięte", + "hub_support_state_unknown": "Nieznany", + "hub_support_account_management": "Zarządzanie kontem", + "hub_support_need_help": "Potrzebujesz pomocy?", + "hub_support_need_help_more": "Masz trudności w korzystaniu z produktów i usług? Sprawdź nasze przewodniki. ", + "hub_support_help": "Jak działają rozwiązania OVHcloud", + "hub_support_error": "Nie można pobrać Twoich zgłoszeń" +} diff --git a/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pt_PT.json b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pt_PT.json new file mode 100644 index 000000000000..70b7a5e14a17 --- /dev/null +++ b/packages/manager/apps/hub-react/public/translations/hub/support/Messages_pt_PT.json @@ -0,0 +1,12 @@ +{ + "hub_support_title": "Últimos pedidos de assistência", + "hub_support_read": "Ler", + "hub_support_state_open": "Aberto", + "hub_support_state_closed": "Encerrado", + "hub_support_state_unknown": "Desconhecido", + "hub_support_account_management": "Gestão da conta", + "hub_support_need_help": "Precisa de ajuda?", + "hub_support_need_help_more": "Tem dificuldades em utilizar os seus produtos e serviços? Consulte os nossos manuais agora.", + "hub_support_help": "Compreender e utilizar os serviços da OVHcloud", + "hub_support_error": "Não foi possível recuperar os seus pedidos de assistência" +} diff --git a/packages/manager/apps/hub-react/src/App.tsx b/packages/manager/apps/hub-react/src/App.tsx new file mode 100644 index 000000000000..c55ab5a7f5f0 --- /dev/null +++ b/packages/manager/apps/hub-react/src/App.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { odsSetup } from '@ovhcloud/ods-common-core'; +import { RouterProvider, createHashRouter } from 'react-router-dom'; +import { Routes } from './routes/routes'; +import queryClient from '@/queryClient'; + +odsSetup(); + +function App() { + const router = createHashRouter(Routes); + + return ( + + + + + ); +} + +export default App; diff --git a/packages/manager/apps/hub-react/src/_mock_/billingServices.ts b/packages/manager/apps/hub-react/src/_mock_/billingServices.ts new file mode 100644 index 000000000000..ea37e8ff18b4 --- /dev/null +++ b/packages/manager/apps/hub-react/src/_mock_/billingServices.ts @@ -0,0 +1,150 @@ +import { + HubBillingServices, + BillingService, +} from '@/billing/types/billingServices.type'; + +const serviceResiliated = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic1', + contactBilling: 'billingNic1', + domain: 'serviceResiliated', + expiration: '2024-10-01T07:37:24Z', + id: 333333, + renew: { + automatic: true, + deleteAtExpiration: false, + forced: false, + manualPayment: false, + period: 12, + }, + renewalType: 'automaticV2016', + serviceId: 'serviceResiliated', + serviceType: 'HOSTING_WEB', + status: 'TERMINATED', + url: + 'https://www.ovh.com/manager/#/web/configuration/hosting/serviceResiliated', +}); +const serviceWithManualRenewNotResiliatedWithoutDebt = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic2', + contactBilling: 'billingNic2', + domain: 'serviceWithManualRenewNotResiliatedWithoutDebt', + expiration: '2024-10-06T16:38:41Z', + id: 444444, + renew: { + automatic: false, + deleteAtExpiration: false, + forced: false, + manualPayment: true, + period: null, + }, + renewalType: 'manual', + serviceId: 'serviceWithManualRenewNotResiliatedWithoutDebt', + serviceType: 'DOMAIN', + status: 'ACTIVE', + url: + 'https://www.ovh.com/manager/#/web/configuration/domain/serviceWithManualRenewNotResiliatedWithoutDebt/information', +}); +const serviceOneShotWithoutResiliation = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic3', + contactBilling: 'billingNic3', + domain: 'serviceOneShotWithoutResiliation', + expiration: '2024-11-19T04:28:17Z', + id: 555555, + renew: { + automatic: false, + deleteAtExpiration: false, + forced: false, + manualPayment: false, + period: 12, + }, + renewalType: 'oneShot', + serviceId: 'serviceOneShotWithoutResiliation', + serviceType: 'DEDICATED_SERVER', + status: 'ACTIVE', + url: + 'https://www.ovh.com/manager/#/dedicated/server/serviceOneShotWithoutResiliation', +}); +const serviceWithoutUrlAndSuspendedBilling = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic4', + contactBilling: 'billingNic4', + domain: 'serviceWithoutUrlAndSuspendedBilling', + expiration: '2024-11-19T14:49:20Z', + id: 666666, + renew: { + automatic: false, + deleteAtExpiration: false, + forced: false, + manualPayment: true, + period: 12, + }, + renewalType: 'automaticV2016', + serviceId: 'serviceWithoutUrlAndSuspendedBilling', + serviceType: 'DEDICATED_CLOUD', + status: 'BILLING_SUSPENDED', + url: null, +}); +const serviceInDebt = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic3', + contactBilling: 'billingNic3', + domain: 'serviceOneShotWithoutResiliation', + expiration: '2024-11-19T04:28:17Z', + id: 777777, + renew: { + automatic: false, + deleteAtExpiration: false, + forced: false, + manualPayment: false, + period: 12, + }, + renewalType: 'oneShot', + serviceId: 'serviceOneShotWithoutResiliation', + serviceType: 'DEDICATED_SERVER', + status: 'PENDING_DEBT', + url: + 'https://www.ovh.com/manager/#/dedicated/server/serviceOneShotWithoutResiliation', +}); +const serviceWithAutomaticRenewNotResiliated = new BillingService({ + canDeleteAtExpiration: false, + contactAdmin: 'adminNic1', + contactBilling: 'billingNic1', + domain: 'serviceWithAutomaticRenewNotResiliated', + expiration: '2024-10-01T07:37:24Z', + id: 888888, + renew: { + automatic: true, + deleteAtExpiration: false, + forced: false, + manualPayment: false, + period: 12, + }, + renewalType: 'automaticV2016', + serviceId: 'serviceWithAutomaticRenewNotResiliated', + serviceType: 'HOSTING_WEB', + status: 'ACTIVE', + url: + 'https://www.ovh.com/manager/#/web/configuration/hosting/serviceWithAutomaticRenewNotResiliated', +}); + +export const NoServices: HubBillingServices = { + services: [], + count: 0, +}; + +export const TwoServices: HubBillingServices = { + services: [serviceInDebt, serviceWithAutomaticRenewNotResiliated], + count: 2, +}; + +export const FourServices: HubBillingServices = { + services: [ + serviceResiliated, + serviceWithManualRenewNotResiliatedWithoutDebt, + serviceOneShotWithoutResiliation, + serviceWithoutUrlAndSuspendedBilling, + ], + count: 4, +}; diff --git a/packages/manager/apps/hub-react/src/_mock_/catalog.ts b/packages/manager/apps/hub-react/src/_mock_/catalog.ts new file mode 100644 index 000000000000..47a024864bc2 --- /dev/null +++ b/packages/manager/apps/hub-react/src/_mock_/catalog.ts @@ -0,0 +1,894 @@ +export const catalogData = { + 'Hosted Private Cloud': [ + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Platform', + 'VMware', + 'VMware on OVHcloud', + ], + category: 'Platform', + description: + 'VMware on OVHcloud is a unique solution on the market, offering cloud scalability on a 100% dedicated hardware infrastructure. Your infrastructure virtualisation is powered by VMware technology, and entirely managed by OVHcloud. ', + featureAvailability: 'dedicated-cloud:order', + highlight: true, + id: 20414, + lang: 'en_GB', + name: 'VMware on OVHcloud', + order: + 'https://www.ovhcloud.com/it/enterprise/products/hosted-private-cloud/prices/', + productName: 'PRIVATE_CLOUD', + regionTags: ['EU', 'US', 'CA'], + universe: 'Hosted Private Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Platform', + 'SAP HANA on Private Cloud', + ], + category: 'Platform', + description: + 'This new platform combines SAP HANA-certified HCI servers with our VMware on OVHcloud infrastructure. It enables secure hosting and effortless deployment of your most critical SAP environments in a sovereign cloud.', + featureAvailability: 'dedicated-cloud:sapHanaOrder', + highlight: true, + id: 21725, + lang: 'en_GB', + name: 'SAP HANA on Private Cloud', + order: 'https://www.ovhcloud.com/it/hosted-private-cloud/sap-hana/', + productName: 'SAP_HANA', + regionTags: ['EU', 'CA'], + universe: 'Hosted Private Cloud', + url: 'https://mockup.url.com', + }, + ], + 'Bare Metal Cloud': [ + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Dedicated Servers'], + category: 'Dedicated Servers', + description: + 'Choose the best infrastructure for your business applications. We offer different ranges of highly efficient dedicated servers, adapted to the most demanding needs of all companies.', + featureAvailability: 'dedicated-server:order', + highlight: true, + id: 20804, + lang: 'en_GB', + name: 'Dedicated Servers', + order: 'https://www.ovhcloud.com/it/bare-metal/prices/', + productName: 'DEDICATED_SERVER', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/bare-metal', + }, + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Virtual Private Servers'], + category: 'Virtual Private Servers', + description: + 'A virtual private server, also referred to as a VPS, is a virtual dedicated server. Unlike web hosting — (also known as shared hosting), where the technical aspects are managed by OVHcloud — you are the administrator of your VPS, and fully manage it.', + featureAvailability: 'vps', + highlight: true, + id: 20817, + lang: 'en_GB', + name: 'Virtual Private Servers', + order: 'https://www.ovh.it/vps/', + productName: 'VPS', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/vps', + }, + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Managed Bare Metal'], + category: 'Managed Bare Metal', + description: + 'Your scalable dedicated cloud, hosted and monitored by OVHcloud, available in just 90 minutes. With the Essentials range, opt for highly available VMware virtualisation, and stay focused on your business.', + featureAvailability: 'managed-bare-metal', + highlight: true, + id: 20820, + lang: 'en_GB', + name: 'Managed Bare Metal', + order: 'https://www.ovhcloud.com/it/managed-bare-metal/', + productName: 'ESSENTIALS', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/managed-bare-metal/', + }, + { + categories: [ + 'Catalogs', + 'Bare Metal Cloud', + 'Storage and Backup', + 'Enterprise File Storage', + ], + category: 'Storage and Backup', + description: + 'Enterprise File Storage, powered by NetApp. Connect of all your OVHcloud solutions to a high-performance file storage service for your most critical business applications.', + featureAvailability: 'netapp', + highlight: true, + id: 20825, + lang: 'en_GB', + name: 'Enterprise File Storage', + order: 'https://www.ovh.com/manager/#/dedicated/netapp/new', + productName: 'NETAPP', + regionTags: ['EU', 'CA'], + universe: 'Bare Metal Cloud', + url: + 'https://ovhcloud.com/en-gb/storage-solutions/enterprise-file-storage/', + }, + ], +}; + +export const rawCatalogData = [ + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Block Storage', + ], + category: 'Storage and Backup', + description: + 'Create storage volumes, which can be used as additional disks and secured via triple replication for data.', + id: 20374, + lang: 'en_GB', + name: 'Block Storage', + order: 'https://www.ovhcloud.com/it/public-cloud/block-storage', + productName: 'BLOCK_STORAGE', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Cloud Archive', + ], + category: 'Storage and Backup', + description: + 'Archive your data over the long term in a cloud storage space, accessible via standard protocols.', + id: 20375, + lang: 'en_GB', + name: 'Cloud Archive', + order: 'https://www.ovhcloud.com/it/public-cloud/cloud-archive', + productName: 'CLOUD_ARCHIVE', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Data Analytics', + 'Data Processing', + ], + category: 'Data Analytics', + description: 'Launch your Apache Spark processing tasks quickly and easily', + featureAvailability: 'data-processing', + id: 20379, + lang: 'en_GB', + name: 'Data Processing', + order: 'https://www.ovhcloud.com/it/public-cloud/data-processing', + productName: 'DATA_PROCESSING', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Email solutions', + 'Email Pro', + ], + category: 'Email solutions', + description: + 'As the most comprehensive and widely-used business email solution on the market, Exchange offers many features — including contact, folder and calendar sharing, and email syncing.', + featureAvailability: 'email-pro', + id: 20385, + lang: 'en_GB', + name: 'Email Pro', + order: 'https://www.ovh.it/emails/email-pro/', + productName: 'EMAIL_PRO', + regions: ['EU'], + regionTags: ['EU'], + universe: 'Web Hosting & Domains', + url: 'https://ovhcloud.com/en-gb/emails/email-pro/', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Instance Backup', + ], + category: 'Storage and Backup', + description: 'Get a backup service for your instances', + id: 20390, + lang: 'en_GB', + name: 'Instance Backup', + order: 'https://www.ovhcloud.com/it/public-cloud/instance-backup', + productName: 'INSTANCES_BACKUP', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Collaborative Solutions', + 'Microsoft 365', + ], + category: 'Collaborative Solutions', + description: + 'Securely access and edit your documents anytime, anywhere, so you can work however you work best. ', + featureAvailability: 'office', + id: 20394, + lang: 'en_GB', + name: 'Microsoft 365', + order: 'https://www.ovh.it/office-365/', + productName: 'LICENSE_OFFICE', + regionTags: ['EU'], + universe: 'Web Hosting & Domains', + url: 'https://ovhcloud.com/en-gb/collaborative-tools/microsoft-365/', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Containers and orchestration', + 'Managed Kubernetes Service', + ], + category: 'Containers and orchestration', + description: + 'Orchestrate your containerised applications with a CNCF-certified Kubernetes cluster', + featureAvailability: 'kubernetes', + id: 20398, + lang: 'en_GB', + name: 'Managed Kubernetes Service', + order: 'https://www.ovhcloud.com/it/public-cloud/kubernetes', + productName: 'MANAGED_KUBERNETES', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Containers and orchestration', + 'Managed Private Registry', + ], + category: 'Containers and orchestration', + description: + 'Manage a repository for your software building blocks, in the form of Docker images or Helm charts', + id: 20399, + lang: 'en_GB', + name: 'Managed Private Registry', + order: 'https://www.ovhcloud.com/it/public-cloud/managed-private-registry', + productName: 'MANAGED_PRIVATE_REGISTRY', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Object Storage', + ], + category: 'Storage and Backup', + description: 'Enjoy unlimited on-demand storage, accessible via S3 API.', + id: 20407, + lang: 'en_GB', + name: 'Object Storage', + order: 'https://www.ovhcloud.com/it/public-cloud/object-storage', + productName: 'OBJECT_STORAGE', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Platform', + 'VMware', + 'VMware on OVHcloud', + ], + category: 'Platform', + description: + 'VMware on OVHcloud is a unique solution on the market, offering cloud scalability on a 100% dedicated hardware infrastructure. Your infrastructure virtualisation is powered by VMware technology, and entirely managed by OVHcloud. ', + featureAvailability: 'dedicated-cloud:order', + highlight: true, + id: 20414, + lang: 'en_GB', + name: 'VMware on OVHcloud', + order: + 'https://www.ovhcloud.com/it/enterprise/products/hosted-private-cloud/prices/', + productName: 'PRIVATE_CLOUD', + regionTags: ['EU', 'US', 'CA'], + universe: 'Hosted Private Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Databases', + 'Private SQL Databases', + ], + category: 'Databases', + description: + 'Works perfectly alongside all of our OVHcloud products, e.g. VPS, dedicated servers, Public Cloud instances and Cloud Web hosting', + featureAvailability: 'private-database', + id: 20415, + lang: 'en_GB', + name: 'Webcloud Databases', + order: 'https://www.ovh.it/hosting-web/opzioni-sql.xml', + productName: 'PRIVATE_DATABASE', + regionTags: ['EU', 'CA'], + universe: 'Web Hosting & Domains', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Web Hosting options', + 'Shared Content Delivery Network (CDN)', + ], + category: 'Web Hosting options', + description: + 'Optimise your website traffic with the Shared CDN option for an optimal user experience', + featureAvailability: 'hosting:shared-cdn', + id: 20418, + lang: 'en_GB', + name: 'Shared Content Delivery Network (CDN)', + order: 'https://www.ovh.it/hosting-web/cdn.xml', + productName: 'SHARED_CDN', + regionTags: ['EU', 'CA'], + universe: 'Web Hosting & Domains', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Web Hosting options', + 'Visibility Pro', + ], + category: 'Web Hosting options', + description: + 'Get your business on the map, wherever your customers are. Receive alerts, and reply quickly to customer reviews. Save time with an easy-to-use, centralised management interface.', + id: 20425, + lang: 'en_GB', + name: 'Visibility Pro', + productName: 'VISIBILITY_PRO', + regionTags: ['EU'], + universe: 'Web Hosting & Domains', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Volume Snapshot', + ], + category: 'Storage and Backup', + description: 'Trigger a snapshot on your Block Storage volumes', + id: 20426, + lang: 'en_GB', + name: 'Volume Snapshot', + order: 'https://www.ovhcloud.com/it/public-cloud/volume-snapshot', + productName: 'VOLUME_SNAPSHOT', + regionTags: ['EU', 'US', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Web Hosting & Domains', + 'Web PaaS', + 'Web PaaS Powered by Platform.sh', + ], + category: 'Web PaaS', + description: + 'A development platform designed for developers and their teams to design, deploy and run web applications.', + featureAvailability: 'web-paas', + id: 20430, + lang: 'en_GB', + name: 'Web PaaS Powered by Platform.sh', + order: 'https://www.ovh.com/manager/#/web/paas/webpaas/new', + productName: 'WEB_PAAS', + regionTags: ['EU'], + universe: 'Web Hosting & Domains', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Containers and orchestration', + 'Workflow Management', + ], + category: 'Containers and orchestration', + description: + 'Automate your tasks to operate cloud resources based on your business logic, and adapt them to suit any situation', + id: 20431, + lang: 'en_GB', + name: 'Workflow Management', + order: 'https://www.ovhcloud.com/it/public-cloud/orchestration', + productName: 'WORKFLOW_MANAGEMENT', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Platform', + 'Nutanix on OVHcloud', + ], + category: 'Platform', + description: + 'The Nutanix on OVHcloud solution combines Nutanix Cloud Platform software licences with dedicated, Nutanix-qualified OVHcloud Hosted Private Cloud infrastructure — so you can pre-deploy a Nutanix hyperconverged environment (HCI) in just a few hours.', + excludeSubsidiaries: ['ASIA', 'AU', 'SG'], + featureAvailability: 'nutanix', + id: 20447, + lang: 'en_GB', + name: 'Nutanix on OVHcloud', + productName: 'NUTANIX', + regionTags: ['EU', 'CA', 'US'], + universe: 'Hosted Private Cloud', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Public Cloud', 'Compute'], + category: 'Compute', + description: + 'Deploy an option from our range of instances, and harness the flexibility of the cloud to grow in a way that suits your needs.', + id: 20449, + lang: 'en_GB', + name: 'Compute', + order: 'https://www.ovhcloud.com/it/public-cloud/', + productName: 'PCI_INSTANCES', + regionTags: ['EU'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Public Cloud', 'Storage and Backup', 'Databases'], + category: 'Storage and Backup', + description: + 'Harness the power of your data by maintaining control of your resources.', + featureAvailability: 'databases', + id: 20466, + lang: 'en_GB', + name: 'Databases', + order: 'https://www.ovhcloud.com/it/public-cloud/databases', + productName: 'DATABASES', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'AI & machine learning', + 'AI Notebooks', + ], + category: 'AI & machine learning', + description: + 'Get a quick, simple start launching your Jupyter or VS Code notebooks in the cloud', + featureAvailability: 'notebooks', + id: 20470, + lang: 'en_GB', + name: 'AI Notebooks', + order: 'https://www.ovhcloud.com/it/public-cloud/ai-notebooks', + productName: 'NOTEBOOKS', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'AI & machine learning', + 'AI Training', + ], + category: 'AI & machine learning', + description: + 'Train your AI, machine learning and deep learning models efficiently and easily, and optimise your GPU usage.', + id: 20473, + lang: 'en_GB', + name: 'AI Training', + order: 'https://www.ovhcloud.com/it/public-cloud/ai-training/', + productName: 'AI_TRAINING', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Management Interfaces', + 'Horizon', + ], + category: 'Management Interfaces', + description: + 'Use the original OpenStack web interface to effortlessly manage your cloud resources', + id: 20633, + lang: 'en_GB', + name: 'Horizon', + order: 'https://docs.ovh.com/it/public-cloud/horizon/', + productName: 'HORIZON', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Dedicated Servers'], + category: 'Dedicated Servers', + description: + 'Choose the best infrastructure for your business applications. We offer different ranges of highly efficient dedicated servers, adapted to the most demanding needs of all companies.', + featureAvailability: 'dedicated-server:order', + highlight: true, + id: 20804, + lang: 'en_GB', + name: 'Dedicated Servers', + order: 'https://www.ovhcloud.com/it/bare-metal/prices/', + productName: 'DEDICATED_SERVER', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/bare-metal', + }, + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Virtual Private Servers'], + category: 'Virtual Private Servers', + description: + 'A virtual private server, also referred to as a VPS, is a virtual dedicated server. Unlike web hosting — (also known as shared hosting), where the technical aspects are managed by OVHcloud — you are the administrator of your VPS, and fully manage it.', + featureAvailability: 'vps', + highlight: true, + id: 20817, + lang: 'en_GB', + name: 'Virtual Private Servers', + order: 'https://www.ovh.it/vps/', + productName: 'VPS', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/vps', + }, + { + categories: ['Catalogs', 'Bare Metal Cloud', 'Managed Bare Metal'], + category: 'Managed Bare Metal', + description: + 'Your scalable dedicated cloud, hosted and monitored by OVHcloud, available in just 90 minutes. With the Essentials range, opt for highly available VMware virtualisation, and stay focused on your business.', + featureAvailability: 'managed-bare-metal', + highlight: true, + id: 20820, + lang: 'en_GB', + name: 'Managed Bare Metal', + order: 'https://www.ovhcloud.com/it/managed-bare-metal/', + productName: 'ESSENTIALS', + regionTags: ['EU', 'US', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en-gb/managed-bare-metal/', + }, + { + categories: [ + 'Catalogs', + 'Bare Metal Cloud', + 'Storage and Backup', + 'Enterprise File Storage', + ], + category: 'Storage and Backup', + description: + 'Enterprise File Storage, powered by NetApp. Connect of all your OVHcloud solutions to a high-performance file storage service for your most critical business applications.', + featureAvailability: 'netapp', + highlight: true, + id: 20825, + lang: 'en_GB', + name: 'Enterprise File Storage', + order: 'https://www.ovh.com/manager/#/dedicated/netapp/new', + productName: 'NETAPP', + regionTags: ['EU', 'CA'], + universe: 'Bare Metal Cloud', + url: + 'https://ovhcloud.com/en-gb/storage-solutions/enterprise-file-storage/', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'Floating IP'], + category: 'Network Services', + description: + 'Assign and move public and flexible IPs between Public Cloud instances and network features (e.g. LBaaS, Gateway).', + featureAvailability: 'additional-ips', + id: 20836, + lang: 'en_GB', + name: 'Floating IP', + order: 'https://www.ovhcloud.com/it/public-cloud/floating-ip/', + productName: 'FLOATING_IP', + regionTags: ['EU'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Network', + 'Network Services', + 'OVHcloud Load Balancer', + ], + category: 'Network Services', + description: + 'Scale to meet your demand with any of our cloud solutions, in any of our datacentres. With the OVHcloud Load Balancer, you can balance the load between services spread across multiple datacentres.', + featureAvailability: 'ip-load-balancer', + id: 20837, + lang: 'en_GB', + name: 'OVHcloud Load Balancer', + order: 'https://www.ovh.it/soluzioni/load-balancer/', + productName: 'IP_LOAD_BALANCER', + regionTags: ['EU', 'CA'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'vRack'], + category: 'Network Services', + description: + 'With vRack (virtual rack) technology, you can connect, isolate and distribute OVHcloud products within one or more private networks, if they are compatible.', + id: 20838, + lang: 'en_GB', + name: 'vRack', + order: + "https://www.ovh.it/order/express/#/new/express/resume?products=~(~(planCode~'vrack~quantity~1~productId~'vrack))", + productName: 'VRACK', + regionTags: ['EU', 'US', 'CA'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'OVHcloud Connect'], + category: 'Network Services', + description: + 'With our OVHcloud Connect hybrid connection solution, you can form a secure, high-performance link between your company network and the OVHcloud vRack.', + featureAvailability: 'cloud-connect', + id: 20839, + lang: 'en_GB', + name: 'OVHcloud Connect', + productName: 'OVH_CLOUD_CONNECT', + regionTags: ['EU', 'CA'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Bare Metal Cloud', + 'Storage and Backup', + 'Veeam Cloud Connect', + ], + category: 'Storage and Backup', + description: + 'Easily and automatically outsource backups in the OVHcloud secure cloud. Using the Veeam Availability Suite®, you can ensure that your data is replicated in our datacentres.', + featureAvailability: 'veeam-cloud-connect', + id: 20847, + lang: 'en_GB', + name: 'Veeam Cloud Connect', + productName: 'VEEAM_CLOUD_CONNECT', + regionTags: ['EU', 'CA'], + universe: 'Bare Metal Cloud', + url: 'https://ovhcloud.com/en/storage-solutions/veeam-cloud-connect', + }, + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Storage and Backup', + 'Veeam Enterprise', + ], + category: 'Storage and Backup', + description: + 'Unlock the power of Veeam Backup & Replication. With OVHcloud, you can enjoy total freedom in terms of how you configure your backups once you have deployed your solution.', + featureAvailability: 'veeam-enterprise:order', + id: 20854, + lang: 'en_GB', + name: 'Veeam Enterprise', + order: 'https://www.ovh.it/storage-solutions/veeam-enterprise.xml', + productName: 'VEEAM_ENTERPRISE', + regionTags: ['EU', 'CA'], + universe: 'Hosted Private Cloud', + url: 'https://ovhcloud.com/en-gb/storage-solutions/veeam-enterprise', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Data Analytics', + 'Logs Data Platform', + ], + category: 'Data Analytics', + description: + 'Index and analyse logs in real time. Receive alerts if anything stops working. Share your data with your employees.', + id: 20872, + lang: 'en_GB', + name: 'Logs Data Platform', + order: 'https://www.ovhcloud.com/it/public-cloud/big-data-hadoop/', + productName: 'ANALYTICS_DATA_PLATFORM', + regionTags: ['EU'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'Private Network'], + category: 'Network Services', + description: + 'Deploy private networks to implement virtual switches that can hot-connect your project instances in real time, without any service interruptions.', + id: 20895, + lang: 'en_GB', + name: 'Private Network', + order: 'https://www.ovhcloud.com/it/public-cloud/private-network', + productName: 'PRIVATE_NETWORK', + regionTags: ['EU'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'Gateway'], + category: 'Network Services', + description: + 'Connect your private instances to the internet securely and use floating IPs to expose your private network services flexibly on the web.', + featureAvailability: 'public-gateways', + id: 20934, + lang: 'en_GB', + name: 'Gateway', + order: 'https://www.ovhcloud.com/it/public-cloud/gateway/', + productName: 'GATEWAYS', + regionTags: ['EU'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Cold Archive', + ], + category: 'Storage and Backup', + description: + 'Get our most cost-effective storage type for long-term data archiving on a tape storage infrastructure.', + id: 21197, + lang: 'en_GB', + name: 'Cold Archive', + order: 'https://www.ovhcloud.com/it/public-cloud/cold-archive/', + productName: 'COLD_ARCHIVE', + regionTags: ['EU'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Storage and Backup', + 'Volume Backup', + ], + category: 'Storage and Backup', + description: + 'Back up your Block Storage volumes. The backed-up data is stored on our Object Storage service.', + id: 21227, + lang: 'en_GB', + name: 'Volume Backup', + order: 'https://www.ovhcloud.com/it/public-cloud/volume-backup/', + productName: 'VOLUME_BACKUP', + regionTags: ['EU', 'CA', 'US'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'AI & machine learning', + 'AI Deploy', + ], + category: 'AI & machine learning', + description: + 'Easily deploy machine learning models and applications to production, create your API access points effortlessly, and make effective predictions.', + id: 21429, + lang: 'en_GB', + name: 'AI Deploy', + order: 'https://www.ovhcloud.com/it/public-cloud/ai-deploy/', + productName: 'AI_DEPLOY', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://www.ovhcloud.com/en-gb/public-cloud/ai-deploy/', + }, + { + categories: [ + 'Catalogs', + 'Hosted Private Cloud', + 'Platform', + 'SAP HANA on Private Cloud', + ], + category: 'Platform', + description: + 'This new platform combines SAP HANA-certified HCI servers with our VMware on OVHcloud infrastructure. It enables secure hosting and effortless deployment of your most critical SAP environments in a sovereign cloud.', + featureAvailability: 'dedicated-cloud:sapHanaOrder', + highlight: true, + id: 21725, + lang: 'en_GB', + name: 'SAP HANA on Private Cloud', + order: 'https://www.ovhcloud.com/it/hosted-private-cloud/sap-hana/', + productName: 'SAP_HANA', + regionTags: ['EU', 'CA'], + universe: 'Hosted Private Cloud', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Containers and orchestration', + 'Kubernetes Load Balancer', + ], + category: 'Containers and orchestration', + description: + 'Automatic distribution of traffic on the Managed Kubernetes Service', + featureAvailability: 'kubernetes', + id: 21763, + lang: 'en_GB', + name: 'Kubernetes Load Balancer', + order: 'https://www.ovhcloud.com/it/public-cloud/kubernetes', + productName: 'KUBERNETES_LOAD_BALANCER', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://mockup.url.com', + }, + { + categories: ['Catalogs', 'Network', 'Network Services', 'Load Balancer'], + category: 'Network Services', + description: 'Automatic traffic distribution on Public Cloud resources', + featureAvailability: 'octavia-load-balancer', + id: 21764, + lang: 'en_GB', + name: 'Load Balancer', + order: 'https://www.ovhcloud.com/it/public-cloud/private-network', + productName: 'OCTAVIA_LOAD_BALANCER', + regionTags: ['EU', 'CA'], + universe: 'Network', + url: 'https://mockup.url.com', + }, + { + categories: [ + 'Catalogs', + 'Public Cloud', + 'Containers and orchestration', + 'Containers and orchestration', + ], + category: 'Containers and orchestration', + description: + 'Seamlessly deploy and manage your containerised applications in a multi-cluster Kubernetes environment, using a multi-cloud or hybrid cloud approach.', + featureAvailability: 'pci-rancher', + id: 100001, + lang: 'en_GB', + name: 'Managed Rancher Service', + order: 'https://www.ovhcloud.com/it/public-cloud/managed-rancher-service', + productName: 'MANAGED_RANCHER_SERVICE', + regionTags: ['EU', 'CA'], + universe: 'Public Cloud', + url: 'https://www.ovhcloud.com/en-gb/public-cloud/managed-rancher-service/', + }, +]; diff --git a/packages/manager/apps/hub-react/src/_mock_/products.ts b/packages/manager/apps/hub-react/src/_mock_/products.ts new file mode 100644 index 000000000000..9f85e3aae862 --- /dev/null +++ b/packages/manager/apps/hub-react/src/_mock_/products.ts @@ -0,0 +1,1983 @@ +import { ProductList, HubProduct } from '@/types/services.type'; + +export const aFewProductsMocked: ProductList = { + count: 1, + data: { + CDN_DEDICATED: { + count: 1, + data: [ + { + propertyId: 'service', + resource: { + displayName: 'displayName1', + name: 'name1', + product: { + description: 'description1', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 111111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/displayName1', + }, + ], + }, + }, +}; + +export const lotsOfProductsMocked: ProductList = { + count: 32, + data: { + CDN_DEDICATED: { + count: 3, + data: [ + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_1', + name: 'cdn_dedicated_1', + product: { + description: 'CDN description 1', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_1', + }, + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_2', + name: 'cdn_dedicated_2', + product: { + description: 'CDN description 2', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_2', + }, + { + propertyId: 'service', + resource: { + displayName: '', + name: 'cdn_dedicated_3', + product: { + description: 'CDN description 3', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_3', + }, + ], + }, + CLOUD_PROJECT: { + count: 8, + data: [ + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 1', + name: 'fakeName1', + product: { + description: 'PCI Project description', + name: 'pci-project-1', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 11111111, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/fakeName1', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 2', + name: 'pci-project-2', + product: { + description: 'PCI Project description', + name: 'pci-project-2', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 22222222, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-2', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 3', + name: 'pci-project-3', + product: { + description: 'PCI Project description', + name: 'pci-project-3', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 33333333, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-3', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 4', + name: 'pci-project-4', + product: { + description: 'PCI Project description', + name: 'pci-project-4', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 44444444, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-4', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 5', + name: 'pci-project-5', + product: { + description: 'PCI Project description', + name: 'pci-project-5', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 55555555, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-5', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 6', + name: 'pci-project-6', + product: { + description: 'PCI Project description', + name: 'pci-project-6', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 66666666, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-6', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 7', + name: 'pci-project-7', + product: { + description: 'PCI Project description', + name: 'pci-project-7', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 77777777, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-7', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 8', + name: 'pci-project-8', + product: { + description: 'PCI Project description', + name: 'pci-project-8', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 88888888, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-8', + }, + ], + }, + DBAAS_LOGS: { + count: 5, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-1', + name: 'ldp-1', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-1/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-2', + name: 'ldp-2', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-2/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-3', + name: 'ldp-3', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-3/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-4', + name: 'ldp-4', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 4444444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-4/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-5', + name: 'ldp-5', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 5555555, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-5/home', + }, + ], + }, + DEDICATED_CLOUD: { + count: 9, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-1', + name: 'pcc-1', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 111, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-1', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-2', + name: 'pcc-2', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 222, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-2', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-3', + name: 'pcc-3', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 333, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-3', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-4', + name: 'pcc-4', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 444, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-4', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-5', + name: 'pcc-5', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 555, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-5', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-6', + name: 'pcc-6', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 666, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-6', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-7', + name: 'pcc-7', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 777, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-7', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-8', + name: 'pcc-8', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 888, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-8', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-9', + name: 'pcc-9', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 999, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-9', + }, + ], + }, + DEDICATED_HOUSING: { + count: 1, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'dedicated_housing_1', + name: 'dedicated_housing_1', + resellingProvider: null, + state: 'toSuspend', + }, + route: { + path: '/dedicated/housing/{serviceName}', + }, + serviceId: 1, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/housing/dedicated_housing_1', + }, + ], + }, + DEDICATED_NASHA: { + count: 2, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_1', + name: 'nasha_1', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 11, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_1/partitions', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_2', + name: 'nasha_2', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 22, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_2/partitions', + }, + ], + }, + DEDICATED_SERVER: { + count: 4, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_1', + name: 'dedicated_server_1', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 11111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_1', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_2', + name: 'dedicated_server_2', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 22222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_2', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_3', + name: 'dedicated_server_3', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 33333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_3', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_4', + name: 'dedicated_server_4', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 44444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_4', + }, + ], + }, + }, +}; + +export const lotsOfProductsParsedMocked: HubProduct[] = [ + { + count: 9, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-1', + name: 'pcc-1', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 111, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-1', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-2', + name: 'pcc-2', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 222, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-2', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-3', + name: 'pcc-3', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 333, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-3', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-4', + name: 'pcc-4', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 444, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-4', + }, + ], + type: 'DEDICATED_CLOUD', + formattedType: 'dedicated-cloud', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 8, + data: [ + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 1', + name: 'fakeName1', + product: { + description: 'PCI Project description', + name: 'pci-project-1', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 11111111, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/fakeName1', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 2', + name: 'pci-project-2', + product: { + description: 'PCI Project description', + name: 'pci-project-2', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 22222222, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-2', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 3', + name: 'pci-project-3', + product: { + description: 'PCI Project description', + name: 'pci-project-3', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 33333333, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-3', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 4', + name: 'pci-project-4', + product: { + description: 'PCI Project description', + name: 'pci-project-4', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 44444444, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-4', + }, + ], + type: 'CLOUD_PROJECT', + formattedType: 'cloud-project', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 5, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-1', + name: 'ldp-1', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-1/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-2', + name: 'ldp-2', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-2/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-3', + name: 'ldp-3', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-3/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-4', + name: 'ldp-4', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 4444444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-4/home', + }, + ], + type: 'DBAAS_LOGS', + formattedType: 'dbaas-logs', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 4, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_1', + name: 'dedicated_server_1', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 11111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_1', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_2', + name: 'dedicated_server_2', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 22222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_2', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_3', + name: 'dedicated_server_3', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 33333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_3', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_4', + name: 'dedicated_server_4', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 44444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_4', + }, + ], + type: 'DEDICATED_SERVER', + formattedType: 'dedicated-server', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 3, + data: [ + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_1', + name: 'cdn_dedicated_1', + product: { + description: 'CDN description 1', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_1', + }, + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_2', + name: 'cdn_dedicated_2', + product: { + description: 'CDN description 2', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_2', + }, + { + propertyId: 'service', + resource: { + displayName: '', + name: 'cdn_dedicated_3', + product: { + description: 'CDN description 3', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_3', + }, + ], + type: 'CDN_DEDICATED', + formattedType: 'cdn-dedicated', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 2, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_1', + name: 'nasha_1', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 11, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_1/partitions', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_2', + name: 'nasha_2', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 22, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_2/partitions', + }, + ], + type: 'DEDICATED_NASHA', + formattedType: 'dedicated-nasha', + link: Promise.resolve('https://fake-link.com'), + }, +]; + +export const lotsOfProductsParsedExpandedMocked: HubProduct[] = [ + { + count: 9, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-1', + name: 'pcc-1', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 111, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-1', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-2', + name: 'pcc-2', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 222, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-2', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-3', + name: 'pcc-3', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 333, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-3', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'pcc-4', + name: 'pcc-4', + product: { + description: 'Dedicated Cloud Hypervisor', + name: 'pcc-hypervisor', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicatedCloud/{serviceName}', + }, + serviceId: 444, + serviceType: 'DEDICATED_CLOUD', + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/dedicated_cloud/pcc-4', + }, + ], + type: 'DEDICATED_CLOUD', + formattedType: 'dedicated-cloud', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 8, + data: [ + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 1', + name: 'fakeName1', + product: { + description: 'PCI Project description', + name: 'pci-project-1', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 11111111, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/fakeName1', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 2', + name: 'pci-project-2', + product: { + description: 'PCI Project description', + name: 'pci-project-2', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 22222222, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-2', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 3', + name: 'pci-project-3', + product: { + description: 'PCI Project description', + name: 'pci-project-3', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 33333333, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-3', + }, + { + propertyId: 'project_id', + resource: { + displayName: 'PCI Project 4', + name: 'pci-project-4', + product: { + description: 'PCI Project description', + name: 'pci-project-4', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cloud/project/{serviceName}', + }, + serviceId: 44444444, + universe: { + CA: 'public-cloud', + EU: 'public-cloud', + US: 'public-cloud', + }, + url: + 'https://www.ovh.com/manager/#/public-cloud/pci/projects/pci-project-4', + }, + ], + type: 'CLOUD_PROJECT', + formattedType: 'cloud-project', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 5, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-1', + name: 'ldp-1', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-1/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-2', + name: 'ldp-2', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-2/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-3', + name: 'ldp-3', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-3/home', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'LDP-4', + name: 'ldp-4', + product: { + description: 'Logs - Account', + name: 'logs-account', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dbaas/logs/{serviceName}', + }, + serviceId: 4444444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: 'https://www.ovh.com/manager/#/dedicated/dbaas/logs/ldp-4/home', + }, + ], + type: 'DBAAS_LOGS', + formattedType: 'dbaas-logs', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 4, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_1', + name: 'dedicated_server_1', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 11111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_1', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_2', + name: 'dedicated_server_2', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 22222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_2', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_3', + name: 'dedicated_server_3', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 33333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_3', + }, + { + propertyId: 'name', + resource: { + displayName: 'dedicated_server_4', + name: 'dedicated_server_4', + product: { + description: 'Dedicated server description', + name: 'Dedicated server name', + }, + resellingProvider: 'ovh.ca', + state: 'active', + }, + route: { + path: '/dedicated/server/{serviceName}', + }, + serviceId: 44444, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/server/dedicated_server_4', + }, + ], + type: 'DEDICATED_SERVER', + formattedType: 'dedicated-server', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 3, + data: [ + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_1', + name: 'cdn_dedicated_1', + product: { + description: 'CDN description 1', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 1111111, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_1', + }, + { + propertyId: 'service', + resource: { + displayName: 'cdn_dedicated_2', + name: 'cdn_dedicated_2', + product: { + description: 'CDN description 2', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 2222222, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_2', + }, + { + propertyId: 'service', + resource: { + displayName: '', + name: 'cdn_dedicated_3', + product: { + description: 'CDN description 3', + name: 'infrastructure', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/cdn/dedicated/{serviceName}', + }, + serviceId: 3333333, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/cdn/cdn_dedicated_3', + }, + ], + type: 'CDN_DEDICATED', + formattedType: 'cdn-dedicated', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 2, + data: [ + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_1', + name: 'nasha_1', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 11, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_1/partitions', + }, + { + propertyId: 'serviceName', + resource: { + displayName: 'nasha_2', + name: 'nasha_2', + product: { + description: 'NAS HA Description', + name: 'nas-ha-name', + }, + resellingProvider: null, + state: 'active', + }, + route: { + path: '/dedicated/nasha/{serviceName}', + }, + serviceId: 22, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/paas/nasha/nasha_2/partitions', + }, + ], + type: 'DEDICATED_NASHA', + formattedType: 'dedicated-nasha', + link: Promise.resolve('https://fake-link.com'), + }, + { + count: 1, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'dedicated_housing_1', + name: 'dedicated_housing_1', + resellingProvider: null, + state: 'toSuspend', + }, + route: { + path: '/dedicated/housing/{serviceName}', + }, + serviceId: 1, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/housing/dedicated_housing_1', + }, + ], + type: 'DEDICATED_HOUSING', + formattedType: 'dedicated-housing', + link: Promise.resolve('https://fake-link.com'), + }, +]; diff --git a/packages/manager/apps/hub-react/src/billing.constants.ts b/packages/manager/apps/hub-react/src/billing.constants.ts new file mode 100644 index 000000000000..a7928fd84544 --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing.constants.ts @@ -0,0 +1,41 @@ +export const SERVICE_TYPE = { + EMAIL_DOMAIN: 'EMAIL_DOMAIN', + ENTERPRISE_CLOUD_DATABASE: 'ENTERPRISE_CLOUD_DATABASE', + EXCHANGE: 'EMAIL_EXCHANGE', + HOSTING_PRIVATE_DATABASE: 'HOSTING_PRIVATE_DATABASE', + HOSTING_WEB: 'HOSTING_WEB', + OVH_CLOUD_CONNECT: 'OVH_CLOUD_CONNECT', + PACK_XDSL: 'PACK_XDSL', + SMS: 'SMS', + TELEPHONY: 'TELEPHONY', + WEBCOACH: 'WEBCOACH', + ALL_DOM: 'ALL_DOM', + OKMS: 'OKMS_RESOURCE', + VRACK_SERVICES: 'VRACK_SERVICES_RESOURCE', +}; + +export const RENEW_URL: Record = { + default: '/cgi-bin/order/renew.cgi?domainChooser=', + AU: 'https://ca.ovh.com/au/cgi-bin/order/renew.cgi?domainChooser=', + CA: 'https://ca.ovh.com/fr/cgi-bin/order/renew.cgi?domainChooser=', + CZ: 'https://www.ovh.cz/cgi-bin/order/renew.cgi?domainChooser=', + DE: 'https://www.ovh.de/cgi-bin/order/renew.cgi?domainChooser=', + EN: 'https://www.ovh.co.uk/cgi-bin/order/renew.cgi?domainChooser=', + ES: 'https://www.ovh.es/cgi-bin/order/renew.cgi?domainChooser=', + FI: 'https://www.ovh-hosting.fi/cgi-bin/order/renew.cgi?domainChooser=', + FR: 'https://eu.ovh.com/fr/cgi-bin/order/renew.cgi?domainChooser=', + GB: 'https://www.ovh.co.uk/cgi-bin/order/renew.cgi?domainChooser=', + IE: 'https://www.ovh.ie/cgi-bin/order/renew.cgi?domainChooser=', + IT: 'https://www.ovh.it/cgi-bin/order/renew.cgi?domainChooser=', + LT: 'https://www.ovh.lt/cgi-bin/order/renew.cgi?domainChooser=', + MA: 'https://www.ovh.com/ma/cgi-bin/order/renew.cgi?domainChooser=', + NL: 'https://www.ovh.nl/cgi-bin/order/renew.cgi?domainChooser=', + PL: 'https://www.ovh.pl/cgi-bin/order/renew.cgi?domainChooser=', + PT: 'https://www.ovh.pt/cgi-bin/order/renew.cgi?domainChooser=', + QC: 'https://ca.ovh.com/fr/cgi-bin/order/renew.cgi?domainChooser=', + RU: 'https://www.ovh.co.uk/cgi-bin/order/renew.cgi?domainChooser=', + SG: 'https://ca.ovh.com/sg/cgi-bin/order/renew.cgi?domainChooser=', + SN: 'https://www.ovh.sn/cgi-bin/order/renew.cgi?domainChooser=', + TN: 'https://www.ovh.com/tn/cgi-bin/order/renew.cgi?domainChooser=', + WE: 'https://ca.ovh.com/fr/cgi-bin/order/renew.cgi?domainChooser=', +}; diff --git a/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.component.tsx b/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.component.tsx new file mode 100644 index 000000000000..065449484fa9 --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.component.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from 'react-i18next'; +import { ODS_CHIP_SIZE } from '@ovhcloud/ods-components'; +import { OsdsChip, OsdsText } from '@ovhcloud/ods-components/react'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { BADGE_INTENT_BY_STATUS } from '@/billing/components/billing-status/BillingStatus.constants'; +import { BillingService } from '@/billing/types/billingServices.type'; + +type BillingStatusProps = { + service: BillingService; +}; + +export default function BillingStatus({ service }: BillingStatusProps) { + const { t } = useTranslation('billing/status'); + const shouldHideAutoRenewStatus = + service.isOneShot() || ['SMS'].includes(service.serviceType); + return ( +
+ {service.hasDebt() && ( + + + {t('manager_billing_service_status_pending_debt')} + + + )} + {shouldHideAutoRenewStatus && !service.isResiliated() && -} + {shouldHideAutoRenewStatus && service.isResiliated() && ( + + + {t('manager_billing_service_status_expired')} + + + )} + {!service.hasDebt() && !shouldHideAutoRenewStatus && ( + + + {t(`manager_billing_service_status_${service.getRenew()}`)} + + + )} +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.constants.ts b/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.constants.ts new file mode 100644 index 000000000000..b28fcfd3c7b8 --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/components/billing-status/BillingStatus.constants.ts @@ -0,0 +1,15 @@ +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; + +export const BADGE_INTENT_BY_STATUS: Record< + string, + keyof typeof ODS_THEME_COLOR_INTENT +> = { + auto: 'success', + automatic: 'success', + billing_suspended: 'info', + delete_at_expiration: 'error', + expired: 'error', + forced_manual: 'info', + manual: 'warning', + manualPayment: 'warning', +}; diff --git a/packages/manager/apps/hub-react/src/billing/components/services-actions/ServicesActions.component.tsx b/packages/manager/apps/hub-react/src/billing/components/services-actions/ServicesActions.component.tsx new file mode 100644 index 000000000000..a861904bf09f --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/components/services-actions/ServicesActions.component.tsx @@ -0,0 +1,108 @@ +import { + ODS_BUTTON_SIZE, + ODS_BUTTON_TYPE, + ODS_BUTTON_VARIANT, + ODS_DIVIDER_SIZE, + ODS_ICON_NAME, + ODS_ICON_SIZE, +} from '@ovhcloud/ods-components'; +import { + OsdsButton, + OsdsDivider, + OsdsIcon, + OsdsLink, + OsdsPopover, + OsdsPopoverContent, + OsdsSkeleton, +} from '@ovhcloud/ods-components/react'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import React, { Suspense } from 'react'; +import { + ServiceAction, + useServiceActions, +} from '@/billing/hooks/useServiceActions'; +import { BillingService } from '@/billing/types/billingServices.type'; +import { useServiceLinks } from '@/billing/hooks/useServiceLinks'; + +type ServicesActionsProps = { + service: BillingService; + autoRenewLink: string; + trackingPrefix: string[]; +}; + +export default function ServicesActions({ + service, + autoRenewLink, + trackingPrefix, +}: ServicesActionsProps) { + const links = useServiceLinks(service, autoRenewLink); + const items: ServiceAction[] = useServiceActions( + service, + links, + trackingPrefix, + ); + const shouldBeDisplayed = + Boolean(autoRenewLink) || + service.canBeEngaged || + service.hasPendingEngagement; + + // When we'll migrate to ODS 18, we should try to have the popover "rounded" & "withArrow" and add a direction to it + return shouldBeDisplayed ? ( + + + + + + + + + + + + } + > + {items.map((item, index) => { + const { disabled, external, ...link } = item; + return ( +
+ {index > 0 && } + + {item.label} + {external && ( + + )} + +
+ ); + })} +
+
+
+ ) : null; +} diff --git a/packages/manager/apps/hub-react/src/billing/hooks/useServiceActions.ts b/packages/manager/apps/hub-react/src/billing/hooks/useServiceActions.ts new file mode 100644 index 000000000000..72568edf0d99 --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/hooks/useServiceActions.ts @@ -0,0 +1,223 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { ServiceLinks } from '@/billing/types/service-links.type'; +import { BillingService } from '@/billing/types/billingServices.type'; + +export type ServiceAction = { + color?: ODS_THEME_COLOR_INTENT; + disabled?: boolean; + external?: boolean; + href: string; + label: string; + onClick?: () => void; + target?: OdsHTMLAnchorElementTarget; + rel?: OdsHTMLAnchorElementRel; +}; + +export const useServiceActions = ( + service: BillingService, + getLinksPromise: Promise, + trackingPrefix?: string[], +) => { + const { t } = useTranslation('billing/actions'); + const { trackClick } = useOvhTracking(); + + const trackAction = (hit: string, hasActionInEvent = true): void => { + if (trackingPrefix) { + trackClick({ + actionType: 'action', + actions: [...trackingPrefix, ...[hasActionInEvent && 'action', hit]], + }); + } + }; + const [actions, setActions] = useState([]); + useEffect(() => { + getLinksPromise.then((links: ServiceLinks) => { + const items: ServiceAction[] = []; + + if (links.warnBillingNic) { + items.push({ + label: t('billing_services_actions_menu_pay_bill'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-pay-bill'); + }, + href: links.warnBillingNic, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.payBill) { + items.push({ + label: t('billing_services_actions_menu_pay_bill'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-pay-bill'); + }, + href: links.payBill, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.configureRenewal) { + items.push({ + label: t('billing_services_actions_menu_manage_renew'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-configure-renew'); + }, + href: links.configureRenewal, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.anticipatePayment) { + items.push({ + label: t('billing_services_actions_menu_anticipate_renew'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-anticipate-payment'); + }, + href: links.anticipatePayment, + external: true, + }); + } + if (links.renewManually) { + items.push({ + disabled: service.hasForcedRenew(), + label: t('billing_services_actions_menu_renew'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-renew-manually'); + }, + href: links.renewManually, + external: true, + target: OdsHTMLAnchorElementTarget._blank, + rel: OdsHTMLAnchorElementRel.noopener, + }); + } + if (links.manageCommitment) { + items.push({ + label: t('billing_services_actions_menu_commit'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-manage-commitment', false); + }, + href: links.manageCommitment, + }); + } + if (links.cancelCommitment) { + items.push({ + label: t('billing_services_actions_menu_commit_cancel'), + color: ODS_THEME_COLOR_INTENT.primary, + href: links.cancelCommitment, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.modifyExchangeBilling) { + items.push({ + label: t('billing_services_actions_menu_exchange_update'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-modify-billing-Exchange', false); + }, + href: links.modifyExchangeBilling, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.configureExchangeAccountsRenewal) { + items.push({ + label: t('billing_services_actions_menu_exchange_update_accounts'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-modify-billing-ExchangeAccounts', false); + }, + href: links.configureExchangeAccountsRenewal, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.resiliate) { + items.push({ + label: t( + `billing_services_actions_menu_resiliate${ + service.hasEngagement() ? 'my_engagement' : '' + }`, + ), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-resiliate'); + }, + href: links.resiliate, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.resiliateByDeletion) { + items.push({ + label: t( + `billing_services_actions_menu_resiliate_${service.serviceType}`, + ), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-resiliate'); + }, + href: links.resiliateByDeletion, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.buySMSCredits) { + items.push({ + label: t('billing_services_actions_menu_sms_credit'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-buy-SMScredits'); + }, + href: links.buySMSCredits, + target: OdsHTMLAnchorElementTarget._blank, + rel: OdsHTMLAnchorElementRel.noopener, + external: true, + }); + } + if (links.configureSMSAutoReload) { + items.push({ + label: t('billing_services_actions_menu_sms_renew'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-configure-SMSautoreload'); + }, + href: links.configureSMSAutoReload, + target: OdsHTMLAnchorElementTarget._blank, + rel: OdsHTMLAnchorElementRel.noopener, + external: true, + }); + } + if (links.cancelResiliation) { + items.push({ + label: t('billing_services_actions_menu_resiliate_cancel'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-cancel-resiliation'); + }, + href: links.cancelResiliation, + target: OdsHTMLAnchorElementTarget._top, + }); + } + if (links.seeService) { + items.push({ + label: t('billing_services_actions_menu_see_dashboard'), + color: ODS_THEME_COLOR_INTENT.primary, + onClick: () => { + trackAction('go-to-service'); + }, + href: links.seeService, + target: OdsHTMLAnchorElementTarget._top, + }); + } + setActions(items); + }); + }, [getLinksPromise]); + + return actions; +}; diff --git a/packages/manager/apps/hub-react/src/billing/hooks/useServiceLinks.tsx b/packages/manager/apps/hub-react/src/billing/hooks/useServiceLinks.tsx new file mode 100644 index 000000000000..43634a3a2cee --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/hooks/useServiceLinks.tsx @@ -0,0 +1,187 @@ +import { useContext } from 'react'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import { BillingService } from '@/billing/types/billingServices.type'; +import { ServiceLinks } from '@/billing/types/service-links.type'; +import { RENEW_URL, SERVICE_TYPE } from '@/billing.constants'; + +export const useServiceLinks = async ( + service: BillingService, + autoRenewLink?: string, +) => { + const { + shell: { navigation }, + environment: { user }, + } = useContext(ShellContext); + const links: ServiceLinks = {}; + + const serviceTypeParam = service.serviceType + ? `&serviceType=${service.serviceType}` + : ''; + const renewUrl = `${RENEW_URL[user.ovhSubsidiary] || RENEW_URL.default}${ + service.serviceId + }`; + const [organization, exchangeName] = service.serviceId.split('/service/'); + // When we will fully migrate billing in React, we should add a possibility to give + // the cancelResiliation link (with an additional parameter in useServiceLinks) + const cancelResiliationLink = + service.serviceType !== SERVICE_TYPE.EMAIL_DOMAIN + ? `${autoRenewLink}/cancel-resiliation?serviceId=${service.serviceId}${serviceTypeParam}` + : null; + // When we will fully migrate billing in React, we should add a possibility to give + // the resiliationByEndRule link (with an additional parameter in useServiceLinks) + const resiliationByEndRuleLink = `${autoRenewLink}/resiliation?serviceId=${service.id}&serviceName=${service.serviceId}${serviceTypeParam}`; + + let resiliateLink: string; + switch (service.serviceType) { + case SERVICE_TYPE.EXCHANGE: + resiliateLink = `${service.url}?action=resiliate`; + break; + case SERVICE_TYPE.EMAIL_DOMAIN: + resiliateLink = `${autoRenewLink}/delete-email?serviceId=${service.serviceId}&name=${service.domain}`; + break; + case SERVICE_TYPE.TELEPHONY: + resiliateLink = (await navigation.getURL( + 'telecom', + '#/telephony/:serviceName/administration/deleteGroup', + { serviceName: service.serviceId }, + )) as string; + break; + case SERVICE_TYPE.PACK_XDSL: + resiliateLink = (await navigation.getURL( + 'telecom', + '#/pack/:serviceName', + { serviceName: service.serviceId }, + )) as string; + break; + case SERVICE_TYPE.ALL_DOM: + resiliateLink = service.canResiliateByEndRule() + ? resiliationByEndRuleLink + : `${autoRenewLink}/delete-all-dom?serviceId=${service.serviceId}&serviceType=${service.serviceType}`; + break; + case SERVICE_TYPE.OKMS: + case SERVICE_TYPE.VRACK_SERVICES: + resiliateLink = `${autoRenewLink}/terminate-service?id=${service.id}${serviceTypeParam}`; + break; + default: + resiliateLink = service.canResiliateByEndRule() + ? resiliationByEndRuleLink + : autoRenewLink && + `${autoRenewLink}/delete?serviceId=${service.serviceId}${serviceTypeParam}`; + break; + } + + if ( + autoRenewLink && + service.hasDebt() && + !service.hasBillingRights(user.nichandle) + ) { + links.warnBillingNic = `${autoRenewLink}/warn-nic?nic=${service.contactBilling}`; + } + if (service.hasDebt() && service.hasBillingRights(user.nichandle)) { + links.payBill = (await navigation.getURL( + 'dedicated', + '#/billing/history', + {}, + )) as string; + } + if ( + autoRenewLink && + !service.hasParticularRenew() && + !service.hasPendingResiliation() && + !service.hasDebt() + ) { + if (!service.isOneShot() && service.canHandleRenew()) { + if ( + !service.isResiliated() && + !service.hasForcedRenew() && + !service.hasEngagement() + ) { + links.configureRenewal = `${autoRenewLink}/update?serviceId=${service.serviceId}${serviceTypeParam}`; + } + if ( + !service.hasManualRenew() && + !service.canBeEngaged && + !service.hasPendingEngagement + ) { + links.anticipatePayment = renewUrl; + } + } + if (service.hasManualRenew() && service.canHandleRenew()) { + links.renewManually = renewUrl; + } + } + if (service.hasPendingEngagement) { + // When we will fully migrate billing in React, we should add a possibility to give + // the cancelCommitment link (with an additional parameter in useServiceLinks) + links.cancelCommitment = `${autoRenewLink}/${service.id}/cancel-commitment`; + } else if (service.canBeEngaged && !service.isSuspended()) { + // When we will fully migrate billing in React, we should add a possibility to give + // the manageCommitment link (with an additional parameter in useServiceLinks) + links.manageCommitment = `${autoRenewLink}/${service.id}/commitment`; + } + if (service.serviceType === SERVICE_TYPE.EXCHANGE) { + const exchangeBillingLink = `${autoRenewLink}/exchange?organization=${organization}&exchangeName=${exchangeName || + service.serviceId}`; + if (service.menuItems?.manageEmailAccountsInBilling) { + links.modifyExchangeBilling = exchangeBillingLink; + } else if (service.menuItems?.manageEmailAccountsInExchange) { + links.configureExchangeAccountsRenewal = exchangeBillingLink; + } + } + if (service.serviceType === SERVICE_TYPE.PACK_XDSL) { + if ( + (service.shouldDeleteAtExpiration() || !service.isResiliated()) && + !service.hasDebt() && + !service.hasPendingResiliation() && + resiliateLink && + service.hasAdminRights(user.auth.account) + ) { + links.resiliate = resiliateLink; + } + } else if ( + (service.shouldDeleteAtExpiration() || !service.isResiliated()) && + !service.hasDebt() && + !service.hasPendingResiliation() + ) { + if ( + resiliateLink && + (service.hasAdminRights(user.auth.account) || + service.hasAdminRights(user.nichandle)) + ) { + links.resiliate = resiliateLink; + } + if (autoRenewLink && service.canBeDeleted()) { + links.resiliateByDeletion = + service.serviceType && + `${autoRenewLink}/delete-${service.serviceType + .replace(/_/g, '-') + .toLowerCase()}?serviceId=${service.serviceId}`; + } + } + if (service.serviceType === SERVICE_TYPE.SMS) { + links.buySMSCredits = (await navigation.getURL( + 'telecom', + '#/sms/:serviceName/order', + { serviceName: service.serviceId }, + )) as string; + links.configureSMSAutoReload = (await navigation.getURL( + 'telecom', + '#/sms/:serviceName/options/recredit', + { serviceName: service.serviceId }, + )) as string; + } + if ( + cancelResiliationLink && + (service.canBeUnresiliated(user.nichandle) || + service.canCancelResiliationByEndRule()) + ) { + links.cancelResiliation = cancelResiliationLink; + } + if (service.url && !service.isByoipService()) { + links.seeService = service.url; + } + return links; +}; diff --git a/packages/manager/apps/hub-react/src/billing/types/billingServices.type.ts b/packages/manager/apps/hub-react/src/billing/types/billingServices.type.ts new file mode 100644 index 000000000000..4b8899ffa6ce --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/types/billingServices.type.ts @@ -0,0 +1,332 @@ +import { ApiAggregateEnvelope, ApiEnvelope } from '@/types/apiEnvelope.type'; + +export const DEBT_STATUS = ['PENDING_DEBT', 'UN_PAID', 'UNPAID']; + +export const BYOIP_SERVICE_PREFIX = 'byoip-failover-'; + +type ServiceState = + // Agora API statuses + | 'ACTIVE' + | 'ERROR' + | 'RUPTURE' + | 'TERMINATED' + | 'TO_RENEW' + | 'UNPAID' + | 'UNRENEWED' + // Rebound statuses + | 'EXPIRED' + | 'PENDING_DEBT' + | 'DELETE_AT_EXPIRATION' + | 'AUTO' + | 'MANUAL'; + +type ServiceMenuItems = { + manageEmailAccountsInBilling: boolean; + manageEmailAccountsInExchange: boolean; + resiliate: boolean; +}; + +type EngagementStrategy = + | 'CANCEL_SERVICE' + | 'REACTIVATE_ENGAGEMENT' + | 'STOP_ENGAGEMENT_FALLBACK_DEFAULT_PRICE' + | 'STOP_ENGAGEMENT_KEEP_PRICE'; + +type EngagementDetails = { + endDate: Date; + endRule: { + possibleStrategies: EngagementStrategy[]; + strategy: EngagementStrategy; + }; +}; + +export type BillingServiceData = { + canBeEngaged?: boolean; + canDeleteAtExpiration: boolean; + contactAdmin: string; + contactBilling: string; + creation?: string; + domain: string; + engagedUpTo?: string | Date; + engagementDetails?: EngagementDetails; + expiration: string; + hasPendingEngagement?: boolean; + id: number | string; + menuItems?: ServiceMenuItems; + renew: { + automatic: boolean; + deleteAtExpiration: boolean; + forced: boolean; + manualPayment: boolean; + period: number; + }; + renewalType: string; + serviceId: string; + serviceType: string; + // FIXME: this should be `status: ServiceState;`but not sure this is the reality + status: string; + url: string; +}; + +export type BillingServicesData = { + billingServices: ApiEnvelope>; +}; + +export type HubBillingServices = { + services: BillingService[]; + count: number; +}; + +export class BillingService implements BillingServiceData { + // Not sent by /hub/billingServices + canBeEngaged?: boolean; + + canDeleteAtExpiration: boolean; + + contactAdmin: string; + + contactBilling: string; + + domain: string; + + expiration: string; + + engagedUpTo?: Date; + + engagementDetails?: EngagementDetails; + + // Not sent by /hub/billingServices + hasPendingEngagement?: boolean; + + id: number | string; + + menuItems?: ServiceMenuItems; + + renew: { + automatic: boolean; + deleteAtExpiration: boolean; + forced: boolean; + manualPayment: boolean; + period: number; + }; + + renewalType: string; + + serviceId: string; + + serviceType: string; + + status: string; + + url: string; + + expirationDate: Date; + + formattedExpiration: Date; + + creationDate: Date; + + constructor({ + canBeEngaged, + canDeleteAtExpiration, + contactAdmin, + contactBilling, + creation, + domain, + engagedUpTo, + engagementDetails, + expiration, + hasPendingEngagement, + id, + menuItems, + renew, + renewalType, + serviceId, + serviceType, + status, + url, + }: BillingServiceData) { + this.canBeEngaged = canBeEngaged; + this.canDeleteAtExpiration = canDeleteAtExpiration; + this.contactAdmin = contactAdmin; + this.contactBilling = contactBilling; + this.domain = domain; + this.engagedUpTo = new Date(engagedUpTo); + this.engagementDetails = engagementDetails; + this.expiration = expiration; + this.hasPendingEngagement = hasPendingEngagement; + this.id = id; + this.menuItems = menuItems; + this.renew = renew; + this.renewalType = renewalType; + this.serviceId = serviceId; + this.serviceType = serviceType; + this.status = status; + this.url = url; + + this.id = id || serviceId; + this.expirationDate = new Date(this.expiration); + this.creationDate = new Date(creation); + this.formattedExpiration = new Date(this.expiration); + } + + isBillingSuspended(): boolean { + return this.status === 'BILLING_SUSPENDED'; + } + + getRenew(): string { + if (this.isResiliated()) { + return 'expired'; + } + + if (this.isManualForced()) { + return this.status.toLowerCase(); + } + + if (this.hasManualRenew()) { + return 'manualPayment'; + } + + if (this.shouldDeleteAtExpiration() && !this.isResiliated()) { + return 'delete_at_expiration'; + } + + if (this.hasAutomaticRenew() || this.hasForcedRenew()) { + return 'automatic'; + } + + return 'manualPayment'; + } + + isResiliated(): boolean { + return ( + this.isExpired() || ['TERMINATED'].includes(this.status.toUpperCase()) + ); + } + + isExpired(): boolean { + return ['expired', 'unrenewed'].includes(this.status.toLowerCase()); + } + + isManualForced(): boolean { + return this.status === 'FORCED_MANUAL'; + } + + hasManualRenew(): boolean { + // From the API code, this.renew.manualPayment is true if this.renewalType === 'manual' + // So this code could be simplified + return this.renew.manualPayment || this.renewalType === 'manual'; + } + + shouldDeleteAtExpiration(): boolean { + return Boolean(this.renew.deleteAtExpiration); + } + + hasAutomaticRenew(): boolean { + return this.renew.automatic; + } + + hasAutomaticRenewal(): boolean { + return this.hasForcedRenew() || this.hasAutomaticRenew(); + } + + hasForcedRenew(): boolean { + return ( + this.renew.forced && !this.shouldDeleteAtExpiration() && !this.isExpired() + ); + } + + hasDebt(): boolean { + return DEBT_STATUS.includes(this.status); + } + + isOneShot(): boolean { + return this.renewalType === 'oneShot'; + } + + hasPendingResiliation(): boolean { + return ( + this.shouldDeleteAtExpiration() && + !this.hasManualRenew() && + !this.isResiliated() + ); + } + + hasResiliationRights(nichandle: string) { + return this.hasBillingRights(nichandle) || nichandle === this.contactAdmin; + } + + hasBillingRights(nichandle: string): boolean { + return nichandle === this.contactBilling; + } + + hasAdminRights(nichandle: string): boolean { + return nichandle === this.contactAdmin; + } + + isSuspended() { + return DEBT_STATUS.includes(this.status) || this.isResiliated(); + } + + canHandleRenew() { + return ![ + 'VIP', + 'OVH_CLOUD_CONNECT', + 'PACK_XDSL', + 'XDSL', + 'OKMS_RESOURCE', + 'VRACK_SERVICES_RESOURCE', + ].includes(this.serviceType); + } + + canBeDeleted() { + return ( + [ + 'EMAIL_DOMAIN', + 'ENTERPRISE_CLOUD_DATABASE', + 'HOSTING_WEB', + 'HOSTING_PRIVATE_DATABASE', + 'WEBCOACH', + ].includes(this.serviceType) && !this.isResiliated() + ); + } + + hasParticularRenew() { + return [ + 'EXCHANGE', + 'EMAIL_EXCHANGE', + 'SMS', + 'EMAIL_DOMAIN', + 'VEEAM_ENTERPRISE', + ].includes(this.serviceType); + } + + hasEngagement() { + return Boolean(this.engagedUpTo) && Date.now() < this.engagedUpTo.getTime(); + } + + canBeUnresiliated(nichandle: string) { + return ( + this.shouldDeleteAtExpiration() && + !this.hasManualRenew() && + this.hasResiliationRights(nichandle) + ); + } + + canCancelResiliationByEndRule() { + return this.engagementDetails?.endRule?.possibleStrategies.includes( + 'REACTIVATE_ENGAGEMENT', + ); + } + + canResiliateByEndRule() { + return ( + this.engagementDetails?.endRule?.strategy === 'REACTIVATE_ENGAGEMENT' && + this.engagementDetails?.endRule?.possibleStrategies?.length > 0 + ); + } + + isByoipService() { + return this.domain?.startsWith(BYOIP_SERVICE_PREFIX); + } +} diff --git a/packages/manager/apps/hub-react/src/billing/types/service-links.type.ts b/packages/manager/apps/hub-react/src/billing/types/service-links.type.ts new file mode 100644 index 000000000000..d324c37d0578 --- /dev/null +++ b/packages/manager/apps/hub-react/src/billing/types/service-links.type.ts @@ -0,0 +1,18 @@ +export type ServiceLinkName = + | 'anticipatePayment' + | 'buySMSCredits' + | 'cancelCommitment' + | 'cancelResiliation' + | 'configureExchangeAccountsRenewal' + | 'configureRenewal' + | 'configureSMSAutoReload' + | 'manageCommitment' + | 'modifyExchangeBilling' + | 'payBill' + | 'renewManually' + | 'resiliate' + | 'resiliateByDeletion' + | 'seeService' + | 'warnBillingNic'; + +export type ServiceLinks = Partial>; diff --git a/packages/manager/apps/hub-react/src/components/banner/Banner.component.tsx b/packages/manager/apps/hub-react/src/components/banner/Banner.component.tsx new file mode 100644 index 000000000000..88468f92e2cf --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/banner/Banner.component.tsx @@ -0,0 +1,61 @@ +import { useContext } from 'react'; +import { + ShellContext, + useOvhTracking, +} from '@ovh-ux/manager-react-shell-client'; +import { OsdsLink, OsdsSkeleton } from '@ovhcloud/ods-components/react'; +import { + OdsHTMLAnchorElementTarget, + OdsHTMLAnchorElementRel, +} from '@ovhcloud/ods-common-core'; +import { useFetchHubBanner } from '@/data/hooks/banner/useBanner'; + +const DEFAULT_TRACKING = ['hub', 'dashboard', 'event-banner']; + +export default function Banner() { + const { environment } = useContext(ShellContext); + const locale = environment.getUserLocale(); + const { trackClick } = useOvhTracking(); + + const { data: banner, isPending: isLoading } = useFetchHubBanner(locale); + + return ( + <> + {isLoading && } + {!isLoading && banner && ( + { + trackClick({ + actionType: 'action', + actions: banner.tracker + ? banner.tracker.split('::') + : DEFAULT_TRACKING, + }); + }} + href={banner.link} + target={OdsHTMLAnchorElementTarget._blank} + rel={OdsHTMLAnchorElementRel.noopener} + data-testid="banner_link" + > + {banner.alt} + {banner.alt} + + )} + + ); +} diff --git a/packages/manager/apps/hub-react/src/components/banner/Banner.spec.tsx b/packages/manager/apps/hub-react/src/components/banner/Banner.spec.tsx new file mode 100644 index 000000000000..70a0a34eb601 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/banner/Banner.spec.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { act, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import * as reactShellClientModule from '@ovh-ux/manager-react-shell-client'; +import { useFetchHubBanner } from '@/data/hooks/banner/useBanner'; +import Banner from '@/components/banner/Banner.component'; +import { Banner as TBanner } from '@/types/banner.type'; + +let loading = true; +let banner: TBanner | null = null; +const locale = 'fr_FR'; + +const trackingSpy = vi.fn(); + +const shellContext = { + environment: { + getUserLocale: () => locale, + }, +}; + +const renderComponent = () => { + return render( + + + , + ); +}; + +vi.mock('@/data/hooks/banner/useBanner', () => ({ + useFetchHubBanner: vi.fn(() => ({ + data: banner, + isPending: loading, + })), +})); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const original: typeof reactShellClientModule = await importOriginal(); + return { + ...original, + useOvhTracking: () => ({ + trackClick: trackingSpy, + }), + }; +}); + +describe('Banner.component', () => { + it('should display a skeleton while loading', async () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('banner_skeleton')).not.toBeNull(); + }); + + it('should not display the banner if none is returned by the api', async () => { + loading = false; + const { queryByTestId } = renderComponent(); + + expect(queryByTestId('queryByText')).not.toBeInTheDocument(); + }); + + it('should call api with user locale', async () => { + expect(useFetchHubBanner).toHaveBeenCalledWith(locale); + }); + + it('should display the banner if one is returned by the api', async () => { + banner = { + alt: 'Summit Banner', + images: { + default: { + src: 'data:image/jpeg;base64,....', + width: 1250, + height: 117, + }, + responsive: { + src: 'data:image/jpeg;base64,....', + width: 453, + height: 117, + }, + }, + link: 'https://link-to-summit.com', + tracker: 'summit::tracking', + }; + const { getByTestId } = renderComponent(); + + expect(getByTestId('banner_link')).not.toBeNull(); + expect(getByTestId('banner_image_responsive')).not.toBeNull(); + expect(getByTestId('banner_image')).not.toBeNull(); + }); + + it('should track any click on the banner', async () => { + const { getByTestId } = renderComponent(); + + const link = getByTestId('banner_link'); + await act(() => fireEvent.click(link)); + + expect(trackingSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.component.tsx b/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.component.tsx new file mode 100644 index 000000000000..d3523efc3dec --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.component.tsx @@ -0,0 +1,227 @@ +import { Suspense, useContext, useMemo } from 'react'; +import { + OsdsChip, + OsdsIcon, + OsdsLink, + OsdsSkeleton, + OsdsText, + OsdsTile, +} from '@ovhcloud/ods-components/react'; +import { + ODS_TILE_VARIANT, + ODS_CHIP_SIZE, + ODS_ICON_NAME, + ODS_TEXT_SIZE, + ODS_ICON_SIZE, + ODS_TEXT_COLOR_HUE, +} from '@ovhcloud/ods-components'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { useTranslation } from 'react-i18next'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { + ButtonType, + ShellContext, + useOvhTracking, +} from '@ovh-ux/manager-react-shell-client'; +import { Await } from 'react-router-dom'; +import { useFetchLastOrder } from '@/data/hooks/apiOrder/useLastOrder'; +import { + ERROR_STATUS, + WAITING_PAYMENT_LABEL, +} from '@/data/api/apiOrder/apiOrder.constants'; +import useDateFormat from '@/hooks/dateFormat/useDateFormat'; +import { LastOrderTrackingResponse, OrderHistory } from '@/types/order.type'; +import { Skeletons } from '@/components/skeletons/Skeletons.component'; +// FIXME: lazy load these comoponents +import TileError from '@/components/tile-error/TileError.component'; + +export default function HubOrderTracking() { + const { t } = useTranslation('hub/order'); + const { + data: orderDataResponse, + isLoading, + error, + refetch, + } = useFetchLastOrder(); + const context = useContext(ShellContext); + const { navigation } = context.shell; + const { trackClick } = useOvhTracking(); + + const orderTrackingLinkAsync = useMemo( + () => + navigation.getURL( + 'dedicated', + `#/billing/order/${orderDataResponse?.orderId}`, + {}, + ), + [orderDataResponse?.orderId], + ); + + const ordersTrackingLinkAsync = useMemo( + () => navigation.getURL('dedicated', `#/billing/orders`, {}), + [], + ); + + const getInitialStatus = (orderData: LastOrderTrackingResponse) => ({ + date: orderData.date, + label: + orderData.status === 'delivered' + ? 'INVOICE_IN_PROGRESS' + : 'custom_creation', + }); + + const getLatestStatus = (history: OrderHistory[]) => { + return history.reduce((latest, item) => { + return new Date(item.date).getTime() > new Date(latest.date).getTime() + ? item + : latest; + }); + }; + + const currentStatus = useMemo(() => { + if (!orderDataResponse) return undefined; + if (!orderDataResponse.history.length) + return getInitialStatus(orderDataResponse); + return getLatestStatus(orderDataResponse.history); + }, [orderDataResponse]); + + const isWaitingPayment = currentStatus?.label === WAITING_PAYMENT_LABEL; + + const { format } = useDateFormat({ + options: { + hourCycle: 'h23', + dateStyle: 'short', + }, + }); + + const handleSeeAll = () => { + trackClick({ + buttonType: ButtonType.link, + actionType: 'navigation', + actions: ['activity', 'order', 'show-all'], + }); + }; + + if (error) + return ( + + ); + + return ( + +
+ + {t('hub_order_tracking_title')} + + + {isLoading ? ( + + ) : ( + <> + }> + ( + + + + {t('hub_order_tracking_order_id', { + orderId: orderDataResponse.orderId, + })} + + + + )} + /> + + +
+ + {format(new Date(currentStatus.date))} + + + {t(`order_tracking_history_${currentStatus.label}`)} + + +
+
+ }> + ( + + {t('hub_order_tracking_see_all')} + + + + + )} + /> + +
+ + )} +
+
+ ); +} diff --git a/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.spec.tsx b/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.spec.tsx new file mode 100644 index 000000000000..2aa689e21a5c --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-order-tracking/HubOrderTracking.spec.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { + render, + screen, + fireEvent, + act, + waitFor, +} from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import HubOrderTracking from '@/components/hub-order-tracking/HubOrderTracking.component'; +import '@testing-library/jest-dom'; + +const { refetch } = vi.hoisted(() => ({ + refetch: vi.fn(), +})); + +const trackClickMock = vi.fn(); + +vi.mock('../skeletons/Skeletons.component', () => ({ + Skeletons: () =>
, +})); + +vi.mock('../tile-error/TileError.component', () => ({ + default: () =>
, +})); + +const useFetchLastOrderMockValue: any = { + data: { orderId: 12345, history: [], date: new Date() }, + isFetched: true, + isLoading: false, + refetch, +}; + +vi.mock('@/data/hooks/apiOrder/useLastOrder', () => ({ + useFetchLastOrder: vi.fn(() => useFetchLastOrderMockValue), +})); + +const mocks = vi.hoisted(() => ({ + environment: { + getRegion: vi.fn(), + getUser: vi.fn(() => ({ ovhSubsidiary: 'FR' })), + }, + shell: { + navigation: { + getURL: vi.fn().mockResolvedValue('mocked-url'), + }, + }, +})); + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + ShellContext: React.createContext({ + shell: mocks.shell, + environment: mocks.environment, + }), + useOvhTracking: () => ({ trackClick: trackClickMock }), + ButtonType: { + link: 'link', + }, +})); + +vi.mock('@/hooks/dateFormat/useDateFormat', () => ({ + default: () => ({ + format: vi.fn((date: Date) => date.toLocaleDateString()), + }), +})); + +describe('HubOrderTracking Component', async () => { + it('renders correctly with data', async () => { + render(); + + await waitFor(() => { + const orderLink = screen.getByText('hub_order_tracking_order_id'); + + expect(orderLink).toBeInTheDocument(); + + const osdsLinkElement = orderLink.closest('osds-link'); + + expect(osdsLinkElement).not.toBeNull(); + expect(osdsLinkElement).toHaveAttribute('href', 'mocked-url'); + expect(osdsLinkElement).toHaveAttribute('target', '_blank'); + expect(osdsLinkElement).toHaveAttribute('rel', 'noreferrer'); + + const seeAllLink = screen.getByText('hub_order_tracking_see_all'); + expect(seeAllLink).toBeInTheDocument(); + }); + }); + + it('displays loading skeleton when isLoading is true', () => { + useFetchLastOrderMockValue.isLoading = true; + + render(); + + const skeleton = screen.getByTestId('tile-skeleton'); + + expect(skeleton).toBeInTheDocument(); + }); + + it('displays TileError when there is an error', () => { + useFetchLastOrderMockValue.error = true; + + render(); + + const tileError = screen.getByTestId('tile-error'); + expect(tileError).toBeInTheDocument(); + }); + + it('handles the "see all" link click correctly and tracks the click', async () => { + useFetchLastOrderMockValue.error = false; + useFetchLastOrderMockValue.isLoading = false; + + render(); + + await waitFor(async () => { + const seeAllLink = screen.getByText('hub_order_tracking_see_all'); + expect(seeAllLink).toBeInTheDocument(); + + await act(() => fireEvent.click(seeAllLink)); + + expect(trackClickMock).toHaveBeenCalledWith({ + buttonType: 'link', + actionType: 'navigation', + actions: ['activity', 'order', 'show-all'], + }); + }); + }); + + it('formats the date correctly using useDateFormat', () => { + render(); + + const formattedDate = screen.getByText(new Date().toLocaleDateString()); + expect(formattedDate).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.component.tsx b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.component.tsx new file mode 100644 index 000000000000..8c43b0d16301 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.component.tsx @@ -0,0 +1,129 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + OsdsChip, + OsdsIcon, + OsdsLink, + OsdsText, + OsdsTile, +} from '@ovhcloud/ods-components/react'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { + ODS_CHIP_SIZE, + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { useFetchHubSupport } from '@/data/hooks/apiHubSupport/useHubSupport'; +import { SUPPORT_URLS } from './HubSupport.constants'; +import { HubSupportHelp } from './hub-support-help/HubSupportHelp.component'; +import { HubSupportTable } from './hub-support-table/HubSupportTable.component'; +import { Skeletons } from '../skeletons/Skeletons.component'; +import TileError from '../tile-error/TileError.component'; + +export default function HubSupport() { + const { t } = useTranslation('hub/support'); + const { t: tCommon } = useTranslation('hub'); + const { data, refetch, isLoading, error } = useFetchHubSupport(); + const context = useContext(ShellContext); + const { navigation } = context.shell; + const { environment } = context; + const region = environment.getRegion(); + const { ovhSubsidiary } = environment.getUser(); + const isEUOrCA = ['EU', 'CA'].includes(region); + + const [urlSeeAll, setUrlSeeAll] = useState(''); + + useEffect(() => { + (async () => { + const url = isEUOrCA + ? SUPPORT_URLS.allTickets + ovhSubsidiary + : ((await navigation.getURL('dedicated', '#/ticket', {})) as string); + setUrlSeeAll(url); + })(); + }, []); + + return ( + + {isLoading ? ( + + ) : ( +
+ {error && ( + + )} + {!error && data.count <= 0 && } + {!error && data.count > 0 && ( + <> +
+ + {t('hub_support_title')} + + + {data.count} + +
+ refetch()} + name={ODS_ICON_NAME.REFRESH} + size={ODS_ICON_SIZE.xs} + color={ODS_THEME_COLOR_INTENT.primary} + /> + + {tCommon('hub_support_see_more')} + + + + +
+
+
+ +
+ + )} +
+ )} +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.constants.ts b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.constants.ts new file mode 100644 index 000000000000..ced3944aa2d3 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.constants.ts @@ -0,0 +1,8 @@ +export const MAX_TICKETS_TO_DISPLAY = 2; + +export const SUPPORT_URLS = { + allTickets: + 'https://help.ovhcloud.com/csm?id=csm_cases_requests&spa=1&table=sn_customerservice_case&filter=active%3Dtrue&p=1&o=sys_updated_on&d=desc&ovhSubsidiary=', + viewTicket: + 'https://help.ovhcloud.com/csm?id=csm_ticket&table=sn_customerservice_case&number=CS{ticketId}&view=csp&ovhSubsidiary=', +}; diff --git a/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.spec.tsx b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.spec.tsx new file mode 100644 index 000000000000..f17d55a170aa --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/HubSupport.spec.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { + render, + screen, + fireEvent, + act, + waitFor, +} from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import HubSupport from '@/components/hub-support/HubSupport.component'; +import '@testing-library/jest-dom'; +import { Ticket } from '@/types/support.type'; + +const { refetch } = vi.hoisted(() => { + return { refetch: vi.fn() }; +}); + +vi.mock('../skeletons/Skeletons.component', () => ({ + Skeletons: () =>
, +})); + +vi.mock('./hub-support-table/HubSupportTable.component', () => ({ + HubSupportTable: ({ tickets }: { tickets: Ticket[] }) => ( +
+ ), +})); + +vi.mock('../tile-error/TileError.component', () => ({ + default: () =>
, +})); + +vi.mock('./hub-support-help/HubSupportHelp.component', () => ({ + HubSupportHelp: () =>
, +})); + +const useFetchMockValue: any = { + data: { count: 3, data: [] }, + isFetched: true, + isLoading: false, + refetch, +}; + +vi.mock('@/data/hooks/apiHubSupport/useHubSupport', () => ({ + useFetchHubSupport: vi.fn(() => useFetchMockValue), +})); + +const mocks = vi.hoisted(() => ({ + environment: { + getRegion: vi.fn(), + getUser: vi.fn(() => ({ ovhSubsidiary: 'FR' })), + }, + shell: { + navigation: { + getURL: vi.fn().mockResolvedValue('mocked-url'), + }, + }, +})); + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + ShellContext: React.createContext({ + shell: mocks.shell, + environment: mocks.environment, + }), + PageLocation: { + datagrid: 'datagrid', + }, + ButtonType: { + link: 'link', + }, +})); + +describe('HubSupport Component', () => { + it('renders correctly with data for EU', async () => { + mocks.environment.getRegion.mockReturnValue('EU'); + + render(); + + const seeMoreLink = screen.getByText('hub_support_see_more'); + expect(seeMoreLink).toBeInTheDocument(); + + const osdsLinkElement = seeMoreLink.closest('osds-link'); + expect(osdsLinkElement).not.toBeNull(); + expect(osdsLinkElement).toHaveAttribute( + 'href', + 'https://help.ovhcloud.com/csm?id=csm_cases_requests&spa=1&table=sn_customerservice_case&filter=active%3Dtrue&p=1&o=sys_updated_on&d=desc&ovhSubsidiary=FR', + ); + expect(osdsLinkElement).toHaveAttribute('target', '_blank'); + expect(osdsLinkElement).toHaveAttribute('rel', 'noreferrer'); + const hubSupportTable = screen.getByTestId('hub-support-table'); + expect(hubSupportTable).toBeInTheDocument(); + }); + + it('renders correctly with data for US', async () => { + mocks.environment.getRegion.mockReturnValue('US'); + + render(); + + await waitFor(() => { + const seeMoreLink = screen.getByText('hub_support_see_more'); + expect(seeMoreLink).toBeInTheDocument(); + + const osdsLinkElement = seeMoreLink.closest('osds-link'); + expect(osdsLinkElement).not.toBeNull(); + expect(osdsLinkElement).toHaveAttribute('href', 'mocked-url'); + expect(osdsLinkElement).toHaveAttribute('target', '_self'); + const hubSupportTable = screen.getByTestId('hub-support-table'); + expect(hubSupportTable).toBeInTheDocument(); + }); + }); + + it('calls refetch on refresh icon click', async () => { + render(); + + const refreshIcon = screen.getByTestId('refresh-icon'); + expect(refreshIcon).toBeInTheDocument(); + expect(refetch).toBeDefined(); + await act(() => fireEvent.click(refreshIcon)); + expect(refetch).toHaveBeenCalled(); + }); + + it('displays loading skeleton when isLoading is true', () => { + useFetchMockValue.isLoading = true; + useFetchMockValue.data = undefined; + + render(); + + const skeleton = screen.getByTestId('tile-skeleton'); + expect(skeleton).toBeInTheDocument(); + }); + + it('displays HubSupportHelp when there are no tickets and loading is false', () => { + useFetchMockValue.isLoading = false; + useFetchMockValue.data = { count: 0, data: [] }; + + render(); + + const helpHubSupport = screen.getByTestId('hub-support-help'); + expect(helpHubSupport).toBeInTheDocument(); + }); + + it('displays TileError when there is an error', () => { + useFetchMockValue.error = true; + + render(); + + const helpHubSupport = screen.getByTestId('tile-error'); + expect(helpHubSupport).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/hub-support/assets/assistance.png b/packages/manager/apps/hub-react/src/components/hub-support/assets/assistance.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d09c1356cc95e4a1ee4173766cb7a6ff8a375b GIT binary patch literal 22390 zcmV)rK$*XZP) z;SYA!+>4)cvYmYR@TsbvL)pI7<;NK7?L-92_5jY7ff6sl5}|vYog>c=8n?f236@Cx z^M_ekb3sh75F=C-=1VwqX#?V};Swx!z+ulBsvYQaeza;~D32NOc7T>(SzDx)d4j1c zrW4d!oCtGT^jn39@mr1I5^VF>zfG`Iqt(&_stUC1XHVg{Rnrmx173jZik4uR-vnE% zrB5b(I?4V5btBNn*&y~EdzhbSrT1d9!8%~PBf`zCa0%8Pv3kG&Aj)L2PbWP*6P!l^ zMV$Aqr?If!T`^3-Z}EMBIN^o+Azb^kCD^tRtO^y7;`CB$GM!*uHlGQhE`cJ-uVHrk zac1iGU4)Ey6WlGMC0Jsufm+tXuw?6`N!4eKFZMD=^Wel*wnFQ1{MG@%u^J3X`ak(mpie>5DTEKWOXS4*% z>{x3i7_6Df1QW^iQsNbGaa9&@MT?krCV8HObUaHqBgU)w4?AZ8+*Xo=;aTQgNe=Te zGlxm>GBYzXGc)gonVFemSVv%HW{gQjG4zc)nXAsrluGY>as{5Net|94eEz$8`n3*q z#w7h-E!)FZWpPG? zU4SbbSA@z6Xi@*0(`vwK3I=R2SmjU%Pz9LO!KkP+$ricNo#fyK&sC|b3P-*3=~?}^ zn{&U-Ep2J2YX+#_r?MKKKdZ2H=hOv><5;g*-C57cJ*F-&Jz`2JPb;nQ!k$Ke%36Sm zJt(|f(*RJ{1W;V*@+D@~?3}hZ<_-$3(rqOnvN~urU^NACS9P!oPz9v8aG$u08E$`$ z=_4mG{n*V+XS6cim?Wk*Ml=1mg-n0T$4qzs2zL4mgWR3uZc8Mq{>oid_or^j`s62) zvud)c>H(^`%4;~N)u@ik2wzE+yM1F+-dF3k7cMa+*99{2T#%Y=hm1Ts1cJE0=f#Qe{2 z$Ne12jU3VzW*7_1U<5qZqd@!)r61#Ey0P&b-d9ZEfK8_#xm2{AsY+cTZc8axHaRDYP`FCD5u2jWu93UiHQYB2A4?WeDk{p-inPM*zxp%kifBuHbz(X| zT)Z9rwYGEF5yDujjl*Z-!1_6?J`QR#g88pi+)l2TefQ1GZv>~y1AY#w$0G#z(`I*8 zZ;i^k3;%)9_d}t@oR)<6!HQ5Cu$n>+g=u~$ffb#^aPxZ{yjBDcAyh4doXgH1>n(gt zNr)`KC6ZQwMOeOZXX*5$43L3`V8)nRIH>iIklut42~ah$$eA`ZNFu~^I7#S zs{yOID+z@K9MoC$kv~gawe!&Wv0z2=Kv{ZUZ?<0=ZEkGco!~^c!lk`= zZVQxDw1C$ekc7wXmerfmZ$@ZEA&S3lj8rJJS0_qBO)}~atc@tcRa)4 z3JB1`0IiKhJaU~9TB#JUcI>0R`r*2Zqb-f?;Rcq5*M+6cQfT)yw3L7KOVkwtT9Kbi z^V#4{6R^LY1C_!`TMm_ZtAG{vP+z!|8OGF#FgPqi=QeJ{h6c&rt!%GX!1~qi zTL*85%3Tnb(CQP#rmln520mYa3&82}LUmmmR5t`5yTB2B^{|z_@pY0SRCuA4>;DaG zR}9?J6hdch~tQ>O?@p%cj{2V1(3I(i7H>Q0V7vIzxpXQ?# z-!6o<2}&z%(AaFpZJ**wA8sAhH33*t8-BvqNuUj>2t~jRKfEe{4Ih+1r73vheDJ*0 z@bHbyFxnY*ScS{#B4>7hhznhSyPp>R7uIDPQ$Mn%IKhOx2*g)D0$FXm5miBb#t*$od&ndX9&qB;c1dL&P!nZhjcN0+Mh#IDstB@l9xq1JQ{m^Pg~SY zfQ#anQK!!8zG8Fc?}_Pt0nI@GZ5yQJv=f&_VPEo2Q`KuCTA)QJ0d4R@4N>8njinyg z@K3?hgl-yIFD|XV>hHU4zsj(#3xLHv-aB67uc=2zQ$B)w>RyRP{ZlTU-D_J+S+X_R zi?AF#YJvPxH-Q%WnECHqg?Bx)dXUIx3AB>F<4y8|`(*%!V{)S^Sc8MAdJ3K<1T=(R z++_#AicY4#^?M!;6Ie&_bFOgbu?F6p(S3F=)?<24R0q6V;`+U>F1mP^siqeK_*zJpW3Lt+AIfFwX3S1f=3pmCTR~Y1(k%s%6(0A zG=p(thvOlv-G;%>pZyAt-uDr(lZJ*H*yXI-)ZPfKMDKjJVQ`eG&J`E$Ku``YXj>tx zfEJ+e&^*W~YR|oP_?rF-XhZ0u7Vydemcy&sZJAIfVI`o-0F^DsAd~Pkt(PCmDuXrR zkG{MBl*5Ivj=KFBfQ%H_v&#h6-S9lHlS6>^v=Ioq`X1xR9sITR*{k`zIlL&m%zyX( z59>y7V7&(?u1z_ct;>Yo`70{9vM%m~YRIsUylH-Ou39SlXsh!I} zRX{2MQa%MwlYH*F` zuv3NsJ7c60It-h2{cnc{?*15#(qR~_GKV#qnPGZA7BROkidTKYzXb5L_z$yPFUxpG9+WPdY@^#eIJKg%%-#ITC)=)|&-e>=;N? zs8jGXA)G?W<^xp6_6e`P;zHuEV(4jj%y?9{(LUCH2t4}aTj;JFRw0D9W@jQS{B1Sd zamxpw?{^!pQ-=e>c^W@IaWLF@>wA#4!UR`e_YD3mbYP7MOh0b`(@!0M;8OA5oiT9w znOmYuGcBS@d{9E$47tTLmj&G7S+lL+Rs4)aG3`_dxU|blKD80y4L-FoQT5cu5P+4# z8uA<=0j6X@3Xx5BO=9pEb<|$yssz@3;ivxL+mG%&3RwTa@YP4(1LR}?1s0W-1nuS| z$X#oN)&FdQ&tIPicis8{Ty^a;aQ1}{!sS;#1*Qd2u=1Z(aB|PcP(oudp(2jyCJ$o0 zW)C{{{(R%H4;LF*uf>CyZpJ{NRa>)GC8YTeVqn{F(uFcC=0o6?;%AISH95G-PA}lxRdB-q5BESp zh6?U2G6Y3LMA>`q4Q0>nHfho{>E3&{Nq3XZ_j6t6dh+V9r7e^aB!7Hf=Oj%}(wx4} zcV6EUvN3?5wLTiGvAY7u^$KCws}p6Ss%=@~KR@K@@o3#{H1Br$ed=Z zZ!2@LT9|OrN<6E8ZQp2;S-*qT3F?lZoc@uvs(A?&bumc}%7k$1EPx%r z(&_VB2DTx%7PVg!5<->1NVPG9I`P=955afkK`!m}FK4XVvd({qJD;D<152RCWTEjW zeGkvBfdsqyd@)7k_y*_Vs);8lA6*eqiO%Gc>Mu@AuOzOt>O{{^nPfn+AenH2`+I0- zwRggWN2}JgEwKLc6|YO8Y&DCLE2(wE)HymD8P7R4J*v6HKTn+AIIeAr z!||;H+X}q--Ujw9^yJ)DVp(ZnUzmK5D}I6=6P&=R{cEN{cuD=|HD_80GAZ}dWwXVh z`HecCvaJwXP|@M%PBJ1{X?5LgYrwYR4mWFE8?G9WR}VK#9j|yh_+-NZGrY4G1}}x4 z)8Voo;aainJF}Sc`utamVV`^HJqWj)5U_=Q!6fX_F>vt6V`hxcy67c3;*pniO5?mCuKf3Zmnl6Dow>qA zt*lxr8=q>|+~I^YJLx$k9_SRcoBsCz7#X>}xxlfPDhRc5$JvdP%JCF?4KYBy}h zuYf$5ZGtzp!+i?{ob%wBHSqaiSQ8Dp0!Tc@?>{yC8@k=_2!FcyF(N(pK)Ct1fh`X* zki1XN_BosAa*UEr|7Smz@oAg?#)OI*d_&XFhgQ2LusR!2iNz$TA>>q5)dEmjQAt9H zqr=a+WTr(cD_L_UpIUN@+vN^78Be0J!O(WOt*B=*e0dZeU!@>@ZiBQ`>iLM0_8srT zpO@18rpJ)Y6N{(Km2vC|S=jj9L6|lj=Hg!eY&KggOFCmFEo=AR9iCT+pP>N%V7to! z>k}j!C~^g;HPlo$9@bV;-to{%W||IuYpYw_3b0Mw;U>esWUZ<7(v4fvP28FY?;nIm zRw__GIs_ZzAguVrj=gBFKizM9R4eT~|LXe?VL5JKZN9<8dIplPCxDy(_KI8>&`kXL zI2*hz$MsqDaTx?#s$9}F4lopxnN!+mmT|R$mMm)t@nzK=`9D$CXdI0iuiDftK0#m` zy2A~IR=a_0S^Lu9^CR%o8u<4{_&yMhWOlGios!FicmJKP*FVJNcRx#xmp52Kj|DeXJ)}UG1@`_3_ za*DOAaYAm##e_U>Hwb4k-&OqXJDPa5dSHL!Phu`{q&Ix^9*-YVQ%0M4lLb{w>l9m!C`5>L}O*2!V(z_t>jLw)hjV zEkH{;Meg+Fs+;~R{;3)!ogU|QQJ;+~?U-0D(prLFXeQ2C#jZC-rjXPo6BhG}AR({n zBmldqVR@&cfK~BpLttgo^O@RhG}i>fcBB)s6iy{({pt8M$WzL$wu8y(j6WmM|?15hm zU@APdpC>eb3dX$a2+r=s)OELOz+TpG5IZeBu>_B!+7JO|ivrgiqPAh7TCKK5>=*bM zFbPl%>wPh;^&)iPE+T(e!L0v%(Zsqw#`(RUBQPcZBwTc=2}#Mp-;}FaTbCIrBdvb0 zjl{G{32h@Q8=vdw7+@QYR6QCOTQ0yN_+O8v*7H;L}_+=dY zav6udSi;dy=HoTyH+)7-!}9)gh7B6gBz}E@SKgm-QvaUl!ZLI*Sz4`DC8Ww-Vqri? zL}4RfGh_lWvzqv_nhpR~C7xQ(6e})U02Vi}4J2!&vS)T_I7GVus46oIq1hi2oO>O9 zDZMe|^y0-&@8zE79^gpabsEHeslD(?xr#kO*RyBxc=mp{h@J1wV%yN^Y#%ZW&o_T& z_t2l&JM3o;zWp;tNB)f8yVKE)`I$Rz{BNVCiF2=fh6Co5&hcXMiUKm3!ZOH$8*>(nsy28dHWak4gZCM zBc^k7)O3PI&tT1K zg`JayFSTP#%Eix^C%~2q; zN3&e_GbWaSIqfY1GkYp96|_?Iu!?=3_+IP{zLGsFpThHvnXLlr{f>P6BbQzEtjZ3b z{|M(@@)WZc-pL`u)f^7*sa+6MlIsbgioH7RZ;rN=qKnDal0d3EOM8s zBfhxu#DJ9yH3U{N<_5NPg%+w9b#ns5SGe|PO)M`YD77E{X%f!nC4u#dk-%QT?wKzO zU}v=oY|!XmdGC?YB0Kz$7Qg=G;wM@1_B;3n-o&1OYcyb;7X@9`bp&PJh`C@IiT27) z``g2F3JHwJL2s62pvq1j)@J>^TWoc>f$eM^%23S@M%(G}d811L3CxhmrdEKB7QpHS zu(Mxi71$#qe_`93Q@HqwXS5Nh*IfN~*1r5L=8gYixBg1@23#wTCy(36svCs0HRilU zTzQWCY@HV0n$q&|H|1fl)O60gC|x;$A^DB&6p)n<;VD)(u$`qCwStD-DfR13lkIg_ zGjqr+EOXhPGpI9+^0Z&YetK}dX1XFlYQQxyKseHxNj)l!+*lO z_zm{xu4ZrGwd~Vhi*Hi%?`6on4t@61#FoW(?)fXJz?hJWztKvtS;n{JC%MPCfP^$h zqhW3OutYboouQCI*pdJSYvbor%d5a_qB=T?^b)%Ze)d}Qng2zX)e5jezKOlrr@K}{ zE3E8`Dvo{g*WUqqXv7TWK0l8C-S;-DUYtzmN7D&d_CNLn30Q-!VUMt`MR5ng^2V*H?UGEjzV%Bn<;j@ONIS$ z9FBfeI_{Pd`+dc8k5XbEL7sh`cx^%3k~KoUIjt{sORlKF9vcTq2~-J7o8dyxhTB$Q zP|Xr;l23wWWfgqE0z#kb*|XsxcF%g5JwFa-@05}3`}Ga>uY87m`|pxqON*?%URu@z z#-8{lt7EklT7A|N#8+gWlJ6&>yb4ol0sbNR7%a6NI^9A}r3jC!2R6F^Vr}*_9oQOj z?HdJ<&j>hg7rIlZkI+2=)O$6_ef8X(RM~G6uYFR0yM|;(^)aUp`sYGO8+(7HYg`o@ z1MxA+J$4ajgR@?jP_zzD(`<2&vX;`t-7BqU@D&o^#``K>ZD3j32W!KaHaPn=673bI zE*RA4g-P*!!HPQ|S41+^)(AvvFrWUoG@-&vHkgrLYgf9+w)eJK@^{M|FNtI2G#YqjDwNQ+aRjBUoa{pZ zSZ6PzVB&O27c5uH(4r%#jEy;^mNv|`qJcZyerRRq)6wW&91Q7M?bcnZPDH9F*8c zQE8W!wLevruTWj}jsWoA654iwE3I>heW*BQxf56t>}|Ygq~u_my%eU*qH^-DSjYcF z!8cQ=Sie~)a?zBAh7w4g~|=Ce1_VZ)leG% zbvm*2rMBu9@%)4Bw6wQU;F#IIi$+-ud2^RzpEQGl1*^$C8bscjZP>n>PT|C#sqys% zvq^(iCZbX*YC3r|YA%6kg9*&M8oxB5j({q)fl^M6n#yM3U5@_y>7R{?sc@kYr9Fi=xDR=UXTu(Ew{ z*zYIZ5#J6AR!2&5HmO3%MGkTVtYs2bX=T6u0mpVv2vY;PiH(+}pU$rx>x}5Vr{Zh+ zCw|GQ@&9y?UbZo2JmpM6xr`$6OVC9WpfgoSaGSNbvOJ^AbfYt z!Y}n23BqXv><#!Pj&(U;8z!crY~@%oiU>)x5t3xXl2uG>eYcw%*bGMm3b4Kc*telB zs2N}bsHg_B`1JYmH!jaZc(wU1h@7GHC1DAR5rG*_KJHbvfhp@@77l1*nG$Q_5qu1Bk#xA zq{pREy6+H#hqZj$3$s)hcMi;;|G)AG$*MgmCswW?sutF~&@V&*s~aMKz3LQL*Iw+g z+{QNj-{gM#emAW*YrGyLEI;-1Si)E)-DEeg4JDf#q}y5#nZ2)gJU~l8Nf)`Y^63Va zwXX(hNzt9y@^7WArC{%HwlkR z^Z4}JEOPVnNh$Bxl8;Po)mcKR2{944Qjd@Sh*M%=eUq+ZozJ}z+I9mg-RHGl_hHDi zoEE?uZK{Uv2DWMa)qY1au=V?0r&Fl_w6NedN(u#}b_w@AtpclD;;kpptf1W7yj&}F z5zScI@DQRs&79pU1&@nUdGX~Gh_g@~8A)k!D!F+!37hjc$F0nwGO#H3JZzHz@BI@y z9astI{)k&xaj-uJqi#M9V0*E_?>+*PeNGEt{d1ePvburQmdV-egBn=brs}Ie6&6*x zz!l|va6)Xa=tD`d1h#LB2`72%bVqE5e7ObLdC(NvXd|;HN8)iwss`)_@5adIlr@fy z)M8v&{&B>{P?C~LK~6qdwo)xol~P_UUEO24!SXc{c)6fny|;aiJZ+|sm1`E0guatg}p2l&xPR-X>6KJmamrpOG%iO zdmCn63ObivVaa6>`JEc8W868DUc$oHD%=Nd`$n(%7vAOfs!G z+D`!+dt3cG!PT%prw8J&c#y!0;0 zN`4P!dD-1fpI6K7?0>`V7^odH$FZBF4lX0)bStf>~H%q9UZ^zEGI@S(v#|mRf>q*BkL)uEXc>*iLhcC7g$%Z(_xP z`&lj|_m2nIExP;IWtw|h0J|*_bOml;f2Rz)iG0TbtoG@YIbN2rs|KBO4QzS&-IQpS zv|mF>kfju;_Pd4TYwn}e@d{SQFWNCP9APKriZuA65{r|#DVt-O*}1togeB3wnb$*0w}Q0ky&G*#X?J!)ph)gwc;Cc zGGBq}*P!Y}C9z+S`x<$rUy0XD*IuiBXR&sS0>@l39lB0E!Kr1H6lP>m6B!9HClOpr z2n9ZRyp{&wvvs+E;kuKae3Ed=gwv8e53*?Y0CtAlApvanF*f-1XM^uorvVjZc+UV9?iwKA zy{rAermDt?bzYyG7QlYv1?D0*u*WR%6gcKJfp0NZu}w@EM|H(is8p(}nnp_Ucyi0; zIjyQw@)*ui&M8$&Srysox!6LZDDs!dA`2C|5UPwuCwNlF1#G3kNWf}et?S8W*RL{L zeA0p4=zou}t`gXRtoOaQ-7&0h@^vgfIFL0*#<)DJ@fGm?euyo11KXlxQ+SrcOY2N( z3T#5ccUTL4p{(RrO{FC>iHn~|e#QJoz{=mva=2JwzM_`2++s4q6G=N7Le5bWdEO=p zd<>KY8K{;3OK>f11lX+ZnC`dIM_5=Ne8tv3;^dZiq&3|X(1(Sd12te*9_lXuZ0~}B z8-#@(%<3bf6XcRD4-vnVc_Ns&;z0M=?> z&?s)rcI@kwG&q&ICy#QHHNkDMD1-3XLZlW*~=?8Uq z30VDC2jI8Pge7eY0qNJCRA9I2?qY!gR)F`QbG1&p%0dfvJ}h7#CYQ3gG_ZS9Ve~#X zux*Am$G#Kg9xGte(x#A=`4diH3vItDtE-^aUYq0al)ro0tkg&>a*&XeM@&#GVV=Q+ zZ`BjA-5{hRdS?*H`+_uxi~NH%i|PnAVh=Wm=XCh5@+CmP8n8By$ONlqv2>aL5W4PU z0V@+t0=PjMwDWfkWM9NBEx5(KRH-TLYL&qj3dp#M-IiOJyZsSX92iQHy~d@_%^VYa z>+J@%9UV|bJi0 z;6%*(LkZp(h;c&@A)A6RZq$o?o!ACx(yh~>U#}-muYTM~;YNaUP9Ctk&9^gO z0V}{8q^;O-CZ2p#dg7CMmC%)V%i(U(eeg0pg_r+Oybphc&z@-<+PsMUYd3Is+a40j zTpQRi`(dHk4Q#thJu>54U|&dX?$0DAe=mWRR#rToE*~pk?eoQJeg8P?BrEDju-b`9 zvJxJeMyPKr#)IML_nHYl5QfP&j)ttV)$A4iw@2w(&7+Zs$# zh3h7r^ab$1qV}%TaRaMnG3C!?)uva-&YnhG+yrL~t8J~;Us%ZOT(!fer z_}Mk?i4*NsvfaV-dH>_<3HM_a7B)F)vIeUYSetbQUr)TBpXU5iwjiEzIOXb&3BK^s zW*D>-?wJpL=Be$2i{Ocs@cc$qE*R&d5*u3+As`Q;I_PR1SDq%|x*h+tj<3V6@#-sq z)nJwSO#mBsbRw9;+f<4&Z}G=xWdH#?j3nA!7ueUf!k0(fz_weVxkZflZ~$XIy^UbQ zXE^NhuvaKm+UH{o`CR+isE_(%%C?@mE3yR!E`>jR2R$aCB=;va(03?JYVw-#aM2HN z9lUo(qSApo~YHcW3V~$)RhqX~GHX-0g#zx$Ob=?jH?iOPl+j zC9r`zjV=poR2kgzo4k;^fo-?e9t@@D9fKG$@>V|n@^+T4e2xQ$Mrc~L>fhqIJ9u-{ zEnIi^K$dyxJN@Ja=D{;-8UQ(7Xk5Fces6*kXtn**M7VwiJiP`!I}9r#r6q3LEk}a0 zUg}V{Sh~ZWraPFsb+8jw1?<)VY~1|{b)m*K^*?7F)@i^7?Fx126*}^Jm*BdAZKsy) z(cy9a6P$O=L-c*PFVFn9F258+uG`9mo!W zcp_Zm0E^d)q*k@BAl@5Pt|YwF<0U zc<;L^kU$p(_JxfyY&pK)b-TnlCEW4QC_FBBT>N2=iS46wz2p&1a$i07I5+kmiZQeE zl1>SBnfC8USla>d#Cj|ISF!!uTzGS*a?7355km9clfZUd!-Vk;=4@>Ys{(cq3%5N% zaquB9hqeh=S=Q0*wYY3M;$>4{X}BBM->%r)oRHMYUMzAVveMsnG@^m}FY;HTS`@&oj z*h&8Ie?LNai5uACR(x3vZ+fc0`%wRo2zTGy%GN z1HbtXL7#r*iNY#-e`U|Gfb*edVU?P-WguyR>%nYk69;xv{lN&AgEh7sZu$*| zYoH-K`fc+7;{BF`Ijr@h(-EBjR)^tew95fI&!jGR zyOQ0&{^9hV>>KbkGp?utQovJL`9EiL{uE>!y zn`B1#7;doA>@5%R>*fcE@?8Mt@K%A%JgmcC0_z>)a@O_EIWS-`Bs$!{{^9iECK)%n z5Uk^czN(`8`^9v5x0Ie^0@)SW2VJ_dvOWURwO)O-Sxn3J-_MFe{aJP7K20kR-^a28 z_lp|qgR+CEA*ijws(^ifaGyDC09Gvt^k1XL7!dDL=W~VudVCK{!rb@P`9o@~mju?; ztZSJDZ1?x9cs!p;|8d(m9Dj>~R=B&~3D-;ODWH7-)xfVbZ-&?XR-G8l;2U2&;s2 zFtdbaZXP5wkm;NI5#}?iO~5AYRlu4}X)bdvKRpONroaNT`|fprSVfn?ZL^@qaYI{2 z_YX^@MU=3$8{FM#~3T;y_d}eZ^UuooAI3cS_apQG4bWr_t_tN z6F$k;2wjVB+7XNlu>1qxtwghgbz#CeqMpXX13`xZ#+>IU`? zt?@o5>=p}XOXxN{m+qgGORL)^aJvcpbzCLA$JxZTiY@~9r+-Xk)jAWw`;5dLF_7#P zEMy=->|6GS;12%qg_7#EXiTuH2XEMf8gA~fkCj4A!lC-=cW?p_XC9$CwE|AMfS9L}wIr{%R_rX?nPSu{v+J&r%oMSGKrs z=zPX#aTuJVn*L8L!}~=g-QLcj+nX8E;wo493x|SNdyP^Tp_|9$5%H1E>x;1h#jihkPw2``Qx+7k2tHn)6dlh2T;u}-w92qU$F7z5jU`Bl-~9{ zVb@pz`E@njM&!}$txUQLi`#uzHr*w>qY6ak*GBizg>-$dh;F0unEvZgtUf*vZg#@z z+LD9MgACfxIRDN1P9G0*FdhEf+=X>KTsj2;^W4CmF^VXGz6<2lx&xs7rbf8N`A%S^ zNxC*3hFs^7_Ge>bpa zln$iBWmBcA)dA3str1qW3A92ULvr|ddI&@sC?J z>InWfp*dVSGKqBWAYzW_m^I&<9DhAT zgi;i!qtF$ttXasShOmWg^~ZaKAHJ*oiOa9(^mF5t1?Nw0=7KswtwXY7msY&rvkhv2_60P}7iszQuy11Q|6vdMBAW^8(A zf!1YdgUsswwRXLMj6*u~>wLA$aPazILJi5M?A(0o-FPXY+@32mUGGjhow157mn(TX z5ZW)R=q|Iz!pffWb{02`jv@Mxo>~FhoF#gWY}Y}U1W`a@G72>hs|2)CK)q#SIJ1{W zvu0f+-kxE^9x{>dXQVtRm}9K%MWCM2fM9LXVWD3zHs4_F_Lc?Sdh)!~Ek&}0 zrOy;V%Gfkze-Md#gJffbR%O2D**BK!ygtA~GzbrPheyxF8v$0X4 zM_Npft%mTjfO+JKe^ufHr2je;)^lN+w5(-%Dga0ZY?WSD5QY+-vn}AgU($!w*R(rE{ ziJ5Ia*&Q?KqoR%j(OGN?PGGYkfq*pIi67SutXq3h;c}IP>JVsU8{=6+a(H%JGz*p) z_;pbT#2AP=V&sMIA{aj>j533Ca}}fraAS^|_-$DvgT6_k$FM93t1J_#643`Gn!3GL zNS9GoJcbwGF)R;{p+duQIeSDdm%Nw7O&@14=<5_-{4Smkr$zDOEDK8(gs^p~o`Wj` z@LTOi&{|(%K^4S-t9&`S!jDahf|&HHg~!LG@Rv{QytdlW0b^wA*b$n7S3<4^ZcIrv zJIyJ0C*`|=J#k>&S{ep_nM{W@_)E~byd}VUGm~ALLs+@i%xgbHlIUa9;FXp%?4XIC z7f0~O*hFDH)0H(9pvq>99v_v;WS0CqjinYNsGkWJyvOGbt>dbzyhw~L!Q z72@$up$7S1KCp55hq>G^DuKJ-kLUi6<9YDYc=~*jOz)4g`0MC=`C1{lAfb#~zb`%E z`>c8==HZj{`Nm0^+_Dj)F{rJ-&#{62s+#VyE9)}{T|cf6pCL=UUxs+UOpzGN!DB>$ zX0f}yYo+_UHf7Dn6m#AuMfhZ%ShK`VVO{lBH4$QCoLVvEoehROJBMS^-N1GXux^dn z*Fj0BiEz)9BJTP&hi)T_MKUT^#G#oAme;a0c)AYDR*e^`wXw|$3Y6Lnv;uNOu27z; z9lukc)DYOwh2ry+vOco)yVXS&RT7$4PMFnBe3^QEU_N}hzmnT$RB-7JaJDMP(EVHZ z(|9$2{h0+hY>lsOa2G!ZA$et(TPq>Cfpts9S~A9pFPEw%UhKcLvG&BF9b3z_pBJz` z$i}M+tepF0CEY)_t7Up0m1(YQkFRSQfYk|$=HDw+z{-onD5(~J{m{!sCf z4M}3VDVamD*&K?_WN$wxOF8$u7Tnj)lLB)@Uh}|8NnoWus$`0H%fRjmOT$0a zT5lmM;KN|(y#(&v1kd@yQ-|S(MbLdLT>cBZ90D)sYxu_@8~1*>obzvcg_nG7e3$?S ztqqnz&e@8`#m}(vfWZx{8(1f(Gyt_$fLcp*Q4LXrHAEIxi|48d%yiIqPWACY`)!?q zVMIO`eq`mUDK*Xv^r_P~wF*=XSScs44WX?E_6M()f!z=ki*rA`1ip%b|C*c!-#lzKMoAV>OFi@tr&7j&QM^l-gIswV9#HB zp(_F#RR-}VY3zqHMr~?Q8-f~MprlW$;81uW2g3^SjzR#pTQ z(eoz>ZQI%tC9Dz*Ri1H$k*d`S)Df)!EA?I>XOAvqqq%}aySm89?@vPOY*jSnmT@#G zpReMf-?nBh4r8nN^H+J?v#NxD2f#mk;KA+iPagre2}UJA|BV$~`h5{k9~JN#;PC@6 zG68m`+c_MSq0XTj*oFTe*p(N%!XLjp0?zwhmQfl^6%daHErIajjX1FS5D0Jf-F zO8l%!NP=65G1D$AYdxq^`$KK)Gub#2Ze_7=5!d`wdu(XOBdw|gR!*hpcv#1`td^Jm z)e0a5h{JR0fmP5p5A5h-F8##H;n;HGO0`6s`u@ta@3Ve-hbfJIo7JsGly!C1pY3%0 zD3>c{RPs)ufH4@(84n&`!gcduzz%rX2+s#q@jq`HZ-m2$SON47cqI(J46(`c; zb`-D+PYhV8(+SwfQn+m<%>ytQCi{0BtkN>bE0MiGw<$)nHTv+4I-Hvbu&&krk# zIT~F-U{Z;IwH{O_tb0vXO^3q@nD14@l~X0OEm<aavxP5sI{Z`p{%?u-AB-lbm z_$U_inbq>V{Q-cT*gUXO^}tT72lj@&Xa649c`gC$-c&fJrGM&4@Z_rFhqa*#EFVu^ zTFhT>&Bo*EJfR$+ER=G!$Cuun$%yf}5>n|7OHdoZx+g^GkfngRM-{Zq40DsSh80ZX zI}B6_o`ka=yrEf*H7r{`uV5TiAB##D+DKSwb+7mh-msv>s3T+LR)KrC*WpRf(EkssJ++kE>EzP`U>1!>?wt+hCP( zY$H&28w;Eg(B?DeNFi7JSkoBVuVrs<$GO4v?rcFrIFG5O5Gz>Q0qmF(uKOyVjlQ{r z<~Rs1P{3|HY~_%-R9*m8Tw75`pM`aF*XqsU{XQ3mqKr_|%2 zk*Mb*YelBGkRb|KUJHeP`Jt70j;I_4(M7fRM3%GPP$JK(>i{bmk}OH4Iv&^==gPEm zLttfE+68X8(DK;PN$`!=@g|t+1gt6xEaTNp{uKhqG@(=p=P@O%?SL0D+2mKi(I^}H z%@SA%YGWNV3uuLX`F*`)$b`L};nPPdU};R#gZ{QS=xfst_%(v~E_t>fjuBCWx~ zX=R@|3Qu{#hQuZd2_nh~PIb5hu#%mIz)H3xV;uzSth1$Wt$>~DQou_6W<-r?adrVs zP`c_TFclu3E37(}R3~@{WvkF;b7j8_-kFrkvc36u1{ZL^EFcZD za=;>0EI=;ikR_YRo>u-csl)q!)kH=ClY~{yK$hwdV2@>G$CUHb&v|V2&LSYO6iZ&E zCYee1iz{c%o_x*9hUHZeBfx!T2mJX5Wnp`KQ=x5TcKEMT;B|`xuytTx)fH)3(0>tJ z{v%xbGn_9+flQVoLfUOzo0M*9h%19F0a0|l^ff%LjFPUeg0fX*XyBfELn_w|N~iC? z(i!+-rcf3SJhz;_kB#A$=jU_Pcq?a5=-_(kXLdRBSi$;sF5QOZv=>;Z_5nM(jJw9? zvuuAR`%G5p4r?;!DrjY`T5ct<+q`W8ph5=!V&|`)=8D8tIjVQMl5XPhIiF^7`AD` zsUfhEIcLJDbzs|8+L2cV88o|6AfI3dgAFW2Fh3OjG#SoO^Lvgf(d29~j1u7L(#q(4 z?+`pL{R-t4r?nAUDH;1Jc(sJpWxt6NHkhuDE)@Ty)zY2s(BzVAZjf*~VZGwLbS@Yr z_D5I+ggM6pZ0m9Cd!^j^O+NEHGuRQ3qjh%mapp7GHV=DqRKp9d**S)gd*|# z6`cKEnW)_ga7X5d@8~(MdFo4E53J%<|4Lr*uVRS4iq~~QK~=mao_|AMN$=->;IFs6 z#`Aj$_|KjS9$iz$#a~r$!`LzclPihHuXfgbT}qN2$xu5=I_(73nRvQ#SED#qym^@r zuBa)2aQ1fs+yM|5@90S|)6n{A8)tKbid_}3GQ%kql3m67lXh{|MGpyRzf&M8OWO`* z;c@wTk#M+=F8#k(ppNgry6P7WdFzjQ7g)e~&WId7_$`jF=f=_Rt2EBmU@PdrI5uCx zDlBUrb9bh*%|8cSYB?d+N}X zTsR1iOFj|qu|+_b-9~GhDwKf7MXw9s9~ZG+ngq6kV$~XPyux~`ss@m27WIko$;{i3 zz|6JrOj{Do@aggN{2)tMMVnk(dt!0yJF@I!1scY_jvF zxsul`(0c(Qv{2#D{?r-{$7H(X|D3m&($0DzxS8- z!co@tTkm!ND^%aiUrlhjowbK^cy9Otx?KFQfbYM;VlEfpMl0*7DN`t;Da7izD||(= zVK8Ui_Wp_Am&ZAc;)4*ATTFovfm zrf}iAa!qTYfG@wz(5gDEO~6`6&rfn0{#!E3cBOD2)QTaa0%NAsv4M>(g*AuG;`@7& zucofS+Xn9>z@4k1%hwXxri~=!bQf`L*Dorx+eE$`(y;m8#@i88mOkr#1mKTRfJdWUO?3$-?cgY-i@WMDba3@C?q!Q=iW+LxBeFnuD3l zU7w)2$xlv5;i~tuRBSuarZl2JL9C>n8&#<3Por|V`Lk?ZotDD1Rq?FclZtn2v4m6t zE7cmX@_Q8f4rXrl7q2Z*v#`?Z7@4dVr=K&Sx!Al$B+?2v>!W<`U0=<}co>-|;0-F` z#o$`5nh6(7SGP|IQ|+h(rWaz!cCFPqk`>8J1MAwfoOA4ilM~qXxW!GaY?Q5Lk*fnM zwLS)V{sKLIgh9)|JG*T$Y}4j`8J`^rFJXyiDx<#L!QGF3&Y!P&R=UbU4+%Xctj9kE z$ioH5KPgbJN)#Xm31A1)<({!^bBD(_v8JC^$Sxu|uULSp#i#;M4N~zqgRal{(xuP$ z;ywRP*NYzIi=X#$z*3^Ys?-?Vb%!%qv^9yTE8-bFGm(ExPNv_NY1-7pYv0e{>MSo7sB1)nZ%XKdUuWX z-T#(S$LI{WaS3$&riJYH*A+axIfvKvHU@mZjho(E$)(fb&J8eN4R~kOpHttI0zz_2 zUFv^OveMMLw$&YO1=!ZDtTSW$&sV%I5nsH|=>V-yHVj^>puOs6m}6?L2HQAG+z?n* zS5#YLrUUQTa^Z4XnYliScfQ?8zo#d1;q|WwXdjiG~xS6W}`GH>?IX6^nomhDVt{^lfR2`Fc66!GuoWR~nm5rC$#@kl1y z0+hv;om;Z~Qm#yrur{%-QpZd>$qV9vsBBHnK5d!hUy@nFt;=iZ+Cs?CZLDhWf6l9n z9=8q0g7WL3b(VFSu61<+EC2aajj>Xb{HK)vs6nzgZzvM~?80JA+|L95 z_<{3#zo@~ZEbl;}N3@box7PxcTRgVb*^a0z=!Yc=(2)fqX(S}!9F|A7A<1-o-XOB4 zTj+A{kDS%}ZBhF8hd9k0gd29u(Qx`qXr&B#SU@n$`wya>;d1ZA<16fHvuOtCg zZIkE(X02=Jv8=0AwJOT*_i$`3o}p>%kIdvibSB<$SuC@ZF=(kwAT_wf zhTI=7rAl>wmRaTZ)9F1Si?8>Wa3Cg!BjR`E!0~;N8JZ#rD_zD7mTWYIwSjeQH?Xa` z#chFBntJJ<%b;3ik12Cya92jcZL^`rRJ5UAQ*{tq=DZq8x4301tDsf?lp#{eq6pdX zi7RDaXc0Sgh0NcYz_2g3aO3?S2#B5#5IrX062J`=Rx+7x?-ZfH6na}oTG2OBx%}M> z?)p5P=cZ-TcgQBrx>{K6i=Py54y*@Mv!suJo15FW(sZI zs}`CJm@Jq~m~31LR@>t+1sGV{Ez6E&`O%m|%Z_OoTsm!y*P7@8G2Ycdw_;)FrsrBo z)7{)0yA;PPeNEiWY1HGjU~yULe(8VJ<Fjg&S(H#UL>e< z2ZDNe5~xSNj&Lim8F7rAE-QMTk`)7=$L4$hug&h38#GHg+b|$?H|S1G0Q-YO!R}}j zI1JAKwShJ0Rip=^9`FsQa|VFg*x#JYeiTi1Uwn*xE+wZoXfn;wX7#L{j2`Bs_XJ&5 zB&gEcf@69MaM~RLF01{)>0pQu^=_nM{4bNmDZkHFjQM&9bbGQL=u?A0pAiTyX#wDZ zYv+P%s7mprHB~;~4fe-9DGu50;P{~HWzU^5+6(mK+tQD_zU13z@FyYA=gmOy&2tC0 zsja|nd=vI(Z*?s9G27p?*KyC&-bVurD%r5a%8$YrhLyEzyKfik5t zIIjx=yJSB^(^fI!s}a(|f>mPlxdXzYf;Ea!-wcDmM_Yj1E?=-a=zj@mwlApmHv#pY z#c)ze z_)*w)yVei3eza^n-l^o_@{P;zQh3Cayc-`}IGe#muIoiZT6}ISp741?T7MC4QsrxS zt#c-{d?A1~l|42uZa&(G7-|n^#Ou5#2%HhH6XJEk=A|HBov{P865I3y_vn3~KZ!~0 z4=&qAgUgOlpib`xs#6Bg9_>l6Ouz{2No=*mlc4E!@dQ>lJCRi{SVWE)`7Ed&h+-|u zkY3)PS=}iv7*%c*sPHk{?Tr9ob0~;wJK?Xl7kYoxOPH4*FU1y~5SN#}AjTCR5@-K4 z55ivR2+Gxs!EQUQ%RvjIX^8ZI4`_3}Kxb;xJVbiT%@S&z&z0wP9khy$gcd8BgYN-% z2wu??TE-c`ZJTSIe+T!#O%K0^*}i5-xklxok`Xor+s1lrU>VxFj%O(hEo)mIuIFQf zmv3H%_@+m**V&vS*26EjC33F^wNmuCv5560Ix(~XlPrHP#g}do#{CpYXq|8)+3)i- zZAu#R2rq6{!kc{&Z+bArA*~gD9||f2tIIYpxIAKlqcQd}gTi)YXr0shfXCM#fX7oZ z=+|Rb%HXxDSLq;UI453gY^%rpKuWe{GyF z`Il8c z8CKh1>qSdoSuD|7!eyf}!%Jc9dnz!Rwb2^IcSb$>#415bj25S{vgqX4XkipBTuFKWm!Q(bS#?u|0Qn194*)5O#z@ zz_GrHejiR3ru?x@TzcVIX+_zOVshm_cozE;F|H&FZ%Ld19bRn@%Jp8R(AdkGlp$p! zFKbf<)y;%7PfzC&obxp7ofc@#`RC`_Lr`vO_&v_{J#)ZAwZzJqx)Q8c2bOEvI@^}P zwZzM}FCPZ_mW5j125{@3r4gSt6%&dV<0gI?@m84d;`fz!lIlG%u5gDm^Y@Wpc%vn? zuj3*;I3yzAE!aK?m^Ci~rcoKkOHk*8(b&m1E`!C;s&+<#=f_##{rBbfCTnNWbzbXO zdl!Rx-eDZcr-CwNG$;>*gAzwuVOux)vG2(~3d7qIh4DWoiwn*i5#vig5R&`Nw>-G0mM6S;j+2Jgnta=Tvr|uQ!=g$&Ge;);1pKE4nUIOe8?{41ws}b@he~?mI zf^vTsa56?v>uOV2+fwM5)APG;YriecuQGUiKsR9Er5y@g>T$xhbquWnhkjq+|FH_3 zj!p&d6N?(Qe=b@W`bDfb_0L_>qH~YnSpSKbgw2^;bwNz5{9BAKc}JXoCPSR~>pa2m zcDNbnQV+0S?TN@66HInI%7D_So9D%v6kC*#(phze)GJp@ZOV_mx-AHGTg~Tw-V@>X zG!K*oE$~pU8*BYw>pu}?8!r#|7KRqjGEdfyZO~SmpYZai2Y(SLEjlw?N+{ck0A9tB zxqvSAZ@{b~#Q&F8RD38Uls+IvpWi7gD2S1!{W(!F>e~TApARCS)AOAm;GquSezY^S zt{U+=9*o2$3x+?RPe%w4{8<_d)&id7kHJCN4ouT(Te!}d(qQsc9 z^YHraUMaESX$13)w6gjfwy{i@*VViSRTY9N5|gX)@z4A!C02clcRYQKE%+7YQ)y|* zPvYEiS)5lUiwiJ|@y9V$vJ_uVpTQy};`d1v_Ybsvgmka0C8j+w_xn^e7- zHY<&N?DsT_oWbuAH|YC%h%ol6P{oYD2jSC8>K-`u3W|FL%u zuyS=l9Ka|3zG_|DMo`TXWL-y}^7ey&FKSJR#VG7ScQvb&ZbAgEo!v1z{F)UA`GfGl{ZPlpo{ijs)X_AUO z%!&8!#Kj~PZ0e;k_O66|Y_}U!VDmsKxULr!TH63uN(?l#+&IbIhU*q{|LNmopz^?o z_oG}z8EN#CrGsaYeC9;`u(LG4DS^5{MMwwh3ReNFfc`i6CCoZxeP1r8na-lenxE6* z-P^WJkny z$cz^sIqz#R_`l45+ZoN|1y-Z^9ZTe5w--@K`TjA3-GU11w;qN^ETizzdVtB`d6~=A z6DPPHP_CS%S7BL;_>CgV#1fY~bru1rtx4Su)A7R&LNe}PvmdehUR;4JsxEhe+243Z z-|YF)RQ%Bn0@la5fL&`;*lrK0(7H}&r04!zFN~l2bnVk4D}QcEH*t!QF9qk{LK)yx zz-D{g0lOns`>y3d6)=@}u%Al3Qy(VBYmJd+7*40i&;|7W#S&Ov`#8lNsO4Y?;ZyG# zGYps+$4Nl~uzvpXG8#!2E#K`L%@}bH>=U50Vtt;ZVwX}4{W;aO_YI03sKLVv0;jRx zyo~)OB3k1dPB;Uo0oV-W-|c}VT78zd_`}^)>Vy4>)Drj5KTN2uoj+4_TEE<8F10Z( zBr`|@r~%l_@4eH7E1%i1qdi-biXMMOC2paj`qMeZuO%ry2F?D!8XGl|f7QNiINHRj zRaFO|SphWw8{ky{i{XFyHkL#i?b)7UgD+Ej!UxR1#>aU`yL(t$exgX%QxqNDfh($* zyVPbb6{rGefHVLbR9=Br8~*3M&p52|9Fzx>h89bhmN15p(1 z(z>*)pldEDHFzdTRLa3!!tu%XrtU8D3vWgWDzF}^^7y|y{BI7V`JJCg0#>jJSa2b|Ibb1%Q*S1JZ!v*)sM`Pl002ovPDHLkV1o9T|BV0u literal 0 HcmV?d00001 diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.component.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.component.tsx new file mode 100644 index 000000000000..d21821514fab --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.component.tsx @@ -0,0 +1,59 @@ +import React, { FunctionComponent } from 'react'; +import { OsdsLink, OsdsText } from '@ovhcloud/ods-components/react'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { ODS_TEXT_SIZE } from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { ButtonType, useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import useGuideUtils from '@/hooks/guideUtils/useGuideUtils'; +import assistance from '../assets/assistance.png'; + +export const HubSupportHelp: FunctionComponent = () => { + const { t } = useTranslation('hub/support'); + const { trackClick } = useOvhTracking(); + const guide = useGuideUtils(); + const helpUrl = guide.Home; + + const handleClick = () => { + trackClick({ + buttonType: ButtonType.link, + actionType: 'navigation', + actions: ['activity', 'assistance', 'guide-welcome', 'go-to-docs'], + }); + }; + + return ( + <> +
+ assistance +
+ + {t('hub_support_need_help')} + + + {t('hub_support_need_help_more')} + + + {t('hub_support_help')} + + + ); +}; diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.spec.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.spec.tsx new file mode 100644 index 000000000000..adf929491465 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-help/HubSupportHelp.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import { HubSupportHelp } from './HubSupportHelp.component'; + +vi.mock('@/hooks/guideUtils/useGuideUtils', () => ({ + default: vi.fn().mockReturnValue({ + Home: 'https://docs.ovh.com/fr/', + }), +})); + +const trackClickMock = vi.fn(); +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + useOvhTracking: () => ({ trackClick: trackClickMock }), + ButtonType: { + link: 'link', + }, +})); + +describe('HubSupportHelp Component', () => { + it('renders help text correctly', () => { + render(); + + expect(screen.getByText('hub_support_need_help')).toBeInTheDocument(); + expect(screen.getByText('hub_support_need_help_more')).toBeInTheDocument(); + expect(screen.getByText('hub_support_help')).toBeInTheDocument(); + }); + + it('renders the correct help URL', () => { + render(); + + const linkElement = screen + .getByText('hub_support_help') + .closest('osds-link'); + expect(linkElement).toHaveAttribute('href', 'https://docs.ovh.com/fr/'); + expect(linkElement).toHaveAttribute('target', '_blank'); + }); + + it('calls trackClick on link click', async () => { + render(); + + const linkElement = screen.getByText('hub_support_help'); + + await act(() => fireEvent.click(linkElement)); + + expect(trackClickMock).toHaveBeenCalledWith({ + buttonType: 'link', + actionType: 'navigation', + actions: ['activity', 'assistance', 'guide-welcome', 'go-to-docs'], + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.component.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.component.tsx new file mode 100644 index 000000000000..b5bb488b3d78 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.component.tsx @@ -0,0 +1,28 @@ +import { OsdsTable } from '@ovhcloud/ods-components/react'; +import React, { FunctionComponent } from 'react'; +import { Ticket } from '@/types/support.type'; +import { MAX_TICKETS_TO_DISPLAY } from '../HubSupport.constants'; +import { HubSupportTableItem } from './hub-support-table-item/HubSupportTableItem.component'; + +type Props = { + tickets: Ticket[]; +}; + +export const HubSupportTable: FunctionComponent = ({ tickets }) => { + // This code below is superfluous because the API always returns only 2 tickets. + const limitedTickets = tickets.slice(0, MAX_TICKETS_TO_DISPLAY); + return ( + + + + {limitedTickets.map((ticketItem) => ( + + ))} + +
+
+ ); +}; diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.spec.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.spec.tsx new file mode 100644 index 000000000000..63f196d8c213 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/HubSupportTable.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import { Ticket } from '@/types/support.type'; +import { HubSupportTable } from './HubSupportTable.component'; + +vi.mock('./hub-support-table-item/HubSupportTableItem.component', () => ({ + HubSupportTableItem: ({ ticket }: { ticket: Ticket }) => ( + + {ticket.ticketId} + + ), +})); + +describe('HubSupportTable Component', () => { + it('renders a table with a maximum of 2 support items', () => { + const tickets: Ticket[] = [ + { + ticketId: '1', + serviceName: 'service 1', + state: 'state 1', + subject: 'subject 1', + }, + { + ticketId: '2', + serviceName: 'service 2', + state: 'state 2', + subject: 'subject 2', + }, + { + ticketId: '3', + serviceName: 'service 3', + state: 'state 3', + subject: 'subject 3', + }, + ]; + + render(); + + expect(screen.getByTestId('support-item-1')).toBeInTheDocument(); + expect(screen.getByTestId('support-item-2')).toBeInTheDocument(); + expect(screen.queryByTestId('support-item-3')).not.toBeInTheDocument(); + + const table = screen.getByRole('table'); + expect(table).toBeInTheDocument(); + expect(table.querySelectorAll('tr')).toHaveLength(2); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.component.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.component.tsx new file mode 100644 index 000000000000..fc3c6be2b228 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.component.tsx @@ -0,0 +1,113 @@ +import { OsdsChip, OsdsLink } from '@ovhcloud/ods-components/react'; +import React, { + FunctionComponent, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { ODS_CHIP_SIZE } from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; +import { + ButtonType, + PageLocation, + ShellContext, + useOvhTracking, +} from '@ovh-ux/manager-react-shell-client'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { Ticket } from '@/types/support.type'; +import { SUPPORT_URLS } from '../../HubSupport.constants'; + +type Props = { + ticket: Ticket; +}; + +export const HubSupportTableItem: FunctionComponent = ({ ticket }) => { + const { t } = useTranslation('hub/support'); + + // The switch case below is superfluous as the API only returns open tickets. + // Since all tickets have the state 'open', all switch cases except 'open' will never be reached. + const stateColor: ODS_THEME_COLOR_INTENT = useMemo(() => { + switch (ticket.state) { + case 'open': + return ODS_THEME_COLOR_INTENT.success; + case 'closed': + return ODS_THEME_COLOR_INTENT.info; + case 'unknown': + return ODS_THEME_COLOR_INTENT.warning; + default: + return ODS_THEME_COLOR_INTENT.error; + } + }, [ticket]); + + const context = useContext(ShellContext); + const { + shell: { navigation }, + environment, + } = context; + const { ovhSubsidiary } = environment.getUser(); + const { trackClick } = useOvhTracking(); + const region = environment.getRegion(); + + const [url, setUrl] = useState(''); + + const isEUOrCA = ['EU', 'CA'].includes(region); + + useEffect(() => { + (async () => { + const linkResult: string = isEUOrCA + ? SUPPORT_URLS.viewTicket.replace('{ticketId}', ticket.ticketId) + + ovhSubsidiary + : ((await navigation.getURL( + 'dedicated', + `#/support/tickets/${ticket.ticketId}`, + {}, + )) as string); + + setUrl(linkResult); + })(); + }, [ticket.ticketId]); + + const handleClick = () => { + trackClick({ + location: PageLocation.datagrid, + buttonType: ButtonType.link, + actionType: 'navigation', + actions: ['activity', 'assistance', 'go-to-ticket'], + }); + }; + + return ( + + + {ticket.serviceName} + + {ticket.subject} + + + {t(`hub_support_state_${ticket.state}`)} + + + + + {t('hub_support_read')} + + + + ); +}; diff --git a/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.spec.tsx b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.spec.tsx new file mode 100644 index 000000000000..5d37cd551455 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/hub-support/hub-support-table/hub-support-table-item/HubSupportTableItem.spec.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { + render, + screen, + fireEvent, + act, + waitFor, +} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { Ticket } from '@/types/support.type'; +import { HubSupportTableItem } from './HubSupportTableItem.component'; + +const trackClickMock = vi.fn(); + +const mocks = vi.hoisted(() => ({ + environment: { + getRegion: vi.fn(), + getUser: () => ({ ovhSubsidiary: 'FR' }), + }, + shell: { + navigation: { + getURL: vi.fn().mockResolvedValue('mocked-url'), + }, + }, +})); + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + ShellContext: React.createContext({ + shell: mocks.shell, + environment: mocks.environment, + }), + useOvhTracking: () => ({ trackClick: trackClickMock }), + PageLocation: { + datagrid: 'datagrid', + }, + ButtonType: { + link: 'link', + }, +})); + +const mockTicket: Ticket = { + ticketId: '123', + serviceName: 'Service A', + subject: 'Subject A', + state: 'open', +}; + +describe('HubSupportTableItem Component', () => { + it('renders support information correctly for EU region', async () => { + mocks.environment.getRegion.mockReturnValue('EU'); + + render(); + + expect(screen.getByText(mockTicket.serviceName)).toBeInTheDocument(); + expect(screen.getByText(mockTicket.subject)).toBeInTheDocument(); + expect(screen.getByText('hub_support_state_open')).toBeInTheDocument(); + await screen.findByText('hub_support_read'); + const osdsLinkElement = screen + .getByText('hub_support_read') + .closest('osds-link'); + expect(osdsLinkElement).toHaveAttribute( + 'href', + 'https://help.ovhcloud.com/csm?id=csm_ticket&table=sn_customerservice_case&number=CS123&view=csp&ovhSubsidiary=FR', + ); + expect(osdsLinkElement).toHaveAttribute('target', '_blank'); + expect(osdsLinkElement).toHaveAttribute('rel', 'noreferrer'); + }); + + it('renders support information correctly for US region', async () => { + mocks.environment.getRegion.mockReturnValue('US'); + + render(); + + await screen.findByText('hub_support_read'); + await waitFor(() => { + const osdsLinkElement = screen + .getByText('hub_support_read') + .closest('osds-link'); + expect(osdsLinkElement).toHaveAttribute('href', 'mocked-url'); + expect(osdsLinkElement).toHaveAttribute('target', '_self'); + expect(osdsLinkElement).not.toHaveAttribute('rel'); + expect(screen.getByText(mockTicket.serviceName)).toBeInTheDocument(); + expect(screen.getByText(mockTicket.subject)).toBeInTheDocument(); + expect(screen.getByText('hub_support_state_open')).toBeInTheDocument(); + }); + }); + + it('calls trackClick on link click', async () => { + mocks.environment.getRegion.mockReturnValue('EU'); + + render(); + + const linkElement = screen.getByText('hub_support_read'); + expect(linkElement).toBeInTheDocument(); + + await act(() => fireEvent.click(linkElement)); + + expect(trackClickMock).toHaveBeenCalledWith({ + location: 'datagrid', + buttonType: 'link', + actionType: 'navigation', + actions: ['activity', 'assistance', 'go-to-ticket'], + }); + }); + + it('applies correct color based on support state', async () => { + mocks.environment.getRegion.mockReturnValue('EU'); + + render(); + + const chip = screen.getByText('hub_support_state_closed'); + expect(chip).toHaveAttribute('color', ODS_THEME_COLOR_INTENT.info); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/products/Products.component.tsx b/packages/manager/apps/hub-react/src/components/products/Products.component.tsx new file mode 100644 index 000000000000..bc0604a01933 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/products/Products.component.tsx @@ -0,0 +1,221 @@ +import React, { Suspense, useState } from 'react'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { + OsdsButton, + OsdsChip, + OsdsIcon, + OsdsLink, + OsdsSkeleton, + OsdsText, + OsdsTile, +} from '@ovhcloud/ods-components/react'; +import { Await, useSearchParams } from 'react-router-dom'; +import { + ODS_BUTTON_TYPE, + ODS_BUTTON_VARIANT, + ODS_CHIP_SIZE, + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_SKELETON_SIZE, + ODS_TEXT_COLOR_HUE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { useTranslation } from 'react-i18next'; +import punycode from 'punycode/punycode'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { HubProduct, ProductList } from '@/types/services.type'; +import { useProducts } from '@/hooks/products/useProducts'; +import './Products.style.scss'; + +type ProductsProps = { + services: ApiEnvelope; +}; + +export default function Products({ services }: ProductsProps) { + const { t } = useTranslation('hub/products'); + const { t: tCommon } = useTranslation(); + const [searchParams] = useSearchParams(); + const [expand, setExpand] = useState(searchParams.get('expand') === 'true'); + const { trackClick } = useOvhTracking(); + + const { isLoading, products, canDisplayMore } = useProducts( + services.data, + expand, + ); + + const trackProductNavigation = (product: HubProduct) => { + trackClick({ + actionType: 'action', + actions: ['product', product.formattedType, 'show-all'], + }); + }; + const trackServiceNavigation = (productType: string) => { + trackClick({ + actionType: 'action', + actions: ['product', productType, 'go-to-service'], + }); + }; + const updateExpand = () => { + setExpand(!expand); + trackClick({ + actionType: 'action', + actions: ['product', 'show_more'], + }); + }; + + // isLoading is misleading here, the task done during this "loading" time is synchronous (MANAGER-15037) + return ( + <> + {products.length > 0 && ( + + {t('manager_hub_dashboard_services')} + + )} +
+ {products.map((product) => ( +
+ +
+
+ + {t(`manager_hub_products_${product.type}`)} + + {product.count} + + + {product.link && ( + + } + > + ( + <> + { + trackProductNavigation(product); + }} + target={OdsHTMLAnchorElementTarget._top} + className="text-right" + data-testid="product_link" + > + {tCommon('hub_support_see_more')} + + + + + + )} + /> + + )} +
+
+
    + {product.data.map((service) => ( +
  • + { + trackServiceNavigation(product.formattedType); + }} + target={OdsHTMLAnchorElementTarget._top} + data-testid="service_link" + > + {punycode.toUnicode( + service.resource.displayName || + service.resource.name, + )} + +
  • + ))} +
+
+
+
+
+ ))} +
+
+ {canDisplayMore && !isLoading && ( + + + {t( + expand + ? 'manager_hub_products_see_less' + : 'manager_hub_products_see_more', + )} + + + + + + )} +
+ + ); +} diff --git a/packages/manager/apps/hub-react/src/components/products/Products.constants.ts b/packages/manager/apps/hub-react/src/components/products/Products.constants.ts new file mode 100644 index 000000000000..9e48072f0de9 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/products/Products.constants.ts @@ -0,0 +1,192 @@ +export const productListingPages: Record< + string, + { application: string; hash: string } +> = { + CDN_DEDICATED: { + application: 'dedicated', + hash: '#/configuration/cdn', + }, + CLOUD_DB_ENTERPRISE_CLUSTER: { + application: 'dedicated', + hash: '#/enterprise-cloud-database', + }, + CLOUD_PROJECT: { + application: 'public-cloud', + hash: '#/pci/projects', + }, + DBAAS_LOGS: { + application: 'dedicated', + hash: '#/dbaas/logs', + }, + DEDICATED_CEPH: { + application: 'dedicated', + hash: '#/cda', + }, + DEDICATED_CLOUD: { + application: 'dedicated', + hash: '#/dedicated_cloud', + }, + DEDICATED_HOUSING: { + application: 'dedicated', + hash: '#/housing', + }, + DEDICATED_NASHA: { + application: 'dedicated', + hash: '#/nasha', + }, + DEDICATED_SERVER: { + application: 'dedicated', + hash: '#/server', + }, + DOMAIN: { + application: 'web', + hash: '#/domain', + }, + DOMAIN_ZONE: { + application: 'web', + hash: '#/zone', + }, + EMAIL_DOMAIN: { + application: 'web', + hash: '#/email_domain', + }, + EMAIL_EXCHANGE_SERVICE: { + application: 'web', + hash: '#/exchange', + }, + EMAIL_PRO: { + application: 'web', + hash: '#/email_pro', + }, + ESSENTIALS: { + application: 'dedicated', + hash: '#/managedBaremetal', + }, + FREEFAX: { + application: 'telecom', + hash: '#/freefax', + }, + HOSTING_PRIVATE_DATABASE: { + application: 'web', + hash: '#/private_database', + }, + HOSTING_WEB: { + application: 'web', + hash: '#/hosting', + }, + IP_LOADBALANCING: { + application: 'dedicated', + hash: '#/iplb', + }, + IP_SERVICE: { + application: 'dedicated', + hash: '#/ip', + }, + KEY_MANAGEMENT_SERVICE: { + application: 'key-management-service', + hash: '#', + }, + LICENSE_CPANEL: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_CLOUD_LINUX: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_DIRECTADMIN: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_DIRECT_ADMIN: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_OFFICE: { + application: 'web', + hash: '#/office/license', + }, + LICENSE_PLESK: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_SQLSERVER: { + application: 'dedicated', + hash: '#/license', + }, + LICENCE_VIRTUOZZO: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_WINDOWS: { + application: 'dedicated', + hash: '#/license', + }, + LICENSE_WORKLIGHT: { + application: 'dedicated', + hash: '#/license', + }, + MS_SERVICES_SHAREPOINT: { + application: 'web', + hash: '#/sharepoint', + }, + NUTANIX: { + application: 'dedicated', + hash: '#/nutanix', + }, + OVH_CLOUD_CONNECT: { + application: 'dedicated', + hash: '#/cloud-connect', + }, + OVER_THE_BOX: { + application: 'telecom', + hash: '#/overTheBox', + }, + PACK_XDSL: { + application: 'telecom', + hash: '#/pack', + }, + SMS: { + application: 'telecom', + hash: '#/sms', + }, + STORAGE_NETAPP: { + application: 'dedicated', + hash: '#/netapp', + }, + TELEPHONY: { + application: 'telecom', + hash: '#/telephony', + }, + VEEAM_CLOUD_CONNECT: { + application: 'dedicated', + hash: '#/veeam', + }, + VEEAM_VEEAM_ENTERPRISE: { + application: 'dedicated', + hash: '#/veeam-enterprise', + }, + VPS: { + application: 'dedicated', + hash: '#/vps', + }, + VRACK: { + application: 'dedicated', + hash: '#/vrack', + }, + WEB_PAA_S_SUBSCRIPTION: { + application: 'web', + hash: '#/paas/webpaas/projects', + }, + WEB_PAAS_SUBSCRIPTION: { + application: 'web', + hash: '#/paas/webpaas/projects', + }, + XDSL: { + application: 'telecom', + hash: '#/pack', + }, +}; + +export const DEFAULT_DISPLAYED_PRODUCTS = 6; +export const DEFAULT_DISPLAYED_SERVICES = 4; diff --git a/packages/manager/apps/hub-react/src/components/products/Products.spec.tsx b/packages/manager/apps/hub-react/src/components/products/Products.spec.tsx new file mode 100644 index 000000000000..a69b7e68fc73 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/products/Products.spec.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { act, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import Products from '@/components/products/Products.component'; +import { ProductList } from '@/types/services.type'; +import { aFewProductsMocked, lotsOfProductsMocked } from '@/_mock_/products'; + +const trackClickMock = vi.fn(); +const url = 'https://fake-link.com'; + +vi.mock('react-router-dom', async (importOriginal) => { + const original: typeof import('react-router-dom') = await importOriginal(); + return { + ...original, + useSearchParams: () => [{ get: (str: string) => str }], + useMatches: vi.fn(() => [ + { + data: 'foo', + handle: { + crumb: (i: string) => `crumb-${i}`, + }, + }, + { + data: 'bar', + handle: { + crumb: (i: string) => `crumb-${i}`, + }, + }, + { + data: 'baz', + handle: {}, + }, + ]), + }; +}); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const original: typeof import('@ovh-ux/manager-react-shell-client') = await importOriginal(); + return { + ...original, + useOvhTracking: () => ({ trackClick: trackClickMock }), + }; +}); + +const shellContext = { + environment: { + getRegion: vi.fn(() => 'EU'), + }, + shell: { + navigation: { + getURL: vi.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve(url), 50); + }), + ), + }, + }, +}; + +const renderComponent = (services: ProductList) => { + return render( + + + , + ); +}; + +describe('Products.component', () => { + it('should display a skeleton while loading', async () => { + const { getByTestId } = renderComponent({ count: 3, data: {} }); + + const container = getByTestId('products-list-container'); + + expect(container.children.length).toBe(0); + }); + + it('should display nothing if user has no services', async () => { + const { getByTestId } = renderComponent({ count: 3, data: {} }); + + const container = getByTestId('products-list-container'); + + expect(container.children.length).toBe(0); + }); + + it('should display correctly a signle product with a single service', async () => { + const [productName] = Object.keys(aFewProductsMocked.data); + const { + getByText, + getByTestId, + findByTestId, + queryByTestId, + } = renderComponent(aFewProductsMocked); + + const productAnchor = await findByTestId('product_link'); + const servicesList = getByTestId('product_services_list'); + + expect(getByTestId('products_title')).not.toBeNull(); + expect(getByText(`manager_hub_products_${productName}`)).not.toBeNull(); + expect(getByText('displayName1')).not.toBeNull(); + expect(servicesList).not.toBeNull(); + expect(servicesList.children.length).toBe(1); + expect(queryByTestId('expand_link')).not.toBeInTheDocument(); + expect(productAnchor).not.toBeNull(); + expect(productAnchor).toHaveAttribute('href', url); + + await act(() => fireEvent.click(productAnchor)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: [ + 'product', + productName.toLowerCase().replace(/_/g, '-'), + 'show-all', + ], + }); + + const serviceAnchor = await findByTestId('service_link'); + + await act(() => fireEvent.click(serviceAnchor)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: [ + 'product', + productName.toLowerCase().replace(/_/g, '-'), + 'go-to-service', + ], + }); + }); + + it('should call onClick when link is clicked', async () => { + const { getByTestId, getByText, findByTestId } = renderComponent( + lotsOfProductsMocked, + ); + + const expandLink = await findByTestId('expand_link'); + const productsList = getByTestId('products-list-container'); + + expect(productsList).not.toBeNull(); + expect(productsList.children.length).toBe(6); + expect(getByText('cdn_dedicated_3')).not.toBeNull(); + + await act(() => fireEvent.click(expandLink)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['product', 'show_more'], + }); + + expect(productsList.children.length).toBe( + Object.keys(lotsOfProductsMocked.data).length, + ); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/products/Products.style.scss b/packages/manager/apps/hub-react/src/components/products/Products.style.scss new file mode 100644 index 000000000000..859e6e2fc482 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/products/Products.style.scss @@ -0,0 +1,11 @@ +.services-list:nth-child(2) { + opacity: 0.75; +} + +.services-list:nth-child(3) { + opacity: 0.5; +} + +.services-list:nth-child(4) { + opacity: 0.35; +} diff --git a/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.component.tsx b/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.component.tsx new file mode 100644 index 000000000000..b797f9b1f3dd --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.component.tsx @@ -0,0 +1,11 @@ +import React, { FunctionComponent } from 'react'; +import { OsdsSkeleton } from '@ovhcloud/ods-components/react'; + +export const Skeletons: FunctionComponent = () => ( + <> + + + + + +); diff --git a/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.spec.tsx b/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.spec.tsx new file mode 100644 index 000000000000..68e76f1d3453 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/skeletons/Skeletons.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import { Skeletons } from './Skeletons.component'; + +vi.mock('@ovhcloud/ods-components/react', () => ({ + OsdsSkeleton: ({ + inline, + randomized, + }: { + inline: boolean; + randomized: boolean; + }) => ( +
+ ), +})); + +describe('Skeletons Component', () => { + it('renders four OsdsSkeleton components', () => { + render(); + + const skeletons = screen.getAllByTestId('osds-skeleton'); + + expect(skeletons).toHaveLength(4); + + skeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-inline', 'true'); + expect(skeleton).toHaveAttribute('data-randomized', 'true'); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.spec.tsx b/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.spec.tsx new file mode 100644 index 000000000000..940e37febe22 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + render, + screen, + fireEvent, + act, + waitFor, +} from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import TileError from './TileError.component'; + +describe('TileError Component', () => { + it('renders title, message, and button correctly', () => { + const refetchMock = vi.fn(); + + render(); + + expect(screen.getByText('manager_hub_error_tile_oops')).toBeInTheDocument(); + expect(screen.getByText('Error message details')).toBeInTheDocument(); + expect( + screen.getByText('manager_hub_error_tile_retry'), + ).toBeInTheDocument(); + }); + + it('calls refetch function when the button is clicked', async () => { + const refetchMock = vi.fn(); + + render(); + + const button = screen.getByText('manager_hub_error_tile_retry'); + await act(() => fireEvent.click(button)); + await waitFor(() => { + expect(refetchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.tsx b/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.tsx new file mode 100644 index 000000000000..f46a100f8d4b --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-error/TileError.component.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { OsdsButton, OsdsIcon, OsdsText } from '@ovhcloud/ods-components/react'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; + +type Props = { + message: string; + refetch: () => void; + className?: string; + contrasted?: boolean; +}; + +export default function TileError({ + message, + refetch, + className, + contrasted, +}: Props) { + const { t } = useTranslation('hub/error'); + return ( +
+ + {t('manager_hub_error_tile_oops')} + + + {message} + + + {t('manager_hub_error_tile_retry')} + + + + +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.component.tsx b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.component.tsx new file mode 100644 index 000000000000..b54c52dc0757 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.component.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { OsdsSkeleton } from '@ovhcloud/ods-components/react'; +import { ODS_SKELETON_SIZE } from '@ovhcloud/ods-components'; +import TileSkeleton from '@/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component'; + +export default function TileGridSkeleton() { + return ( + <> + +
+ + + +
+ + ); +} diff --git a/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.spec.tsx b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.spec.tsx new file mode 100644 index 000000000000..5733464a64d6 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/TileGridSkeleton.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import TileGridSkeleton from '@/components/tile-grid-skeleton/TileGridSkeleton.component'; + +describe('TileGridSkeleton', () => { + it('should display a skeleton for the title and 4 inner skeletons', async () => { + const { getByTestId } = render(); + + expect(getByTestId('tile_grid_title_skeleton')).not.toBeNull(); + const contentContainer = getByTestId('tile_grid_content_skeletons'); + expect(contentContainer).not.toBeNull(); + expect(contentContainer.children.length).toBe(3); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component.tsx b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component.tsx new file mode 100644 index 000000000000..881e44580f4f --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { OsdsSkeleton, OsdsTile } from '@ovhcloud/ods-components/react'; +import { ODS_SKELETON_SIZE } from '@ovhcloud/ods-components'; + +export default function TileSkeleton() { + return ( + +
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.spec.tsx b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.spec.tsx new file mode 100644 index 000000000000..4af4c94355f1 --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import TileSkeleton from '@/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component'; + +describe('TileGridSkeleton', () => { + it('should display a skeleton for the title and 4 inner skeletons', async () => { + const { getByTestId } = render(); + + const headerContainer = getByTestId('tile_skeleton_header'); + expect(headerContainer).not.toBeNull(); + expect(headerContainer.children.length).toBe(2); + const contentContainer = getByTestId('tile_skeleton_content'); + expect(contentContainer).not.toBeNull(); + expect(contentContainer.children.length).toBe(2); + }); +}); diff --git a/packages/manager/apps/hub-react/src/components/welcome/Welcome.component.tsx b/packages/manager/apps/hub-react/src/components/welcome/Welcome.component.tsx new file mode 100644 index 000000000000..062e471d988f --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/welcome/Welcome.component.tsx @@ -0,0 +1,56 @@ +import React, { useContext } from 'react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { + ODS_CHIP_SIZE, + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_COLOR_HUE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { OsdsChip, OsdsIcon, OsdsText } from '@ovhcloud/ods-components/react'; +import { useTranslation } from 'react-i18next'; + +export default function Welcome() { + const { t } = useTranslation('hub'); + const { environment } = useContext(ShellContext); + const { user } = environment; + + return ( + <> + + {t('manager_hub_dashboard_welcome', { name: user.firstname })} + + {user.isTrusted && ( + + + + {t('ovh_trusted_nic_label')} + + + )} + + ); +} diff --git a/packages/manager/apps/hub-react/src/components/welcome/Welcome.spec.tsx b/packages/manager/apps/hub-react/src/components/welcome/Welcome.spec.tsx new file mode 100644 index 000000000000..babb576e4dbf --- /dev/null +++ b/packages/manager/apps/hub-react/src/components/welcome/Welcome.spec.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import Welcome from '@/components/welcome/Welcome.component'; + +const user = { + firstname: 'first-test', + isTrusted: false, +}; + +const shellContext = { + environment: { + user, + }, +}; + +const renderComponent = () => { + return render( + + + , + ); +}; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (translationKey: string, params: any) => + `${translationKey} ${params?.name}`, + }), +})); + +describe('Welcome.component', () => { + it('should display customer first name', async () => { + const { getByText } = renderComponent(); + + expect( + getByText(`manager_hub_dashboard_welcome ${user.firstname}`), + ).not.toBeNull(); + }); + + it('should not display SNC badge for non trusted customer', async () => { + const { queryByTestId } = renderComponent(); + + expect(queryByTestId('snc_chip')).not.toBeInTheDocument(); + }); + + it('should display SNC badge for trusted customer', async () => { + user.isTrusted = true; + const { getByTestId } = renderComponent(); + + expect(getByTestId('snc_chip')).not.toBeNull(); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/api/apiHubSupport.ts b/packages/manager/apps/hub-react/src/data/api/apiHubSupport.ts new file mode 100644 index 000000000000..e81963f021e9 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/apiHubSupport.ts @@ -0,0 +1,16 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { AxiosResponse } from 'axios'; +import { SupportResponse } from '@/types/support.type'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export const getHubSupport: ( + cached: boolean, +) => Promise>> = (cached: boolean) => + aapi.get( + '/hub/support', + !cached && { + headers: { + Pragma: 'no-cache', + }, + }, + ); diff --git a/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.constants.ts b/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.constants.ts new file mode 100644 index 000000000000..cbef180fa4f3 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.constants.ts @@ -0,0 +1,13 @@ +const FRAUD_DOCS_REQUESTED = 'fraudDocsRequested'; +const FRAUD_MANUAL_REVIEW = 'fraudManualReview'; +const FRAUD_REFUSED = 'fraudRefused'; +export const NOT_PAID = 'notPaid'; + +export const ERROR_STATUS = [ + FRAUD_DOCS_REQUESTED, + FRAUD_MANUAL_REVIEW, + FRAUD_REFUSED, + NOT_PAID, +]; + +export const WAITING_PAYMENT_LABEL = 'custom_payment_waiting'; diff --git a/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.ts b/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.ts new file mode 100644 index 000000000000..c63b3f04909b --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/apiOrder/apiOrder.ts @@ -0,0 +1,85 @@ +import { aapi, v6 } from '@ovh-ux/manager-core-api'; +import { + OrderFollowUpResponse, + OrderResponseData, + OrderDetailsResponse, + OrderStatus, + OrderHistory, + LastOrderTrackingResponse, +} from '@/types/order.type'; +import { NOT_PAID, WAITING_PAYMENT_LABEL } from './apiOrder.constants'; + +export const getLastOrder: () => Promise = async () => + aapi.get('/hub/lastOrder').then(({ data }) => { + return data.data.lastOrder; + }); + +export const getOrderDetails = async ( + orderId: number, +): Promise => { + const { data } = await v6.get(`/me/order/${orderId}/details`); + return data as OrderDetailsResponse; +}; + +export const getOrderStatus = async (orderId: number): Promise => { + const { data } = await v6.get(`/me/order/${orderId}/status`); + return data as OrderStatus; +}; + +export const getOrderFollowUp = async ( + orderId: number, +): Promise => { + const { data } = await v6.get(`/me/order/${orderId}/followUp`); + return data as OrderFollowUpResponse; +}; + +export const getCompleteHistory = async ( + orderId: number, + orderStatus: string, + orderDate: string, +): Promise => { + try { + const followUp = await getOrderFollowUp(orderId); + const history = followUp.flatMap((follow) => follow.history).reverse(); + + if (orderStatus === NOT_PAID && history.length === 0) { + history.push({ + date: orderDate, + label: WAITING_PAYMENT_LABEL, + description: undefined, + }); + } + + return history; + } catch (err) { + if (err.response?.status === 404) { + return []; + } + throw err; + } +}; + +export const fetchOrder = async (): Promise => { + const lastOrderResponse = await getLastOrder(); + + if (!lastOrderResponse || lastOrderResponse.status !== 'OK') return undefined; + + const { data: lastOrder } = lastOrderResponse; + const [orderStatus, details] = await Promise.all([ + getOrderStatus(lastOrder.orderId), + getOrderDetails(lastOrder.orderId), + ]); + + const histories = await getCompleteHistory( + lastOrder.orderId, + lastOrder.date, + orderStatus, + ); + + return { + ...details, + status: orderStatus, + history: histories, + ...lastOrder, + }; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/banner.ts b/packages/manager/apps/hub-react/src/data/api/banner.ts new file mode 100644 index 000000000000..cabf89bd1b51 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/banner.ts @@ -0,0 +1,9 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { Banner } from '@/types/banner.type'; + +export const getBanner: (locale: string) => Promise = async ( + locale: string, +) => { + const { data } = await aapi.get(`/banner?locale=${locale}`); + return data; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/billingServices.ts b/packages/manager/apps/hub-react/src/data/api/billingServices.ts new file mode 100644 index 000000000000..bf132ca55be5 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/billingServices.ts @@ -0,0 +1,29 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { AxiosResponse } from 'axios'; +import { + BillingService, + BillingServicesData, + HubBillingServices, +} from '@/billing/types/billingServices.type'; + +export const getBillingServices: () => Promise< + HubBillingServices +> = async () => { + const { data } = await aapi.get>( + `/hub/billingServices`, + ); + const services = data.data?.billingServices; + // The returned value when status is 'ERROR' has changed in order to keep a standard return type + // for this hook, also this give the same result as previous code without the confusing part + return services.status === 'ERROR' + ? { + count: 0, + services: [], + } + : { + count: services?.data?.count, + services: + services?.data?.data?.map((service) => new BillingService(service)) || + [], + }; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/bills.ts b/packages/manager/apps/hub-react/src/data/api/bills.ts new file mode 100644 index 000000000000..d1065c977066 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/bills.ts @@ -0,0 +1,17 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { AxiosResponse } from 'axios'; +import { Bills, BillsCapsule } from '@/types/bills.type'; + +export const getBills: (period: number) => Promise = async ( + period: number, +) => { + const { data }: AxiosResponse = await aapi.get( + `/hub/bills`, + { + params: { + billingPeriod: period, + }, + }, + ); + return data.data?.bills?.data; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/catalog.ts b/packages/manager/apps/hub-react/src/data/api/catalog.ts new file mode 100644 index 000000000000..43935ee6fc5d --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/catalog.ts @@ -0,0 +1,35 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import i18next from 'i18next'; +import { AxiosResponse } from 'axios'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { CatalogData, CatalogItem } from '@/types/catalog'; + +export const getCatalog: () => Promise< + Record +> = async () => { + const { data }: AxiosResponse> = await aapi.get< + ApiEnvelope + >('/hub/catalog', { + headers: { + 'Content-Language': i18next.language.replace('-', '_'), + }, + }); + // Grouping items by universe while filtering non highlighted elements (as we don't want to display them) + return (data?.data?.catalog.data || []).reduce( + ( + groupedCatalogItems: Record, + item: CatalogItem, + ) => ({ + ...groupedCatalogItems, + ...(item.highlight + ? { + [item.universe]: [ + ...(groupedCatalogItems[item.universe] || []), + item, + ], + } + : {}), + }), + {}, + ); +}; diff --git a/packages/manager/apps/hub-react/src/data/api/debt.ts b/packages/manager/apps/hub-react/src/data/api/debt.ts new file mode 100644 index 000000000000..725aa0b72b92 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/debt.ts @@ -0,0 +1,8 @@ +import { v6 } from '@ovh-ux/manager-core-api'; +import { AxiosResponse } from 'axios/index'; +import { Debt } from '@/types/debt.type'; + +export const getDebt: () => Promise = async () => { + const { data }: AxiosResponse = await v6.get(`/me/debtAccount`); + return data; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/kyc.ts b/packages/manager/apps/hub-react/src/data/api/kyc.ts new file mode 100644 index 000000000000..d74bea5f56d7 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/kyc.ts @@ -0,0 +1,9 @@ +import { v6 } from '@ovh-ux/manager-core-api'; +import { KycProcedures, KycStatus } from '@/types/kyc.type'; + +export const getKycStatus: ( + procedure: KycProcedures, +) => Promise = async (procedure: KycProcedures) => { + const { data } = await v6.get(`/me/procedure/${procedure}`); + return data; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/lastOrder.ts b/packages/manager/apps/hub-react/src/data/api/lastOrder.ts new file mode 100644 index 000000000000..aef74fb368b7 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/lastOrder.ts @@ -0,0 +1,7 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { LastOrder, LastOrderEnvelope } from '@/types/lastOrder.type'; + +export const getLastOrder: () => Promise = async () => { + const { data } = await aapi.get(`/hub/lastOrder`); + return data.data?.lastOrder; +}; diff --git a/packages/manager/apps/hub-react/src/data/api/notifications.ts b/packages/manager/apps/hub-react/src/data/api/notifications.ts new file mode 100644 index 000000000000..6b5d45b1570f --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/notifications.ts @@ -0,0 +1,16 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { Notification, NotificationsList } from '@/types/notifications.type'; + +const hubNotificationStatuses = ['warning', 'error']; + +export const getNotifications: () => Promise = async () => { + const { data } = await aapi.get>( + `/hub/notifications`, + ); + return ( + data.data?.notifications.data || [] + ).filter((notification: Notification) => + hubNotificationStatuses.includes(notification.level), + ); +}; diff --git a/packages/manager/apps/hub-react/src/data/api/services.ts b/packages/manager/apps/hub-react/src/data/api/services.ts new file mode 100644 index 000000000000..06869cc31755 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/api/services.ts @@ -0,0 +1,10 @@ +import { aapi } from '@ovh-ux/manager-core-api'; +import { ServicesEnvelope, ProductList } from '@/types/services.type'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export const getServices: () => Promise< + ApiEnvelope +> = async () => { + const { data } = await aapi.get(`/hub/services`); + return data.data?.services; +}; diff --git a/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.spec.tsx new file mode 100644 index 000000000000..ca354e099103 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.spec.tsx @@ -0,0 +1,69 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { PropsWithChildren } from 'react'; +import { AxiosResponse } from 'axios'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubSupport } from '@/data/hooks/apiHubSupport/useHubSupport'; +import { SupportResponse } from '@/types/support.type'; +import * as hubSupportApi from '@/data/api/apiHubSupport'; + +import queryClient from '@/queryClient'; + +const supportDataResponse: SupportResponse = { + support: { + data: { + count: 3, + data: [], + }, + status: 'OK', + }, +}; + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubSupport', () => { + it('should return expected result', async () => { + const getHubSupport = vi + .spyOn(hubSupportApi, 'getHubSupport') + .mockReturnValue( + Promise.resolve({ + data: { data: supportDataResponse, status: 'OK' }, + } as AxiosResponse), + ); + + const { result } = renderHook(() => useFetchHubSupport(), { + wrapper, + }); + + await waitFor(() => { + expect(getHubSupport).toHaveBeenCalled(); + expect(result.current.data.count).toEqual( + supportDataResponse.support.data.count, + ); + expect(result.current.data.data).toEqual( + supportDataResponse.support.data.data, + ); + }); + }); + it('should call API without cache when refetching', async () => { + const getHubSupport = vi + .spyOn(hubSupportApi, 'getHubSupport') + .mockReturnValue( + Promise.resolve({ + data: { data: supportDataResponse, status: 'OK' }, + } as AxiosResponse), + ); + + const { result } = renderHook(() => useFetchHubSupport(), { + wrapper, + }); + getHubSupport.mockReset(); + await act(() => result.current.refetch()); + + await waitFor(() => { + expect(getHubSupport).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.tsx b/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.tsx new file mode 100644 index 000000000000..956da412b36d --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/apiHubSupport/useHubSupport.tsx @@ -0,0 +1,31 @@ +import { DefinedInitialDataOptions, useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; +import { SupportDataResponse, SupportResponse } from '@/types/support.type'; +import { getHubSupport } from '@/data/api/apiHubSupport'; +import queryClient from '@/queryClient'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +const queryKey = ['get-hub-support']; + +type ApiResponse = AxiosResponse>; + +const DEFAULT_RESULT: SupportDataResponse = { + data: [], + count: 0, +}; + +export const useFetchHubSupport = ( + options?: Partial< + DefinedInitialDataOptions + >, +) => + useQuery({ + queryKey, + queryFn: () => { + const hasBeenFetched = Boolean(queryClient.getQueryData(queryKey)); + return getHubSupport(!hasBeenFetched); + }, + select: ({ data }: ApiResponse) => + data?.data?.support?.data ?? DEFAULT_RESULT, + ...(options ?? {}), + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.spec.tsx new file mode 100644 index 000000000000..04d1d7bfa71f --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.spec.tsx @@ -0,0 +1,47 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import React, { PropsWithChildren } from 'react'; +import { describe, it, vi } from 'vitest'; +import { useFetchLastOrder } from '@/data/hooks/apiOrder/useLastOrder'; +import { LastOrderTrackingResponse } from '@/types/order.type'; +import * as orderApi from '@/data/api/apiOrder/apiOrder'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useLastOrder', () => { + it('useFetchLastOrder should return expected result', async () => { + const orderResponseData: LastOrderTrackingResponse = { + date: 'date', + expirationDate: 'expirationDate', + orderId: 2, + password: 'password', + pdfUrl: 'pdfurl', + priceWithoutTax: undefined, + priceWithTax: undefined, + retractionDate: 'date r', + tax: undefined, + url: 'url', + history: [], + status: 'status', + }; + const getHubSupport = vi + .spyOn(orderApi, 'fetchOrder') + .mockReturnValue(new Promise((resolve) => resolve(orderResponseData))); + + const { result } = renderHook(() => useFetchLastOrder(), { + wrapper, + }); + + await waitFor(() => { + expect(getHubSupport).toHaveBeenCalled(); + expect(result.current.data.expirationDate).toEqual( + orderResponseData.expirationDate, + ); + expect(result.current.data.status).toEqual(orderResponseData.status); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.tsx b/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.tsx new file mode 100644 index 000000000000..18768ecfa3e4 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/apiOrder/useLastOrder.tsx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { LastOrderTrackingResponse } from '@/types/order.type'; +import { fetchOrder } from '@/data/api/apiOrder/apiOrder'; + +export const useFetchLastOrder = () => + useQuery({ + queryKey: ['get-last-order'], + queryFn: fetchOrder, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.spec.tsx new file mode 100644 index 000000000000..95ca09ad4648 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.spec.tsx @@ -0,0 +1,62 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubBanner } from '@/data/hooks/banner/useBanner'; +import * as BannerApi from '@/data/api/banner'; +import { Banner } from '@/types/banner.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubBanner', () => { + it('returns no banner if api returned none', async () => { + const banner: Banner | null = null; + const getBanner = vi + .spyOn(BannerApi, 'getBanner') + .mockReturnValue(new Promise((resolve) => resolve(banner))); + + const { result } = renderHook(() => useFetchHubBanner('fr_FR'), { + wrapper, + }); + + await waitFor(() => { + expect(getBanner).toHaveBeenCalledWith('fr_FR'); + expect(result.current.data).toEqual(banner); + }); + }); + + it('returns a banner if api returned one', async () => { + const banner: Banner = { + alt: 'Summit Banner', + images: { + default: { + src: 'data:image/jpeg;base64,....', + width: 1250, + height: 117, + }, + responsive: { + src: 'data:image/jpeg;base64,....', + width: 453, + height: 117, + }, + }, + link: 'https://link-to-summit.com', + tracker: 'summit::tracking', + }; + vi.spyOn(BannerApi, 'getBanner').mockReturnValue( + new Promise((resolve) => resolve(banner)), + ); + + const { result } = renderHook(() => useFetchHubBanner('fr_FR'), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(banner); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.tsx b/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.tsx new file mode 100644 index 000000000000..e8cfe7ea6349 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/banner/useBanner.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getBanner } from '@/data/api/banner'; +import { Banner } from '@/types/banner.type'; + +export const useFetchHubBanner = (locale: string) => + useQuery({ + queryKey: ['getHubBanner'], + queryFn: () => getBanner(locale), + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.spec.tsx new file mode 100644 index 000000000000..81820f2d639b --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.spec.tsx @@ -0,0 +1,34 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubBillingServices } from '@/data/hooks/billingServices/useBillingServices'; +import * as BillingServicesApi from '@/data/api/billingServices'; +import { HubBillingServices } from '@/billing/types/billingServices.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubBillingServices', () => { + it('returns capsule even if api returned no services', async () => { + const services: HubBillingServices = { + services: [], + count: 0, + }; + const getServices = vi + .spyOn(BillingServicesApi, 'getBillingServices') + .mockReturnValue(new Promise((resolve) => resolve(services))); + + const { result } = renderHook(() => useFetchHubBillingServices(), { + wrapper, + }); + + await waitFor(() => { + expect(getServices).toHaveBeenCalled(); + expect(result.current.data).toEqual(services); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.tsx b/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.tsx new file mode 100644 index 000000000000..5d054e51bdaa --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/billingServices/useBillingServices.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getBillingServices } from '@/data/api/billingServices'; +import { HubBillingServices } from '@/billing/types/billingServices.type'; + +export const useFetchHubBillingServices = () => + useQuery({ + queryKey: ['getHubBillingServices'], + queryFn: getBillingServices, + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.spec.tsx new file mode 100644 index 000000000000..3ac24ba16781 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.spec.tsx @@ -0,0 +1,52 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubBills } from '@/data/hooks/bills/useBills'; +import * as BillsApi from '@/data/api/bills'; +import { Bills } from '@/types/bills.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubBills', () => { + it('returns no bills summary if api returned none', async () => { + const bills: Bills | null = null; + const getBills = vi + .spyOn(BillsApi, 'getBills') + .mockReturnValue(Promise.resolve(bills)); + + const { result } = renderHook(() => useFetchHubBills(1), { + wrapper, + }); + + await waitFor(() => { + expect(getBills).toHaveBeenCalledWith(1); + expect(result.current.data).toEqual(bills); + }); + }); + + it('returns a bills summary if api returned one', async () => { + const bills: Bills = { + currency: { + code: 'EUR', + format: '{{price}} €', + symbol: '€', + }, + period: { from: '2024-08-01', to: '2024-08-31' }, + total: 0, + }; + vi.spyOn(BillsApi, 'getBills').mockReturnValue(Promise.resolve(bills)); + + const { result } = renderHook(() => useFetchHubBills(1), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(bills); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.tsx b/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.tsx new file mode 100644 index 000000000000..28d1cceb94f3 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/bills/useBills.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getBills } from '@/data/api/bills'; +import { Bills } from '@/types/bills.type'; + +export const useFetchHubBills = (period: number) => + useQuery({ + queryKey: ['getHubBills', period], + queryFn: () => getBills(period), + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.spec.tsx new file mode 100644 index 000000000000..fa143a5887bf --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.spec.tsx @@ -0,0 +1,46 @@ +import { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { aapi as Api } from '@ovh-ux/manager-core-api'; +import { useFetchHubCatalog } from '@/data/hooks/catalog/useCatalog'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { CatalogData, CatalogItem } from '@/types/catalog'; +import { catalogData, rawCatalogData } from '@/_mock_/catalog'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); +vi.mock('i18next', () => ({ + default: { + language: 'fr_FR', + }, +})); + +describe('useFetchHubCatalog', () => { + it('should return a list of highlighted products grouped by universe', async () => { + const catalog: ApiEnvelope = { + data: { + catalog: { + data: rawCatalogData as CatalogItem[], + status: 'OK', + }, + }, + status: 'OK', + }; + const getCatalog = vi + .spyOn(Api, 'get') + .mockReturnValue(Promise.resolve({ data: catalog })); + + const { result } = renderHook(() => useFetchHubCatalog(), { + wrapper, + }); + + await waitFor(() => { + expect(getCatalog).toHaveBeenCalled(); + expect(result.current.data).toEqual(catalogData); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.tsx b/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.tsx new file mode 100644 index 000000000000..a6c7ced18229 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/catalog/useCatalog.tsx @@ -0,0 +1,11 @@ +import { AxiosError } from 'axios'; +import { useQuery } from '@tanstack/react-query'; +import { getCatalog } from '@/data/api/catalog'; +import { CatalogItem } from '@/types/catalog'; + +export const useFetchHubCatalog = () => + useQuery, AxiosError>({ + queryKey: ['getHubCatalog'], + queryFn: getCatalog, + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.spec.tsx new file mode 100644 index 000000000000..c84de87cf6ec --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.spec.tsx @@ -0,0 +1,66 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubDebt } from '@/data/hooks/debt/useDebt'; +import * as DebtApi from '@/data/api/debt'; +import { Debt } from '@/types/debt.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubDebt', () => { + it('returns no debt if api returned none', async () => { + const debt: Debt | null = null; + const getDebt = vi + .spyOn(DebtApi, 'getDebt') + .mockReturnValue(Promise.resolve(debt)); + + const { result } = renderHook(() => useFetchHubDebt(), { + wrapper, + }); + + await waitFor(() => { + expect(getDebt).toHaveBeenCalledWith(); + expect(result.current.data).toEqual(debt); + }); + }); + + it('returns a banner if api returned one', async () => { + const debt: Debt = { + pendingAmount: { + value: 0, + text: '0.00 €', + currencyCode: 'EUR', + }, + dueAmount: { + value: 0, + text: '0.00 €', + currencyCode: 'EUR', + }, + unmaturedAmount: { + value: 0, + text: '0.00 €', + currencyCode: 'EUR', + }, + todoAmount: { + value: 0, + text: '0.00 €', + currencyCode: 'EUR', + }, + active: false, + }; + vi.spyOn(DebtApi, 'getDebt').mockReturnValue(Promise.resolve(debt)); + + const { result } = renderHook(() => useFetchHubDebt(), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(debt); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.tsx b/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.tsx new file mode 100644 index 000000000000..744e29ac9742 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/debt/useDebt.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getDebt } from '@/data/api/debt'; +import { Debt } from '@/types/debt.type'; + +export const useFetchHubDebt = () => + useQuery({ + queryKey: ['getHubDebt'], + queryFn: () => getDebt(), + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.spec.tsx new file mode 100644 index 000000000000..896f126cf97e --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.spec.tsx @@ -0,0 +1,39 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { v6 as Api } from '@ovh-ux/manager-core-api'; +import { useKyc } from '@/data/hooks/kyc/useKyc'; +import { KycProcedures } from '@/types/kyc.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useKyc', () => { + it('should return an helper to make kyc request', async () => { + const { result } = renderHook(() => useKyc(KycProcedures.INDIA), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.useKycStatus).not.toBeNull(); + }); + }); + + it('should make a call to the correct API given a kyc procedure name', async () => { + const apiGet = vi + .spyOn(Api, 'get') + .mockReturnValue(Promise.resolve({ status: 'required' })); + + renderHook(() => useKyc(KycProcedures.INDIA).useKycStatus(), { + wrapper, + }); + + await waitFor(() => { + expect(apiGet).toHaveBeenCalledWith('/me/procedure/identity'); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.tsx b/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.tsx new file mode 100644 index 000000000000..ee28dd1592ac --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/kyc/useKyc.tsx @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { KycProcedures, KycStatus } from '@/types/kyc.type'; +import { getKycStatus } from '@/data/api/kyc'; + +export const useKyc = (procedure: KycProcedures) => { + const useKycStatus = () => + useQuery({ + queryKey: ['getKycStatus', procedure], + queryFn: () => getKycStatus(procedure), + retry: 0, + }); + + return { + useKycStatus, + }; +}; diff --git a/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.spec.tsx new file mode 100644 index 000000000000..2b2d9b60d9d0 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.spec.tsx @@ -0,0 +1,77 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubLastOrder } from '@/data/hooks/lastOrder/useLastOrder'; +import * as LastOrderApi from '@/data/api/lastOrder'; +import { LastOrder } from '@/types/lastOrder.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubLastOrder', () => { + it('returns no order if api returned none', async () => { + const lastOrder: LastOrder = { + status: 'OK', + data: null, + }; + const getLastOrder = vi + .spyOn(LastOrderApi, 'getLastOrder') + .mockReturnValue(new Promise((resolve) => resolve(lastOrder))); + + const { result } = renderHook(() => useFetchHubLastOrder(), { + wrapper, + }); + + await waitFor(() => { + expect(getLastOrder).toHaveBeenCalled(); + expect(result.current.data).toEqual(lastOrder); + }); + }); + + it('returns the last order if api returned one', async () => { + const lastOrder: LastOrder = { + status: 'OK', + data: { + date: '2024-08-22T12:24:08+02:00', + expirationDate: '2024-09-05T23:29:59+02:00', + orderId: 99999999999, + password: 'fakepassword', + pdfUrl: + 'https://www.fake-order-url.com?orderId=fakeId&orderPassword=fakePassword', + priceWithTax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + priceWithoutTax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + retractionDate: '2024-09-06T00:00:00+02:00', + tax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + url: + 'https://www.fake-order-url.com?orderId=fakeId&orderPassword=fakePassword', + }, + }; + vi.spyOn(LastOrderApi, 'getLastOrder').mockReturnValue( + new Promise((resolve) => resolve(lastOrder)), + ); + + const { result } = renderHook(() => useFetchHubLastOrder(), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.data).toEqual(lastOrder); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.tsx b/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.tsx new file mode 100644 index 000000000000..18618094b8cb --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/lastOrder/useLastOrder.tsx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getLastOrder } from '@/data/api/lastOrder'; +import { LastOrder } from '@/types/lastOrder.type'; + +export const useFetchHubLastOrder = () => + useQuery({ + queryKey: ['getHubLastOrder'], + queryFn: getLastOrder, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.spec.tsx new file mode 100644 index 000000000000..82a41d227aaa --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.spec.tsx @@ -0,0 +1,42 @@ +import { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { aapi as Api } from '@ovh-ux/manager-core-api'; +import { useFetchHubNotifications } from '@/data/hooks/notifications/useNotifications'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { NotificationsList } from '@/types/notifications.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubNotifications', () => { + it('should return notifications after extracting them from api envelope', async () => { + const notifications: ApiEnvelope = { + data: { + notifications: { + data: [], + status: 'OK', + }, + }, + status: 'OK', + }; + const getNotifications = vi + .spyOn(Api, 'get') + .mockReturnValue(Promise.resolve(notifications)); + + const { result } = renderHook(() => useFetchHubNotifications(), { + wrapper, + }); + + await waitFor(() => { + expect(getNotifications).toHaveBeenCalled(); + expect(result.current.data).toEqual( + notifications.data.notifications.data, + ); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.tsx b/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.tsx new file mode 100644 index 000000000000..eae1e1698dc8 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/notifications/useNotifications.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { Notification } from '@/types/notifications.type'; +import { getNotifications } from '@/data/api/notifications'; + +export const useFetchHubNotifications = () => + useQuery({ + queryKey: ['getHubNotifications'], + queryFn: getNotifications, + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/data/hooks/services/useServices.spec.tsx b/packages/manager/apps/hub-react/src/data/hooks/services/useServices.spec.tsx new file mode 100644 index 000000000000..57129beed8d8 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/services/useServices.spec.tsx @@ -0,0 +1,38 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { describe, it, vi } from 'vitest'; +import { useFetchHubServices } from '@/data/hooks/services/useServices'; +import * as ServicesApi from '@/data/api/services'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { ProductList } from '@/types/services.type'; + +const queryClient = new QueryClient(); + +const wrapper = ({ children }: PropsWithChildren) => ( + {children} +); + +describe('useFetchHubServices', () => { + it('returns capsule even if api returned no services', async () => { + const services: ApiEnvelope = { + data: { + data: {}, + count: 0, + }, + status: 'OK', + }; + const getServices = vi + .spyOn(ServicesApi, 'getServices') + .mockReturnValue(new Promise((resolve) => resolve(services))); + + const { result } = renderHook(() => useFetchHubServices(), { + wrapper, + }); + + await waitFor(() => { + expect(getServices).toHaveBeenCalled(); + expect(result.current.data).toEqual(services); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/data/hooks/services/useServices.tsx b/packages/manager/apps/hub-react/src/data/hooks/services/useServices.tsx new file mode 100644 index 000000000000..ff421d70b650 --- /dev/null +++ b/packages/manager/apps/hub-react/src/data/hooks/services/useServices.tsx @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { getServices } from '@/data/api/services'; +import { ProductList } from '@/types/services.type'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export const useFetchHubServices = () => + useQuery, AxiosError>({ + queryKey: ['getHubServices'], + queryFn: getServices, + retry: 0, + }); diff --git a/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.spec.tsx b/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.spec.tsx new file mode 100644 index 000000000000..ee85ad042cca --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.spec.tsx @@ -0,0 +1,52 @@ +import { renderHook } from '@testing-library/react'; +import React from 'react'; +import { vi } from 'vitest'; +import useDateFormat from './useDateFormat'; + +const mocks = vi.hoisted(() => ({ + environment: { + getUserLocale: vi.fn().mockReturnValue('en_US'), + }, + shell: { + navigation: { + getURL: vi.fn().mockResolvedValue('mocked-url'), + }, + }, +})); + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + ShellContext: React.createContext({ + shell: mocks.shell, + environment: mocks.environment, + }), +})); + +describe('useDateFormat', () => { + it('should return Intl.DateTimeFormat with the correct locale and options', () => { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + }; + + vi.spyOn(React, 'useContext').mockReturnValue(mocks); + + const { result } = renderHook(() => useDateFormat({ options })); + + const formatter = result.current; + expect(formatter.resolvedOptions().locale).toBe('en-US'); + expect(formatter.resolvedOptions().year).toBe('numeric'); + expect(formatter.resolvedOptions().month).toBe('long'); + expect(formatter.resolvedOptions().day).toBe('numeric'); + expect(formatter.format(new Date('2024-01-01'))).toBe('January 1, 2024'); + }); + + it('should handle locale conversion correctly', () => { + mocks.environment.getUserLocale.mockReturnValue('fr_FR'); + + const { result } = renderHook(() => useDateFormat({ options: {} })); + + const formatter = result.current; + expect(formatter.resolvedOptions().locale).toBe('fr-FR'); + }); +}); diff --git a/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.tsx b/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.tsx new file mode 100644 index 000000000000..c27bfbc558db --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/dateFormat/useDateFormat.tsx @@ -0,0 +1,16 @@ +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import React, { useContext } from 'react'; + +type Props = { + options?: Intl.DateTimeFormatOptions; +}; + +const useDateFormat = ({ options }: Props): Intl.DateTimeFormat => { + const context = useContext(ShellContext); + const { environment } = context; + const userLanguage = environment.getUserLocale().replace('_', '-'); + + return new Intl.DateTimeFormat(userLanguage, options); +}; + +export default useDateFormat; diff --git a/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.constants.ts b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.constants.ts new file mode 100644 index 000000000000..b991883fab40 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.constants.ts @@ -0,0 +1,30 @@ +import { CountryCode } from '@ovh-ux/manager-config'; + +const guidesRoot = 'https://docs.ovh.com'; + +type GuideLinks = { [key in CountryCode | string]: string }; + +export const GUIDE_LIST: { [guideName: string]: Partial } = { + Home: { + DE: `${guidesRoot}/de/`, + ES: `${guidesRoot}/es/`, + FR: `${guidesRoot}/fr/`, + GB: `${guidesRoot}/gb/en/`, + IE: `${guidesRoot}/ie/en/`, + IT: `${guidesRoot}/it/`, + MA: `${guidesRoot}/fr/`, + NL: `${guidesRoot}/nl/`, + PL: `${guidesRoot}/pl/`, + PT: `${guidesRoot}/pt/`, + SN: `${guidesRoot}/fr/`, + TN: `${guidesRoot}/fr/`, + ASIA: `${guidesRoot}/asia/en/`, + AU: `${guidesRoot}/au/en/`, + CA: `${guidesRoot}/ca/en/`, + QC: `${guidesRoot}/ca/fr/`, + SG: `${guidesRoot}/sg/en/`, + WE: `${guidesRoot}/us/en/`, + WS: `${guidesRoot}/us/es/`, + US: 'https://support.us.ovhcloud.com', + }, +}; diff --git a/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.spec.tsx b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.spec.tsx new file mode 100644 index 000000000000..848fbfe02fb2 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.spec.tsx @@ -0,0 +1,50 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; +import { describe, it, vi } from 'vitest'; +import useGuideUtils from './useGuideUtils'; +import { GUIDE_LIST } from './useGuideUtils.constants'; + +const environmentMock = { + getUser: () => ({ + ovhSubsidiary: 'FR', + }), +}; + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + ShellContext: React.createContext({ + environment: { + getEnvironment: vi.fn(() => environmentMock), + }, + shell: { + environment: { + getEnvironment: vi.fn(() => environmentMock), + }, + }, + }), +})); + +describe('useGuideUtils Hook', () => { + it('returns guide links based on FR subsidiary', async () => { + environmentMock.getUser = () => ({ + ovhSubsidiary: 'FR', + }); + + const { result } = renderHook(() => useGuideUtils()); + + await waitFor(() => { + expect(result.current.Home).toBe(GUIDE_LIST.Home.FR); + }); + }); + + it('returns guide links for US subsidiary', async () => { + environmentMock.getUser = () => ({ + ovhSubsidiary: 'US', + }); + + const { result } = renderHook(() => useGuideUtils()); + + await waitFor(() => { + expect(result.current.Home).toBe(GUIDE_LIST.Home.US); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.tsx b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.tsx new file mode 100644 index 000000000000..ea8132a4c147 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/guideUtils/useGuideUtils.tsx @@ -0,0 +1,41 @@ +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { useContext, useEffect, useState } from 'react'; +import { CountryCode } from '@ovh-ux/manager-config'; +import { GUIDE_LIST } from './useGuideUtils.constants'; + +type GetGuideLinkProps = { + name?: string; + subsidiary: CountryCode | string; +}; + +function getGuideListLink({ subsidiary }: GetGuideLinkProps) { + const list: { [guideName: string]: string } = {}; + const keys = Object.entries(GUIDE_LIST); + keys.forEach((key) => { + list[key[0]] = GUIDE_LIST[key[0]][subsidiary as CountryCode]; + }); + return list; +} + +interface GuideLinkProps { + [guideName: string]: string; +} + +function useGuideUtils() { + const { shell } = useContext(ShellContext); + const { environment } = shell; + const [list, setList] = useState({}); + + useEffect(() => { + const getSubSidiary = async () => { + const env = await environment.getEnvironment(); + const { ovhSubsidiary } = env.getUser(); + const guideList = getGuideListLink({ subsidiary: ovhSubsidiary }); + setList(guideList); + }; + getSubSidiary(); + }, []); + return list as GuideLinkProps; +} + +export default useGuideUtils; diff --git a/packages/manager/apps/hub-react/src/hooks/periodFilter/usePeriodFilter.tsx b/packages/manager/apps/hub-react/src/hooks/periodFilter/usePeriodFilter.tsx new file mode 100644 index 000000000000..3ad5746b16d9 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/periodFilter/usePeriodFilter.tsx @@ -0,0 +1,14 @@ +import { Period } from '@/types/bills.type'; + +export const usePeriodFilter = ({ from, to }: Period) => [ + { + field: 'date', + comparator: 'isAfter', + reference: [from], + }, + { + field: 'date', + comparator: 'isBefore', + reference: [to], + }, +]; diff --git a/packages/manager/apps/hub-react/src/hooks/priceFormat/usePriceFormat.tsx b/packages/manager/apps/hub-react/src/hooks/priceFormat/usePriceFormat.tsx new file mode 100644 index 000000000000..331c9bd7fbba --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/priceFormat/usePriceFormat.tsx @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; + +export const usePriceFormat = (price: number, currency: string) => { + const { environment } = useContext(ShellContext); + const locale = environment.getUserLocale(); + + return currency && price + ? Intl.NumberFormat(locale.replace('_', '-'), { + style: 'currency', + currency, + }).format(price) + : ''; +}; diff --git a/packages/manager/apps/hub-react/src/hooks/products/useProducts.spec.tsx b/packages/manager/apps/hub-react/src/hooks/products/useProducts.spec.tsx new file mode 100644 index 000000000000..7e459c17d547 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/products/useProducts.spec.tsx @@ -0,0 +1,117 @@ +import React, { PropsWithChildren } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { + ShellContext, + ShellContextType, + useRouteSynchro, +} from '@ovh-ux/manager-react-shell-client'; +import { useProducts } from '@/hooks/products/useProducts'; +import { + lotsOfProductsMocked, + lotsOfProductsParsedExpandedMocked, + lotsOfProductsParsedMocked, +} from '@/_mock_/products'; + +const shellContext = { + environment: { + getRegion: vi.fn(() => 'EU'), + }, + shell: { + navigation: { + getURL: vi.fn(() => Promise.resolve('https://fake-link.com')), + }, + }, +}; + +const wrapper = ({ children }: PropsWithChildren) => ( + + {children} + +); + +describe('useProducts', () => { + it('returns non expanded products list', async () => { + const { result } = renderHook( + () => useProducts(lotsOfProductsMocked, false), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.products).toEqual(lotsOfProductsParsedMocked); + }); + }); + + it('returns expanded products list', async () => { + const { result } = renderHook( + () => useProducts(lotsOfProductsMocked, true), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.products).toEqual( + lotsOfProductsParsedExpandedMocked, + ); + }); + }); + + it('returns a product with no link if it is unknown', async () => { + const { result } = renderHook( + () => + useProducts( + { + count: 1, + data: { + fakeProduct: { + count: 1, + data: [ + { + propertyId: 'name', + resource: { + displayName: 'fake_service', + name: 'fake_service', + resellingProvider: null, + state: 'toSuspend', + }, + route: { + path: '/dedicated/housing/{serviceName}', + }, + serviceId: 1, + universe: { + CA: 'dedicated', + EU: 'dedicated', + US: 'dedicated', + }, + url: + 'https://www.ovh.com/manager/#/dedicated/configuration/fake_product/fake_service', + }, + ], + }, + }, + }, + true, + ), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.products[0].link).toBeNull(); + }); + }); + + it('returns an empty array if services parameters is falsy', async () => { + const { result } = renderHook(() => useProducts(null, true), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.products.length).toBe(0); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/hooks/products/useProducts.ts b/packages/manager/apps/hub-react/src/hooks/products/useProducts.ts new file mode 100644 index 000000000000..ab102530f631 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hooks/products/useProducts.ts @@ -0,0 +1,47 @@ +import { useContext } from 'react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { HubProduct, ProductList } from '@/types/services.type'; +import { + DEFAULT_DISPLAYED_PRODUCTS, + DEFAULT_DISPLAYED_SERVICES, + productListingPages, +} from '@/components/products/Products.constants'; + +export const useProducts = (services: ProductList, expand = false) => { + const { shell } = useContext(ShellContext); + let isLoading = true; + const servicesByProducts = services?.data || {}; + const productNames = Object.keys(servicesByProducts); + const products: HubProduct[] = productNames + .map((name: string & keyof typeof servicesByProducts) => { + const { application, hash } = productListingPages[name] || { + application: null, + hash: null, + }; + return { + data: servicesByProducts[name].data.slice( + 0, + DEFAULT_DISPLAYED_SERVICES, + ), + count: servicesByProducts[name].count, + type: name, + formattedType: name.toLowerCase().replace(/_/g, '-'), + // Link to product page should be done on the BFF side + link: + application && hash + ? (shell.navigation.getURL(application, hash, {}) as Promise< + string + >) + : null, + }; + }) + .sort((productA, productB) => productB.count - productA.count) + .slice(0, expand ? productNames.length : DEFAULT_DISPLAYED_PRODUCTS); + isLoading = false; + + return { + isLoading, + products, + canDisplayMore: productNames.length > DEFAULT_DISPLAYED_PRODUCTS, + }; +}; diff --git a/packages/manager/apps/hub-react/src/hub.config.ts b/packages/manager/apps/hub-react/src/hub.config.ts new file mode 100644 index 000000000000..cf39e2064c40 --- /dev/null +++ b/packages/manager/apps/hub-react/src/hub.config.ts @@ -0,0 +1,3 @@ +export default { + rootLabel: 'hub', +}; diff --git a/packages/manager/apps/hub-react/src/hub.constants.ts b/packages/manager/apps/hub-react/src/hub.constants.ts new file mode 100644 index 000000000000..7fa561b8196b --- /dev/null +++ b/packages/manager/apps/hub-react/src/hub.constants.ts @@ -0,0 +1,12 @@ +const BILLING_URL = /^\/billing\/.*/; + +const USER_ACCOUNT_URL = /^\/useraccount\/.*/; + +export const CATALOG_URL_REGEX = /^\/catalog(\/.)*/; + +export const BILLING_REDIRECTIONS: RegExp[] = [BILLING_URL, USER_ACCOUNT_URL]; + +export default { + BILLING_REDIRECTIONS, + CATALOG_URL_REGEX, +}; diff --git a/packages/manager/apps/hub-react/src/index.scss b/packages/manager/apps/hub-react/src/index.scss new file mode 100644 index 000000000000..4a0044ae7e05 --- /dev/null +++ b/packages/manager/apps/hub-react/src/index.scss @@ -0,0 +1,102 @@ +@tailwind components; +@tailwind utilities; + +h1, +h2 { + margin-top: 1rem; +} + +$ovh-sidebar-width: 18.75rem; +$device-breakpoint-medium-max-width: 1279px; + +$hub-brand-color: #001191; + +$hub-subtitle-color: #001758; + +$hub-text-color: #4d5592; +$hub-text-weight: 400; + +$hub-button-bg-color: #3046d1; +$hub-button-fg-color: white; + +$hub-tile-border-radius: 0.5rem; +$hub-tile-padding: 1rem 0 1rem 0; + +$hub-border-radius-default: 6pt; + +@mixin hub-pill { + background-color: #bef1ff; + border-radius: 1rem; + font-size: 0.9rem; + color: #0050d7; + margin-left: 0.5rem; + padding: 0.3rem 0.5rem; +} + +ovh-manager-banner-text .notification-banner { + padding-right: $ovh-sidebar-width !important; +} + +@media screen and (min-width: $device-breakpoint-medium-max-width) { + .hub-main-view_sidebar_expanded { + margin-right: $ovh-sidebar-width; + max-width: calc(100% - $ovh-sidebar-width); + } +} + +@media screen and (max-width: $device-breakpoint-medium-max-width) { + .hub-main-view { + margin-right: 0; + } + ovh-manager-banner-text .notification-banner { + padding-right: 0 !important; + } +} + +#root { + height: inherit; + + .hub-main-view { + .minw-0 { + min-width: 0; + } + + &_container { + max-width: 80rem; // 1280px with 16px font size + margin: auto; + } + } + + .skipnav osds-button { + width: 1px; + height: 1px; + position: fixed; + left: -100%; + top: auto; + overflow: hidden; + z-index: -1; + + &:focus, + &:active { + width: auto; + height: auto; + left: 0.625rem; + top: 0.625rem; + overflow: auto; + outline-width: 1px !important; + outline-style: dotted !important; + outline-color: initial !important; + outline-offset: -2px !important; + background-color: #fff; + z-index: 1100; + } + } + + .skiptarget { + width: 1px; + height: 1px; + overflow: hidden; + float: right; + position: absolute; + } +} diff --git a/packages/manager/apps/hub-react/src/index.tsx b/packages/manager/apps/hub-react/src/index.tsx new file mode 100644 index 000000000000..9d90c7293aed --- /dev/null +++ b/packages/manager/apps/hub-react/src/index.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + ShellContext, + initShellContext, + initI18n, +} from '@ovh-ux/manager-react-shell-client'; +import { isTopLevelApplication } from '@ovh-ux/manager-config'; +import App from './App'; +import '@ovhcloud/ods-theme-blue-jeans/dist/index.css'; +import './index.scss'; +import './vite-hmr'; + +import { UNIVERSE, SUB_UNIVERSE, APP_NAME, LEVEL2 } from './tracking.constant'; + +const trackingContext = { + chapter1: UNIVERSE, + chapter2: SUB_UNIVERSE, + chapter3: APP_NAME, + appName: APP_NAME, + pageTheme: UNIVERSE, + level2Config: LEVEL2, +}; + +const init = async (appName: string) => { + const context = await initShellContext(appName, trackingContext); + + const isSidebarMenuVisible = await context.shell.ux.isMenuSidebarVisible(); + if (!isTopLevelApplication()) { + context.shell.ux.startProgress(); + } + + context.shell.ux.setForceAccountSiderBarDisplayOnLargeScreen(true); + if (!isSidebarMenuVisible) { + context.shell.ux.showAccountSidebar(); + } + + await initI18n({ + context, + reloadOnLocaleChange: true, + defaultNS: appName, + ns: [ + `${appName}/support`, + `${appName}/products`, + `${appName}/order`, + `${appName}/billing`, + `${appName}/error`, + `${appName}/payment-status`, + `${appName}/siret`, + `${appName}/kyc`, + `billing/actions`, + `billing/status`, + ], + }); + + const region = context.environment.getRegion(); + context.shell.tracking.setConfig(region, LEVEL2); + try { + await import(`./config-${region}.js`); + } catch (error) { + // nothing to do + } + + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , + ); +}; + +init('hub'); diff --git a/packages/manager/apps/hub-react/src/pages/404.tsx b/packages/manager/apps/hub-react/src/pages/404.tsx new file mode 100644 index 000000000000..46ab24c17b1d --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/404.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import { Navigate } from 'react-router-dom'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { BILLING_REDIRECTIONS, CATALOG_URL_REGEX } from '@/hub.constants'; + +export default function NotFound() { + const { shell } = useContext(ShellContext); + + const hash = window.location.hash.replace('#', ''); + for (let index = 0; index < BILLING_REDIRECTIONS.length; index += 1) { + const redirectionRegex = BILLING_REDIRECTIONS[index]; + if (redirectionRegex.test(hash)) { + shell.navigation + .getURL('dedicated', window.location.hash, {}) + .then((url: string) => { + window.top.location.href = url; + }); + } + } + if (CATALOG_URL_REGEX.test(hash)) { + shell.navigation.getURL('catalog', '/', {}).then((url: string) => { + window.top.location.href = url; + }); + return null; + } + + return ; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout.test.tsx b/packages/manager/apps/hub-react/src/pages/layout.test.tsx new file mode 100644 index 000000000000..385521f1738b --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout.test.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import * as reactShellClientModule from '@ovh-ux/manager-react-shell-client'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import Layout from '@/pages/layout'; + +const shellContext = { + environment: { + getUser: vi.fn(), + }, + shell: { + ux: { + hidePreloader: vi.fn(), + }, + }, +}; + +const renderComponent = () => + render( + + + , + ); + +const mockPath = '/foo'; + +vi.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: mockPath, + }), +})); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const original: typeof reactShellClientModule = await importOriginal(); + return { + ...original, + useOvhTracking: vi.fn(() => ({ + trackPage: vi.fn(), + trackClick: vi.fn(), + trackCurrentPage: vi.fn(), + usePageTracking: vi.fn(), + })), + useRouteSynchro: vi.fn(() => {}), + }; +}); + +describe('Form.page', () => { + it('should render select LegalForms correctly when the sub is FR and legalForms is other', async () => { + const { getByText } = renderComponent(); + + expect(getByText('Layout')).not.toBeNull(); + }); +}); diff --git a/packages/manager/apps/hub-react/src/pages/layout.tsx b/packages/manager/apps/hub-react/src/pages/layout.tsx new file mode 100644 index 000000000000..2255576cefc8 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout.tsx @@ -0,0 +1,29 @@ +import React, { useEffect, useContext } from 'react'; +import { defineCurrentPage } from '@ovh-ux/request-tagger'; +import { useLocation } from 'react-router-dom'; +import { + useOvhTracking, + useRouteSynchro, + ShellContext, +} from '@ovh-ux/manager-react-shell-client'; + +export default function Layout() { + const location = useLocation(); + const { shell } = useContext(ShellContext); + const { trackCurrentPage } = useOvhTracking(); + useRouteSynchro(); + + useEffect(() => { + defineCurrentPage(`app.dashboard`); + }, []); + + useEffect(() => { + trackCurrentPage(); + }, [location]); + + useEffect(() => { + shell.ux.hidePreloader(); + }, []); + + return
Layout
; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.component.tsx new file mode 100644 index 000000000000..67eb4f36be2f --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.component.tsx @@ -0,0 +1,223 @@ +import React, { lazy, Suspense, useContext, useMemo, useState } from 'react'; +import { + OsdsIcon, + OsdsLink, + OsdsSelect, + OsdsSelectOption, + OsdsSkeleton, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ShellContext, + useOvhTracking, +} from '@ovh-ux/manager-react-shell-client'; +import { useTranslation } from 'react-i18next'; +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_SKELETON_SIZE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { Await } from 'react-router-dom'; +import { useFetchHubBills } from '@/data/hooks/bills/useBills'; +import { useFetchHubDebt } from '@/data/hooks/debt/useDebt'; +import '@/pages/layout/BillingSummary.style.scss'; +import { BILLING_SUMMARY_PERIODS_IN_MONTHS } from '@/pages/layout/layout.constants'; +import { usePeriodFilter } from '@/hooks/periodFilter/usePeriodFilter'; +import { usePriceFormat } from '@/hooks/priceFormat/usePriceFormat'; + +const TileError = lazy(() => + import('@/components/tile-error/TileError.component'), +); + +export default function BillingSummary() { + const { t } = useTranslation('hub/billing'); + const { + shell: { navigation }, + } = useContext(ShellContext); + const { trackClick } = useOvhTracking(); + const [months, setMonths] = useState(1); + + const { + isPending: areBillsLoading, + data: bills, + error: billsError, + refetch, + } = useFetchHubBills(months); + const { isPending: isDebtLoading, data: debt } = useFetchHubDebt(); + + const isLoading = areBillsLoading || isDebtLoading; + + const formattedPrice = usePriceFormat(bills?.total, bills?.currency?.code); + const formattedDebtPrice = usePriceFormat( + debt?.dueAmount?.value, + debt?.dueAmount?.currencyCode, + ); + + const payDebtLink = useMemo( + () => navigation.getURL('dedicated', '#/billing/history/debt/all/pay', {}), + [], + ); + + const viewBillsLink = useMemo( + () => + navigation.getURL('dedicated', '#/billing/history', { + // From BFF's code, it seems that period cannot be null or undefined, so this code could probably be simplified + filter: bills?.period + ? JSON.stringify(usePeriodFilter(bills.period)) + : '', + }), + [bills?.period], + ); + + const trackGoToBilling = () => { + trackClick({ + actionType: 'action', + actions: ['activity', 'billing', 'show-all'], + }); + }; + + return ( +
+ + {t('hub_billing_summary_title')} + + {!isLoading && billsError && ( + } + > + + + )} + {!billsError && ( +
+ + setMonths(Number(event.detail.value as string)) + } + > + {BILLING_SUMMARY_PERIODS_IN_MONTHS.map((month) => ( + + {t(`hub_billing_summary_period_${month}`)} + + ))} + + + {!isLoading && formattedPrice} + +

+ {isLoading && ( + + )} + {!isLoading && bills?.total > 0 && debt?.dueAmount?.value === 0 && ( + <> + + {t('hub_billing_summary_debt_null')} + + )} + {!isLoading && debt?.dueAmount?.value > 0 && ( + <> + + {t('hub_billing_summary_debt', { + debt: formattedDebtPrice, + })} + + } + > + ( + + {t('hub_billing_summary_debt_pay')} + + )} + /> + + + )} + {!isLoading && + bills?.total === 0 && + t('hub_billing_summary_debt_no_bills')} +

+ {isLoading && ( + + )} + {!isLoading && ( + } + > + ( + + {t('hub_billing_summary_display_bills')} + + + + + )} + /> + + )} +
+ )} +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.style.scss b/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.style.scss new file mode 100644 index 000000000000..eea5400e1580 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/BillingSummary.style.scss @@ -0,0 +1,46 @@ +@import '@/index.scss'; + +@mixin manager-hub-billing-summary { + $bg-color: #4bb2f6; + $bg-image: url('./assets/billing-background.svg'); + + background-color: $bg-color; + background-image: $bg-image; + background-repeat: no-repeat; + background-size: cover; + color: #fff; + font-family: 'Source Sans Pro', sans-serif; + font-weight: 600; + text-align: center; + border-radius: $hub-tile-border-radius; + padding: $hub-tile-padding; + width: 100%; + + > osds-text, + > div > osds-text { + color: #fff; + } + + p, + span { + > a { + margin-bottom: -1.5rem; + } + } + + :host(osds-select).select-trigger { + background-color: transparent; + color: #fff; + border-color: #fff; + border-width: 2px; + } + + &__bill-total { + font-weight: 600; + font-size: calc(2rem + 1vw); + } +} + +.manager-hub-billing-summary { + @include manager-hub-billing-summary; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/Catalog.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/Catalog.component.tsx new file mode 100644 index 000000000000..1e72edd6e152 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/Catalog.component.tsx @@ -0,0 +1,102 @@ +import { useTranslation } from 'react-i18next'; +import { Card } from '@ovh-ux/manager-react-components'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { OsdsText } from '@ovhcloud/ods-components/react'; +import { + ODS_THEME_COLOR_HUE, + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { + ODS_TEXT_COLOR_HUE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import React from 'react'; +import TileGridSkeleton from '@/components/tile-grid-skeleton/TileGridSkeleton.component'; +import { useFetchHubCatalog } from '@/data/hooks/catalog/useCatalog'; +import { CatalogItem } from '@/types/catalog'; + +export default function Catalog() { + const { t } = useTranslation('hub/catalog'); + const { isLoading, data: catalog } = useFetchHubCatalog(); + const { trackClick } = useOvhTracking(); + + const trackProductOrder = (category: string, product: string) => { + trackClick({ + actionType: 'action', + actions: [ + 'hub', + 'dashboard', + 'catalog', + category.toLowerCase().replace(' ', '-'), + product.toLowerCase().replace(/_/g, '-'), + ], + }); + }; + + return ( + <> + + {t('manager_hub_catalog_title')} + + + {t('manager_hub_catalog_description')} + + {isLoading && } + {!isLoading && catalog && Object.keys(catalog).length > 0 && ( + <> + {Object.keys(catalog).map((category) => { + const items = catalog[category]; + return ( +
+ + {category} + +
+ {items.map((item: CatalogItem) => ( + + trackProductOrder(category, item.productName) + } + /> + ))} +
+
+ ); + })} + + )} + + ); +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/EnterpriseBillingSummary.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/EnterpriseBillingSummary.component.tsx new file mode 100644 index 000000000000..e82f0ca96ba3 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/EnterpriseBillingSummary.component.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { OsdsIcon, OsdsLink, OsdsText } from '@ovhcloud/ods-components/react'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { useTranslation } from 'react-i18next'; +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { LINK } from '@/pages/layout/layout.constants'; +import '@/pages/layout/BillingSummary.style.scss'; + +export default function EnterpriseBillingSummary() { + const { t } = useTranslation('hub/billing'); + const { trackClick } = useOvhTracking(); + + const trackNavigation = () => { + trackClick({ + actionType: 'action', + actions: ['activity', 'billing', 'show-all'], + }); + }; + + return ( +
+
+ + {t('hub_enterprise_billing_summary_title')} + + + {t('hub_enterprise_billing_summary_description')} + +
+ + {t('hub_enterprise_billing_summary_goto')} + + + + +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/KycFraudBanner.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/KycFraudBanner.component.tsx new file mode 100644 index 000000000000..1b1e648b9785 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/KycFraudBanner.component.tsx @@ -0,0 +1,120 @@ +import { Suspense, useContext, useEffect, useMemo } from 'react'; +import { Await } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { ODS_MESSAGE_TYPE } from '@ovhcloud/ods-components'; +import { + OsdsLink, + OsdsMessage, + OsdsSkeleton, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { useKyc } from '@/data/hooks/kyc/useKyc'; +import { KycProcedures, KycStatuses } from '@/types/kyc.type'; +import { SUPPORT_URLS } from '@/components/hub-support/HubSupport.constants'; +import { KYC_FRAUD_TRACK_IMPRESSION } from '@/pages/layout/layout.constants'; + +export default function KycFraudBanner() { + const { t } = useTranslation('hub/kyc'); + const { + environment, + shell: { navigation, tracking }, + } = useContext(ShellContext); + const { useKycStatus } = useKyc(KycProcedures.FRAUD); + const { data } = useKycStatus(); + const { user } = environment; + const region = environment.getRegion(); + const isEUOrCA = ['EU', 'CA'].includes(region); + + const shouldBeDisplayed = useMemo( + () => + data?.status === KycStatuses.REQUIRED || + (data?.status === KycStatuses.OPEN && Boolean(data?.ticketId)), + [data], + ); + + useEffect(() => { + if (shouldBeDisplayed) { + tracking.trackImpression({ + ...KYC_FRAUD_TRACK_IMPRESSION, + variant: data?.status, + }); + } + }, [shouldBeDisplayed]); + + const link = useMemo(() => { + if (data?.status === KycStatuses.REQUIRED) { + return navigation.getURL('dedicated', '#/documents', {}); + } + return isEUOrCA + ? Promise.resolve(`${SUPPORT_URLS.allTickets}${user.ovhSubsidiary}`) + : navigation.getURL('dedicated', '#/support/tickets', {}); + }, [data]); + + const trackLink = () => { + tracking.trackClickImpression({ + click: { + ...KYC_FRAUD_TRACK_IMPRESSION, + variant: data?.status, + }, + }); + }; + + return shouldBeDisplayed ? ( + + + {t(`kyc_fraud_${data.status}_banner_text`)} + {link && ( + + } + > + ( + + {t(`kyc_fraud_${data.status}_banner_link`)} + + )} + /> + + )} + + + ) : null; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/KycIndiaBanner.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/KycIndiaBanner.component.tsx new file mode 100644 index 000000000000..0a2f51ff3185 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/KycIndiaBanner.component.tsx @@ -0,0 +1,110 @@ +import { Suspense, useContext, useEffect, useMemo } from 'react'; +import { Await } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + OdsHTMLAnchorElementRel, + OdsHTMLAnchorElementTarget, +} from '@ovhcloud/ods-common-core'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { ODS_MESSAGE_TYPE } from '@ovhcloud/ods-components'; +import { + OsdsLink, + OsdsMessage, + OsdsSkeleton, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ShellContext, + useOvhTracking, + PageType, +} from '@ovh-ux/manager-react-shell-client'; +import { useKyc } from '@/data/hooks/kyc/useKyc'; +import { KycProcedures, KycStatuses } from '@/types/kyc.type'; + +export default function KycIndiaBanner() { + const { t } = useTranslation('hub/kyc'); + const { + shell: { navigation }, + } = useContext(ShellContext); + const { trackClick, trackPage } = useOvhTracking(); + const { useKycStatus } = useKyc(KycProcedures.INDIA); + const { data } = useKycStatus(); + + const shouldBeDisplayed = useMemo( + () => data?.status === KycStatuses.REQUIRED, + [data], + ); + + useEffect(() => { + if (shouldBeDisplayed && !data?.ticketId) { + trackPage({ + pageType: PageType.bannerInfo, + pageName: 'kyc-india', + }); + } + }, [data]); + + const link = useMemo( + () => + data?.ticketId + ? null + : navigation.getURL('dedicated', '#/identity-documents', {}), + [data], + ); + + const trackLink = () => { + trackClick({ + actionType: 'action', + actions: ['kyc-india', 'verify-identity'], + }); + }; + + return shouldBeDisplayed ? ( + + + {t( + `manager_hub_dashboard_kyc_banner_description${ + data.ticketId ? '_waiting' : '' + }`, + )} + {link && ( + + } + > + ( + + {t('manager_hub_dashboard_kyc_banner_link')} + + )} + /> + + )} + + + ) : null; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/NotificationsCarousel.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/NotificationsCarousel.component.tsx new file mode 100644 index 000000000000..b54eccbc53c5 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/NotificationsCarousel.component.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react'; +import { + OsdsIcon, + OsdsMessage, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_MESSAGE_TYPE, + ODS_TEXT_COLOR_INTENT, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { useFetchHubNotifications } from '@/data/hooks/notifications/useNotifications'; +import { Notification, NotificationType } from '@/types/notifications.type'; + +const getMessageColor = (type: NotificationType) => { + switch (type) { + case NotificationType.Success: + return ODS_TEXT_COLOR_INTENT.success; + case NotificationType.Error: + return ODS_TEXT_COLOR_INTENT.error; + case NotificationType.Warning: + return ODS_TEXT_COLOR_INTENT.warning; + case NotificationType.Info: + return ODS_TEXT_COLOR_INTENT.info; + default: + return ODS_TEXT_COLOR_INTENT.info; + } +}; + +const getMessageType = (type: NotificationType) => { + switch (type) { + case NotificationType.Success: + return ODS_MESSAGE_TYPE.success; + case NotificationType.Error: + return ODS_MESSAGE_TYPE.error; + case NotificationType.Warning: + return ODS_MESSAGE_TYPE.warning; + case NotificationType.Info: + return ODS_MESSAGE_TYPE.info; + default: + return ODS_MESSAGE_TYPE.info; + } +}; + +const getTextColor = (type: NotificationType) => { + switch (type) { + case NotificationType.Success: + return ODS_THEME_COLOR_INTENT.success; + case NotificationType.Error: + return ODS_THEME_COLOR_INTENT.error; + case NotificationType.Warning: + return ODS_THEME_COLOR_INTENT.warning; + case NotificationType.Info: + return ODS_THEME_COLOR_INTENT.info; + default: + return ODS_THEME_COLOR_INTENT.info; + } +}; + +export default function NotificationsCarousel() { + const { trackClick } = useOvhTracking(); + const { data: notifications } = useFetchHubNotifications(); + const [currentIndex, setCurrentIndex] = useState(0); + + const showNextNotification = () => { + setCurrentIndex( + (previousIndex) => (previousIndex + 1) % notifications.length, + ); + trackClick({ + actionType: 'action', + actions: ['hub', 'dashboard', 'alert', 'action'], + }); + }; + + return ( + <> + {notifications?.length > 0 && ( + 1 ? '!pb-8' : ''}`} + role="alert" + color={getMessageColor(notifications[currentIndex].level)} + type={getMessageType(notifications[currentIndex].level)} + data-testid="notifications_carousel" + > + + + + {notifications?.length > 1 && ( + <> + +
+ {notifications.map( + (notification: Notification, index: number) => ( + 0 ? 'ml-2' : '' + }`} + name={ODS_ICON_NAME.SHAPE_DOT} + size={ODS_ICON_SIZE.xxs} + color={getTextColor(notification.level)} + contrasted={currentIndex === index || undefined} + onClick={() => setCurrentIndex((previousIndex) => index)} + /> + ), + )} +
+ + )} +
+ )} + + ); +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/PaymentStatus.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/PaymentStatus.component.tsx new file mode 100644 index 000000000000..51074fa93a12 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/PaymentStatus.component.tsx @@ -0,0 +1,304 @@ +import { lazy, Suspense, useContext } from 'react'; +import { + OsdsChip, + OsdsIcon, + OsdsLink, + OsdsSkeleton, + OsdsTable, + OsdsText, + OsdsTile, +} from '@ovhcloud/ods-components/react'; +import { + ODS_CHIP_SIZE, + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_SKELETON_SIZE, + ODS_TEXT_COLOR_HUE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_LEVEL, +} from '@ovhcloud/ods-common-theming'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { + ShellContext, + useOvhTracking, +} from '@ovh-ux/manager-react-shell-client'; +import { useTranslation } from 'react-i18next'; +import { Await } from 'react-router-dom'; +import { useFetchHubBillingServices } from '@/data/hooks/billingServices/useBillingServices'; +import { BillingService } from '@/billing/types/billingServices.type'; +import useDateFormat from '@/hooks/dateFormat/useDateFormat'; + +const TileError = lazy(() => + import('@/components/tile-error/TileError.component'), +); +const BillingStatus = lazy(() => + import('@/billing/components/billing-status/BillingStatus.component'), +); +const ServicesActions = lazy(() => + import('@/billing/components/services-actions/ServicesActions.component'), +); + +type PaymentStatusProps = { + canManageBilling: boolean; +}; + +export default function PaymentStatus({ + canManageBilling, +}: PaymentStatusProps) { + const { t } = useTranslation('hub/payment-status'); + const { t: tProducts } = useTranslation('hub/products'); + const { t: tCommon } = useTranslation('hub'); + const { data, isLoading, refetch } = useFetchHubBillingServices(); + const { + shell: { navigation }, + } = useContext(ShellContext); + const { trackClick } = useOvhTracking(); + const { format } = useDateFormat({ + options: { + year: 'numeric', + month: 'long', + day: 'numeric', + }, + }); + + const autorenewLink = canManageBilling + ? navigation.getURL('dedicated', '#/billing/autorenew', {}) + : null; + + const trackServiceAccess = () => { + trackClick({ + actionType: 'action', + actions: ['activity', 'payment-status', 'go-to-service'], + }); + }; + + const services = data?.services; + const count = data?.count || 0; + + return ( + +
+ + {t('ovh_manager_hub_payment_status_tile_title')} + + {count} + + + {autorenewLink && ( + } + > + ( + + {tCommon('hub_support_see_more')} + + + + + )} + /> + + )} +
+ {!isLoading && !services && ( + } + > + + + )} + {!isLoading && services?.length === 0 && ( + + {t('ovh_manager_hub_payment_status_tile_no_services')} + + )} + {(isLoading || services) && ( + + + + {!isLoading && + services.map((service: BillingService) => ( + + + + {autorenewLink && ( + + )} + + ))} + {isLoading && + [1, 2, 3, 4].map((index) => ( + + + + ))} + +
+ {service.url ? ( + + {service.domain} + + ) : ( + + {service.domain} + + )} + + {tProducts( + `manager_hub_products_${service.serviceType}`, + )} + + +
+ + } + > + + +
+ {!service.isBillingSuspended() && ( +
+ {service.isOneShot() && + !service.isResiliated() && + !service.hasPendingResiliation() && ( + + - + + )} + {service.hasManualRenew() && + !service.isResiliated() && + !service.hasDebt() && ( + + {t( + 'ovh_manager_hub_payment_status_tile_before', + { + date: format(service.formattedExpiration), + }, + )} + + )} + {(service.isResiliated() || + service.hasPendingResiliation()) && ( + + {t('ovh_manager_hub_payment_status_tile_renew', { + date: format(service.formattedExpiration), + })} + + )} + {service.hasAutomaticRenewal() && + !service.isOneShot() && + !service.hasDebt() && + !service.isResiliated() && + !service.hasPendingResiliation() && ( + + {format(service.formattedExpiration)} + + )} + {service.hasDebt() && ( + + {t('ovh_manager_hub_payment_status_tile_now')} + + )} +
+ )} +
+ + } + > + ( + + )} + /> + +
+ +
+
+ )} +
+ ); +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/SiretBanner.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/SiretBanner.component.tsx new file mode 100644 index 000000000000..137c474684a2 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/SiretBanner.component.tsx @@ -0,0 +1,111 @@ +import { Suspense, useContext, useEffect, useMemo } from 'react'; +import { Await } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { + ODS_ICON_NAME, + ODS_ICON_SIZE, + ODS_MESSAGE_TYPE, + ODS_SKELETON_SIZE, +} from '@ovhcloud/ods-components'; +import { + OsdsIcon, + OsdsLink, + OsdsMessage, + OsdsSkeleton, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ShellContext, + useOvhTracking, + PageType, +} from '@ovh-ux/manager-react-shell-client'; + +export default function SiretBanner() { + const { t } = useTranslation('hub/siret'); + const { + shell: { navigation }, + environment: { user }, + } = useContext(ShellContext); + const { trackClick, trackPage } = useOvhTracking(); + + const shouldBeDisplayed = useMemo( + () => + !user.companyNationalIdentificationNumber && + user.legalform === 'corporation' && + user.country === 'FR', + [user], + ); + + const link = navigation.getURL('dedicated', '#/useraccount/infos', { + fieldToFocus: 'siretForm', + }); + + const trackLink = () => { + trackClick({ + actionType: 'action', + actions: ['hub', 'add-siret-banner', 'goto-edit-profile'], + }); + }; + + useEffect(() => { + if (shouldBeDisplayed) { + trackPage({ + pageType: PageType.bannerInfo, + pageName: 'siret', + }); + } + }, [shouldBeDisplayed]); + + return shouldBeDisplayed ? ( + +
+ + {t('manager_hub_dashboard_banner_siret')} + + } + > + ( + + {t('manager_hub_dashboard_banner_siret_link')} + + + )} + /> + + +
+
+ ) : null; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/SiretModal.component.tsx b/packages/manager/apps/hub-react/src/pages/layout/SiretModal.component.tsx new file mode 100644 index 000000000000..3b279b980d11 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/SiretModal.component.tsx @@ -0,0 +1,115 @@ +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + ODS_BUTTON_VARIANT, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { + OsdsButton, + OsdsModal, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { + ShellContext, + useOvhTracking, + PageType, +} from '@ovh-ux/manager-react-shell-client'; + +export default function SiretModal() { + const { t } = useTranslation('hub/siret'); + const { + shell: { navigation }, + environment: { user }, + } = useContext(ShellContext); + const { trackClick, trackPage } = useOvhTracking(); + const modal = useRef(null); + const [isClosed, setIsClosed] = useState(false); + + const shouldBeDisplayed = useMemo( + () => + !user.companyNationalIdentificationNumber && + user.legalform === 'corporation' && + user.country === 'FR', + [user], + ); + + const link = navigation.getURL('dedicated', '#/useraccount/infos', { + fieldToFocus: 'siretForm', + }) as Promise; + + const cancel = async () => { + trackClick({ + actionType: 'action', + actions: ['hub', 'add-siret-popup', 'cancel'], + }); + setIsClosed(() => true); + }; + + const confirm = async () => { + trackClick({ + actionType: 'action', + actions: ['hub', 'add-siret-popup', 'confirm'], + }); + window.open(await link, '_top'); + }; + + useEffect(() => { + if (shouldBeDisplayed) { + trackPage({ + pageType: PageType.popup, + pageName: 'siret', + }); + } + }, [shouldBeDisplayed]); + + return shouldBeDisplayed ? ( + + + + {t('manager_hub_dashboard_modal_siret_part_1')} + + + {t('manager_hub_dashboard_modal_siret_part_2')} + + + + {t('manager_hub_dashboard_modal_siret_cancel')} + + + {t('manager_hub_dashboard_modal_siret_link')} + + + ) : null; +} diff --git a/packages/manager/apps/hub-react/src/pages/layout/assets/billing-background.svg b/packages/manager/apps/hub-react/src/pages/layout/assets/billing-background.svg new file mode 100644 index 000000000000..9d8eb4d5b96d --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/assets/billing-background.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/apps/hub-react/src/pages/layout/layout.constants.ts b/packages/manager/apps/hub-react/src/pages/layout/layout.constants.ts new file mode 100644 index 000000000000..4e2584358f83 --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/layout.constants.ts @@ -0,0 +1,30 @@ +export const BILLING_FEATURE = 'billing:management'; +export const SIRET_BANNER_FEATURE = 'hub:banner-hub-invite-customer-siret'; +export const SIRET_MODAL_FEATURE = 'hub:popup-hub-invite-customer-siret'; +export const KYC_INDIA_FEATURE = 'identity-documents'; +export const KYC_FRAUD_FEATURE = 'procedures:fraud'; + +export const features = [ + BILLING_FEATURE, + SIRET_BANNER_FEATURE, + SIRET_MODAL_FEATURE, + KYC_INDIA_FEATURE, + KYC_FRAUD_FEATURE, +]; + +export const BILLING_SUMMARY_PERIODS_IN_MONTHS = [1, 3, 6]; + +export const LINK = 'https://billing.us.ovhcloud.com/login'; + +export const KYC_FRAUD_TRACK_IMPRESSION = { + campaignId: 'kyc-fraud', + creation: 'notification', + format: 'banner', + generalPlacement: 'manager-hub', +}; + +export default { + features, + BILLING_SUMMARY_PERIODS_IN_MONTHS, + LINK, +}; diff --git a/packages/manager/apps/hub-react/src/pages/layout/layout.test.tsx b/packages/manager/apps/hub-react/src/pages/layout/layout.test.tsx new file mode 100644 index 000000000000..e4b535e37f7c --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/layout.test.tsx @@ -0,0 +1,1056 @@ +import { ReactNode } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import * as reactShellClientModule from '@ovh-ux/manager-react-shell-client'; +import * as ReactComponentsModule from '@ovh-ux/manager-react-components'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import { + OdsSelectValueChangeEventDetail, + OsdsSelect, +} from '@ovhcloud/ods-components'; +import { User } from '@ovh-ux/manager-config'; + +import Layout from '@/pages/layout/layout'; +import BillingSummary from '@/pages/layout/BillingSummary.component'; +import EnterpriseBillingSummary from '@/pages/layout/EnterpriseBillingSummary.component'; +import PaymentStatus from '@/pages/layout/PaymentStatus.component'; +import Catalog from '@/pages/layout/Catalog.component'; +import SiretBanner from '@/pages/layout/SiretBanner.component'; +import SiretModal from '@/pages/layout/SiretModal.component'; +import KycIndiaBanner from '@/pages/layout/KycIndiaBanner.component'; +import KycFraudBanner from '@/pages/layout/KycFraudBanner.component'; +import NotificationsCarousel from '@/pages/layout/NotificationsCarousel.component'; + +import * as UseBillsHook from '@/data/hooks/bills/useBills'; +import * as UseBillingServicesHook from '@/data/hooks/billingServices/useBillingServices'; + +import { Notification, NotificationType } from '@/types/notifications.type'; +import { CatalogItem } from '@/types/catalog'; +import { ApiEnvelope } from '@/types/apiEnvelope.type'; +import { KycStatus } from '@/types/kyc.type'; +import { LastOrder } from '@/types/lastOrder.type'; +import { ProductList } from '@/types/services.type'; + +import { + FourServices, + NoServices, + TwoServices, +} from '@/_mock_/billingServices'; +import { catalogData } from '@/_mock_/catalog'; + +const queryClient = new QueryClient(); + +const trackClickMock = vi.fn(); +const trackPageMock = vi.fn(); +const trackImpressionMock = vi.fn(); +const trackClickImpressionMock = vi.fn(); +const mocks = vi.hoisted(() => ({ + bills: { + data: { + currency: { + code: 'EUR', + format: '{{price}} €', + symbol: '€', + }, + period: { from: '2024-08-01', to: '2024-08-31' }, + total: 0, + }, + isPending: true, + error: null, + refetch: vi.fn(() => ({})), + }, + catalog: { + data: {}, + isLoading: true, + } as { + data: Record; + isLoading: boolean; + }, + debt: { + data: { + unmaturedAmount: { + currencyCode: 'EUR', + value: 0, + text: '0.00 €', + }, + active: false, + dueAmount: { + currencyCode: 'EUR', + text: '0.00 €', + value: 0, + }, + pendingAmount: { + currencyCode: 'EUR', + text: '0.00 €', + value: 0, + }, + todoAmount: { + text: '0.00 €', + value: 0, + currencyCode: 'EUR', + }, + }, + isPending: false, + refetch: vi.fn(() => ({})), + }, + featureAvailability: { + 'billing:management': false, + 'hub:banner-hub-invite-customer-siret': true, + 'hub:popup-hub-invite-customer-siret': true, + 'identity-documents': true, + 'procedures:fraud': true, + }, + isLastOrderLoading: true, + isAccountSidebarVisible: false, + lastOrder: { + data: null, + status: 'OK', + } as LastOrder, + locale: 'fr_FR', + kycStatus: { + status: 'required', + } as KycStatus, + region: 'EU', + services: { + data: { count: 0, data: {} }, + status: 'OK', + }, +})); + +const shellContext = { + environment: { + user: { + enterprise: false, + companyNationalIdentificationNumber: null, + legalform: 'corporation', + country: 'FR', + } as User, + getUser: vi.fn(() => ({ + currency: { + code: 'USD', + }, + })), + getUserLocale: vi.fn(() => mocks.locale), + getRegion: vi.fn(() => mocks.region), + }, + shell: { + ux: { + hidePreloader: vi.fn(), + stopProgress: vi.fn(), + isAccountSidebarVisible: () => mocks.isAccountSidebarVisible, + }, + navigation: { + getURL: vi.fn( + () => + new Promise((resolve) => + setTimeout(() => resolve('https://fake-link.com'), 50), + ), + ), + }, + tracking: { + trackImpression: trackImpressionMock, + trackClickImpression: trackClickImpressionMock, + }, + }, +}; + +const renderComponent = (component: ReactNode) => { + return render( + + + {component} + + , + ); +}; + +vi.mock('react-router-dom', async (importOriginal) => { + const original: typeof reactShellClientModule = await importOriginal(); + return { + ...original, + useLocation: () => ({}), + }; +}); + +vi.mock('@ovh-ux/request-tagger', () => ({ + defineCurrentPage: () => ({}), +})); + +vi.mock('@/components/welcome/Welcome.component', () => ({ + default: () =>
Welcome
, +})); + +vi.mock('@/components/banner/Banner.component', () => ({ + default: () =>
Banner
, +})); + +vi.mock('@/components/products/Products.component', () => ({ + default: () =>
Products
, +})); + +vi.mock('@/components/hub-support/HubSupport.component', () => ({ + default: () =>
Support
, +})); + +vi.mock('@/components/hub-order-tracking/HubOrderTracking.component', () => ({ + default: () =>
Order Tracking
, +})); + +vi.mock('@/pages/layout/BillingSummary.component', () => ({ + default: () =>
Billing Summary
, +})); + +vi.mock('@/pages/layout/EnterpriseBillingSummary.component', () => ({ + default: () =>
Enterprise Billing Summary
, +})); + +vi.mock('@/billing/components/billing-status/BillingStatus.component', () => ({ + default: () =>
Billing Status
, +})); + +vi.mock( + '@/billing/components/services-actions/ServicesActions.component', + () => ({ + default: () =>
Service Actions
, + }), +); + +vi.mock('@/pages/layout/PaymentStatus.component', () => ({ + default: () =>
Payment Status
, +})); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const original: typeof reactShellClientModule = await importOriginal(); + return { + ...original, + useOvhTracking: vi.fn(() => ({ + trackPage: trackPageMock, + trackClick: trackClickMock, + trackCurrentPage: vi.fn(), + usePageTracking: vi.fn(), + })), + useRouteSynchro: vi.fn(() => {}), + }; +}); + +vi.mock('@/data/hooks/services/useServices', () => ({ + useFetchHubServices: (): { + data: ApiEnvelope; + isPending: boolean; + } => ({ data: mocks.services, isPending: false }), +})); + +vi.mock('@/data/hooks/lastOrder/useLastOrder', () => ({ + useFetchHubLastOrder: (): { data: LastOrder; isPending: boolean } => ({ + data: mocks.lastOrder, + isPending: mocks.isLastOrderLoading, + }), +})); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const module: typeof ReactComponentsModule = await importOriginal(); + return { + ...module, + useFeatureAvailability: (): { data: any; isPending: boolean } => ({ + data: mocks.featureAvailability, + isPending: false, + }), + }; +}); +vi.mock('@/data/hooks/bills/useBills', () => ({ + useFetchHubBills: vi.fn(() => mocks.bills), +})); +vi.mock('@/data/hooks/debt/useDebt', () => ({ + useFetchHubDebt: vi.fn(() => mocks.debt), +})); + +vi.mock('@/data/hooks/kyc/useKyc', () => ({ + useKyc: () => ({ + useKycStatus: () => ({ data: mocks.kycStatus }), + }), +})); + +vi.mock('@/data/hooks/notifications/useNotifications', () => ({ + useFetchHubNotifications: (): { + data: Notification[]; + isPending: boolean; + } => ({ + data: [ + { + data: {}, + date: '2022-02-08', + description: + 'Fraudulent emails circulate and direct to scam websites claiming to be OVHcloud. Find out more', + level: 'error' as NotificationType, + id: 'GLOBAL_COMMUNICATION_PHISHING', + status: 'acknowledged', + subject: 'General information', + }, + ], + isPending: false, + }), +})); + +vi.mock('@/data/hooks/catalog/useCatalog', () => ({ + useFetchHubCatalog: (): { + data: Record; + isLoading: boolean; + } => mocks.catalog, +})); + +const useBillingServicesMockValue: any = { + data: null, + isLoading: true, + error: null, +}; +vi.spyOn(UseBillingServicesHook, 'useFetchHubBillingServices').mockReturnValue( + useBillingServicesMockValue, +); + +const intlSpy = vi.spyOn(Intl, 'NumberFormat'); + +describe('Layout.page', () => { + it('should render skeletons while loading', async () => { + const { getByTestId, findByTestId } = renderComponent(); + expect(getByTestId('welcome_skeleton')).not.toBeNull(); + expect(getByTestId('banners_skeleton')).not.toBeNull(); + + const tileGridTitleSkeleton = await findByTestId( + 'tile_grid_title_skeleton', + ); + const tileGridContentSkeleton = await findByTestId( + 'tile_grid_content_skeletons', + ); + expect(tileGridTitleSkeleton).not.toBeNull(); + expect(tileGridContentSkeleton).not.toBeNull(); + }); + + it('should render correct components for "fresh" customers', async () => { + mocks.isLastOrderLoading = false; + const { + queryByText, + findByText, + findByTestId, + queryByTestId, + } = renderComponent(); + + const welcome = await findByText('Welcome'); + + expect(welcome).not.toBeNull(); + expect(queryByText('Banner')).not.toBeInTheDocument(); + expect(queryByTestId('notifications_carousel')).not.toBeInTheDocument(); + expect(queryByTestId('siret_banner')).not.toBeInTheDocument(); + expect(queryByTestId('siret_modal')).not.toBeInTheDocument(); + expect(queryByText('Payment Status')).not.toBeInTheDocument(); + expect(queryByText('Support')).not.toBeInTheDocument(); + expect(queryByText('Order Tracking')).not.toBeInTheDocument(); + expect(queryByText('Products')).not.toBeInTheDocument(); + + const kycIndiaBanner = await findByTestId('kyc_india_banner'); + const kycFraudBanner = await findByTestId('kyc_fraud_banner'); + const catalog = await findByTestId('catalog_title'); + expect(kycIndiaBanner).not.toBeNull(); + expect(kycFraudBanner).not.toBeNull(); + expect(catalog).not.toBeNull(); + }); + + it('should render correct components for customers with services or order', async () => { + mocks.lastOrder.data = { + date: '2024-08-22T12:24:08+02:00', + expirationDate: '2024-09-05T23:29:59+02:00', + orderId: 99999999999, + password: 'fakepassword', + pdfUrl: + 'https://www.fake-order-url.com?orderId=fakeId&orderPassword=fakePassword', + priceWithTax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + priceWithoutTax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + retractionDate: '2024-09-06T00:00:00+02:00', + tax: { + currencyCode: 'points', + text: '0 PTS', + value: 0, + }, + url: + 'https://www.fake-order-url.com?orderId=fakeId&orderPassword=fakePassword', + }; + const { + getByText, + getByTestId, + findByText, + findByTestId, + queryByTestId, + } = renderComponent(); + + expect(getByTestId('banner_skeleton')).not.toBeNull(); + + const welcome = await findByText('Welcome'); + const banner = await findByText('Banner'); + + expect(welcome).not.toBeNull(); + expect(banner).not.toBeNull(); + expect(queryByTestId('notifications_carousel')).not.toBeNull(); + expect(getByTestId('siret_banner')).not.toBeNull(); + expect(getByTestId('siret_modal')).not.toBeNull(); + expect(getByTestId('kyc_india_banner')).not.toBeNull(); + expect(getByTestId('kyc_fraud_banner')).not.toBeNull(); + expect(getByText('Support')).not.toBeNull(); + expect(getByText('Order Tracking')).not.toBeNull(); + expect(getByText('Products')).not.toBeNull(); + expect(queryByTestId('catalog_title')).not.toBeInTheDocument(); + + const billingSummary = await findByTestId('billing_summary'); + const paymentStatus = await findByTestId('payment_status'); + const siretBanner = await findByTestId('siret_banner'); + const siretModal = await findByTestId('siret_modal'); + expect(billingSummary).not.toBeNull(); + expect(paymentStatus).not.toBeNull(); + expect(siretBanner).not.toBeNull(); + expect(siretModal).not.toBeNull(); + }); + + it('should have correct css class if account sidebard is closed', async () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('hub_main_div')).toHaveAttribute( + 'class', + 'absolute hub-main w-full h-full ', + ); + }); + + it('should have correct css class if account sidebard is open', async () => { + mocks.isAccountSidebarVisible = true; + const { getByTestId } = renderComponent(); + + await waitFor(() => { + expect(getByTestId('hub_main_div')).toHaveAttribute( + 'class', + 'absolute hub-main w-full h-full hub-main-view_sidebar_expanded', + ); + }); + }); + + it('should scroll into view skipnav button is clicked', async () => { + const { getByTestId } = renderComponent(); + + // This is a workaround to overcome this jsdom issue: https://github.com/jsdom/jsdom/issues/1695 + const scrollIntoView = vi.fn(); + Element.prototype.scrollIntoView = scrollIntoView; + + const button = getByTestId('skipnav_button'); + await act(() => fireEvent.click(button)); + + expect(scrollIntoView).toHaveBeenCalled(); + }); + + it('should display enterprise billing summary if customer is enterprise', async () => { + shellContext.environment.user.enterprise = true; + const { findByTestId } = renderComponent(); + + const enterpriseBillingSummary = await findByTestId( + 'enterprise_billing_summary', + ); + expect(enterpriseBillingSummary).not.toBeNull(); + }); + + describe('BillingSummary component', () => { + vi.unmock('@/pages/layout/BillingSummary.component'); + + it('should render skeletons while loading', async () => { + const { getByText, getByTestId } = renderComponent(); + + expect(getByText('hub_billing_summary_title')).not.toBeNull(); + expect(getByTestId('bills_status_skeleton')).not.toBeNull(); + expect(getByTestId('bills_period_selector')).not.toBeNull(); + expect(getByTestId('bills_link_skeleton')).not.toBeNull(); + }); + + it('should display correct wording when customer has no bills', async () => { + mocks.bills.isPending = false; + const { findByTestId, getByText, getByTestId } = renderComponent( + , + ); + + expect(getByText('hub_billing_summary_debt_no_bills')).not.toBeNull(); + expect(getByTestId('bills_link_skeleton')).not.toBeNull(); + const link = await findByTestId('bills_link'); + expect(link).not.toBeNull(); + }); + + it('should display correct wording when customer has bills but no debt', async () => { + mocks.bills.data.total = 15034.94; + const expectedAmount = '15\u202f034'; + const { getByText, getByTestId, getAllByTestId } = renderComponent( + , + ); + + const amount = getAllByTestId('bills_amount_container'); + expect(amount.length).toBe(1); + expect(amount[0].innerHTML.includes(expectedAmount)).toBe(true); + + expect(getByTestId('bills_amount_container')).not.toBeNull(); + expect(getByText('hub_billing_summary_debt_null')).not.toBeNull(); + await waitFor(() => { + expect(intlSpy).toHaveBeenCalledWith('fr-FR', { + style: 'currency', + currency: mocks.bills.data.currency.code, + }); + }); + }); + + it('should update bills amount when period is changed', async () => { + const useFetchHubBillsSpy = vi.spyOn(UseBillsHook, 'useFetchHubBills'); + const { getByTestId } = renderComponent(); + const periodSelector = (getByTestId( + 'bills_period_selector', + ) as unknown) as OsdsSelect; + + await act(() => + periodSelector.odsValueChange.emit({ + value: '3', + } as OdsSelectValueChangeEventDetail), + ); + + await waitFor(() => { + expect(useFetchHubBillsSpy).toHaveBeenCalledWith(3); + }); + }); + + it('should track click on bills link', async () => { + const { findByTestId, getByTestId } = renderComponent(); + + expect(getByTestId('bills_link_skeleton')).not.toBeNull(); + const link = await findByTestId('bills_link'); + await act(() => fireEvent.click(link)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['activity', 'billing', 'show-all'], + }); + }); + + it('should display debt information if customer has debt', async () => { + mocks.debt.data.dueAmount.value = 964.23; + const { getByTestId, findByTestId } = renderComponent(); + expect(getByTestId('debt_amount')).not.toBeNull(); + + const link = await findByTestId('debt_link'); + expect(link).not.toBeNull(); + }); + + it('should display error tile', async () => { + mocks.bills.error = new Error(); + const { findByText } = renderComponent(); + const tileError = await findByText('manager_hub_error_tile_oops'); + expect(tileError).not.toBeNull(); + }); + }); + + describe('EnterpriseBillingSummary component', () => { + vi.unmock('@/pages/layout/EnterpriseBillingSummary.component'); + + it('should render title, description and tracked link', async () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('enterprise_billing_summary')).not.toBeNull(); + expect(getByTestId('enterprise_billing_summary_title')).not.toBeNull(); + expect( + getByTestId('enterprise_billing_summary_description'), + ).not.toBeNull(); + const link = getByTestId('enterprise_billing_summary_link'); + expect(link).not.toBeNull(); + + await act(() => fireEvent.click(link)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['activity', 'billing', 'show-all'], + }); + }); + }); + + describe('PaymentStatus component', () => { + vi.unmock('@/pages/layout/PaymentStatus.component'); + it('should render title and badge', async () => { + const { findByTestId, getByTestId } = renderComponent( + , + ); + + expect(getByTestId('payment_status_title')).not.toBeNull(); + expect(getByTestId('payment_status_badge')).not.toBeNull(); + expect(getByTestId('my_services_link_skeleton')).not.toBeNull(); + + const myServiceLink = await findByTestId('my_services_link'); + expect(myServiceLink).not.toBeNull(); + }); + + it('should render table with skeletons while loading', async () => { + const { getAllByTestId, getByTestId } = renderComponent( + , + ); + + expect(getByTestId('payment_status_table')).not.toBeNull(); + expect(getAllByTestId('payment_status_skeleton_line').length).toBe(4); + }); + + it('should render error if loading is done and no data has been retrieved', async () => { + useBillingServicesMockValue.isLoading = false; + const { findByText } = renderComponent( + , + ); + + const tileError = await findByText('manager_hub_error_tile_oops'); + expect(tileError).not.toBeNull(); + }); + + it('should render a message if loading is done and user has no services', async () => { + useBillingServicesMockValue.data = NoServices; + const { getByText } = renderComponent( + , + ); + + expect( + getByText('ovh_manager_hub_payment_status_tile_no_services'), + ).not.toBeNull(); + }); + + it('should render the correct number of services', async () => { + useBillingServicesMockValue.data = TwoServices; + const { findAllByText, getAllByTestId, getByTestId } = renderComponent( + , + ); + + expect(getByTestId('payment_status_badge').innerHTML.includes('2')).toBe( + true, + ); + const servicesLine = getAllByTestId('billing_service'); + expect(servicesLine.length).toBe(2); + expect(getAllByTestId('billing_status_skeleton').length).toBe(2); + const servicesStatuses = await findAllByText('Billing Status'); + expect(servicesStatuses.length).toBe(2); + expect(getAllByTestId('service_expiration_date_message').length).toBe(2); + }); + + it('should display the correct message for service in debt', async () => { + const { getByTestId } = renderComponent( + , + ); + + expect(getByTestId('service_with_debt')).not.toBeNull(); + }); + + it('should display the correct message for service in automatic renew without debt and not resiliated', async () => { + const { getByTestId } = renderComponent( + , + ); + + expect(getByTestId('service_with_expiration_date')).not.toBeNull(); + }); + + it('should display service type for each service', async () => { + useBillingServicesMockValue.data = FourServices; + const { getByText } = renderComponent( + , + ); + + expect(getByText('manager_hub_products_HOSTING_WEB')).not.toBeNull(); + expect(getByText('manager_hub_products_DOMAIN')).not.toBeNull(); + expect(getByText('manager_hub_products_DEDICATED_SERVER')).not.toBeNull(); + expect(getByText('manager_hub_products_DEDICATED_CLOUD')).not.toBeNull(); + }); + + it('should display the correct information for resiliated service', async () => { + const { getByTestId, getByText } = renderComponent( + , + ); + const serviceLink = getByText('serviceResiliated'); + expect(serviceLink).not.toBeNull(); + expect(serviceLink).toHaveAttribute( + 'href', + 'https://www.ovh.com/manager/#/web/configuration/hosting/serviceResiliated', + ); + + expect(getByTestId('service_with_termination_date')).not.toBeNull(); + }); + + it('should display the correct information for service in manual renew without debt and not resiliated', async () => { + const { getByTestId, getByText } = renderComponent( + , + ); + const serviceLink = getByText( + 'serviceWithManualRenewNotResiliatedWithoutDebt', + ); + expect(serviceLink).not.toBeNull(); + expect(serviceLink).toHaveAttribute( + 'href', + 'https://www.ovh.com/manager/#/web/configuration/domain/serviceWithManualRenewNotResiliatedWithoutDebt/information', + ); + + expect(getByTestId('service_valid_until_date')).not.toBeNull(); + }); + + it('should display the correct information for one shot service not resiliated', async () => { + const { getByTestId, getByText } = renderComponent( + , + ); + const serviceLink = getByText('serviceOneShotWithoutResiliation'); + expect(serviceLink).not.toBeNull(); + expect(serviceLink).toHaveAttribute( + 'href', + 'https://www.ovh.com/manager/#/dedicated/server/serviceOneShotWithoutResiliation', + ); + + expect(getByTestId('service_without_expiration_date')).not.toBeNull(); + }); + + it('should display the correct information for service without url and billing suspended', async () => { + const { getByText } = renderComponent( + , + ); + const serviceWithoutUrlAndSuspendedBillingLink = getByText( + 'serviceWithoutUrlAndSuspendedBilling', + ); + expect(serviceWithoutUrlAndSuspendedBillingLink).not.toBeNull(); + expect(serviceWithoutUrlAndSuspendedBillingLink).not.toHaveAttribute( + 'href', + ); + }); + + it('should track service access', async () => { + const { getByText } = renderComponent( + , + ); + const service = getByText( + 'serviceWithManualRenewNotResiliatedWithoutDebt', + ); + expect(service).not.toBeNull(); + await act(() => fireEvent.click(service)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['activity', 'payment-status', 'go-to-service'], + }); + }); + + describe('With billing management', () => { + it('should render "see all" link', async () => { + const { + getAllByTestId, + findAllByText, + findByTestId, + getByTestId, + } = renderComponent(); + expect(getByTestId('my_services_link_skeleton')).not.toBeNull(); + expect(getAllByTestId('services_actions_skeleton').length).toBe(4); + + const myServiceLink = await findByTestId('my_services_link'); + expect(myServiceLink).not.toBeNull(); + + const serviceActionsComponents = await findAllByText('Service Actions'); + expect(serviceActionsComponents).not.toBeNull(); + }); + }); + + describe('Without billing management', () => { + it('should not render "see all" link', async () => { + const { queryAllByTestId, queryByTestId } = renderComponent( + , + ); + expect( + queryByTestId('my_services_link_skeleton'), + ).not.toBeInTheDocument(); + expect(queryAllByTestId('services_actions_skeleton').length).toBe(0); + }); + }); + }); + + describe('SiretBanner component', () => { + it('should render for french company without national company identification number', async () => { + const { getByText, findByText } = renderComponent(); + expect(getByText('manager_hub_dashboard_banner_siret')).not.toBeNull(); + const link = await findByText('manager_hub_dashboard_banner_siret_link'); + expect(link).not.toBeNull(); + }); + + it('should send tracking hit when displayed', async () => { + trackPageMock.mockReset(); + renderComponent(); + + expect(trackPageMock).toHaveBeenCalledWith({ + pageType: 'banner-info', + pageName: 'siret', + }); + }); + + it('should send tracking hit when clicking on the link', async () => { + trackClickMock.mockReset(); + const { findByText } = renderComponent(); + const link = await findByText('manager_hub_dashboard_banner_siret_link'); + expect(link).not.toBeNull(); + await act(() => fireEvent.click(link)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['hub', 'add-siret-banner', 'goto-edit-profile'], + }); + }); + + it('should not be displayed for non company customer', async () => { + shellContext.environment.user.legalform = 'individual'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_banner')).not.toBeInTheDocument(); + }); + + it('should not be displayed for customer not residing in France', async () => { + shellContext.environment.user.legalform = 'corporation'; + shellContext.environment.user.country = 'GB'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_banner')).not.toBeInTheDocument(); + }); + + it('should not be displayed for french company with national company identification number', async () => { + shellContext.environment.user.country = 'FR'; + shellContext.environment.user.companyNationalIdentificationNumber = 99999; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_banner')).not.toBeInTheDocument(); + }); + }); + + describe('SiretModal component', () => { + it('should render for french company without national company identification number', async () => { + shellContext.environment.user.companyNationalIdentificationNumber = null; + const { getByTestId, getByText } = renderComponent(); + expect(getByTestId('siret_modal')).not.toBeNull(); + expect( + getByText('manager_hub_dashboard_modal_siret_part_1'), + ).not.toBeNull(); + expect( + getByText('manager_hub_dashboard_modal_siret_part_2'), + ).not.toBeNull(); + expect( + getByText('manager_hub_dashboard_modal_siret_cancel'), + ).not.toBeNull(); + expect( + getByText('manager_hub_dashboard_modal_siret_link'), + ).not.toBeNull(); + }); + + it('should send tracking hit when displayed', async () => { + trackPageMock.mockReset(); + renderComponent(); + + expect(trackPageMock).toHaveBeenCalledWith({ + pageType: 'pop-up', + pageName: 'siret', + }); + }); + + it('should send tracking hit when clicking on confirmation button', async () => { + trackClickMock.mockReset(); + const { findByText } = renderComponent(); + const confirmLink = await findByText( + 'manager_hub_dashboard_modal_siret_link', + ); + expect(confirmLink).not.toBeNull(); + await act(() => fireEvent.click(confirmLink)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['hub', 'add-siret-popup', 'confirm'], + }); + }); + + it('should send tracking hit when clicking and close modal on cancellation button', async () => { + trackClickMock.mockReset(); + const { findByText, queryByTestId } = renderComponent(); + const cancelLink = await findByText( + 'manager_hub_dashboard_modal_siret_cancel', + ); + expect(cancelLink).not.toBeNull(); + await act(() => fireEvent.click(cancelLink)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['hub', 'add-siret-popup', 'cancel'], + }); + expect(queryByTestId('siret_banner')).not.toBeInTheDocument(); + }); + + it('should not be displayed for non company customer', async () => { + shellContext.environment.user.legalform = 'individual'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_modal')).not.toBeInTheDocument(); + }); + + it('should not be displayed for customer not residing in France', async () => { + shellContext.environment.user.legalform = 'corporation'; + shellContext.environment.user.country = 'GB'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_modal')).not.toBeInTheDocument(); + }); + + it('should not be displayed for french company with national company identification number', async () => { + shellContext.environment.user.country = 'FR'; + shellContext.environment.user.companyNationalIdentificationNumber = 99999; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('siret_modal')).not.toBeInTheDocument(); + }); + }); + + describe('KycIndiaBanner component', () => { + it('should render the banner if user is required to validated his KYC', async () => { + trackClickMock.mockReset(); + const { findByTestId, getByTestId } = renderComponent(); + expect(getByTestId('kyc_india_banner')).not.toBeNull(); + + expect(trackPageMock).toHaveBeenCalledWith({ + pageType: 'banner-info', + pageName: 'kyc-india', + }); + expect(getByTestId('kyc_india_banner_link_skeleton')).not.toBeNull(); + const link = await findByTestId('kyc_india_link'); + expect(link).not.toBeNull(); + + await act(() => fireEvent.click(link)); + + expect(trackClickMock).toHaveBeenCalledWith({ + actionType: 'action', + actions: ['kyc-india', 'verify-identity'], + }); + }); + + it('should render the banner and track display if user has started his KYC validation', async () => { + mocks.kycStatus.ticketId = 'CS0013982'; + const { getByTestId } = renderComponent(); + expect(getByTestId('kyc_india_banner')).not.toBeNull(); + }); + + it('should render nothing if user already validated his KYC', async () => { + mocks.kycStatus.status = 'ok'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('kyc_india_banner')).not.toBeInTheDocument(); + }); + }); + + describe('KycFraudBanner component', () => { + it('should render the banner if user is required to validated his KYC', async () => { + mocks.kycStatus.status = 'required'; + delete mocks.kycStatus.ticketId; + const { findByTestId, getByTestId } = renderComponent(); + expect(getByTestId('kyc_fraud_banner')).not.toBeNull(); + + expect(trackImpressionMock).toHaveBeenCalledWith({ + campaignId: 'kyc-fraud', + creation: 'notification', + format: 'banner', + generalPlacement: 'manager-hub', + variant: 'required', + }); + const link = await findByTestId('kyc_fraud_link'); + expect(link).not.toBeNull(); + + await act(() => fireEvent.click(link)); + + expect(trackClickImpressionMock).toHaveBeenCalledWith({ + click: { + campaignId: 'kyc-fraud', + creation: 'notification', + format: 'banner', + generalPlacement: 'manager-hub', + variant: 'required', + }, + }); + }); + + it('should render the banner and track display if user has started his KYC validation', async () => { + trackImpressionMock.mockReset(); + mocks.kycStatus.status = 'open'; + mocks.kycStatus.ticketId = 'CS0013982'; + const { getByTestId } = renderComponent(); + expect(getByTestId('kyc_fraud_banner')).not.toBeNull(); + + expect(trackImpressionMock).toHaveBeenCalledWith({ + campaignId: 'kyc-fraud', + creation: 'notification', + format: 'banner', + generalPlacement: 'manager-hub', + variant: 'open', + }); + }); + + it('should render nothing if user already validated his KYC', async () => { + mocks.kycStatus.status = 'ok'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('kyc_fraud_banner')).not.toBeInTheDocument(); + }); + }); + + describe('NotificationsCarousel component', () => { + it('should render a single notification without "navigation"', async () => { + const { getByTestId, queryByTestId } = renderComponent( + , + ); + + expect(getByTestId('notification_content')).not.toBeNull(); + expect(queryByTestId('next-notification-button')).not.toBeInTheDocument(); + expect(queryByTestId('notification-navigation')).not.toBeInTheDocument(); + }); + }); + + describe('Catalog component', () => { + mocks.catalog.data = catalogData as Record; + it('should display a title and a description', async () => { + const { getByText } = renderComponent(); + + expect(getByText('manager_hub_catalog_title')).not.toBeNull(); + expect(getByText('manager_hub_catalog_description')).not.toBeNull(); + }); + + it('should display skeletons', async () => { + const { findByTestId } = renderComponent(); + + const tileGridTitleSkeleton = await findByTestId( + 'tile_grid_title_skeleton', + ); + const tileGridContentSkeleton = await findByTestId( + 'tile_grid_content_skeletons', + ); + expect(tileGridTitleSkeleton).not.toBeNull(); + expect(tileGridContentSkeleton).not.toBeNull(); + }); + + it('should display skeletons', async () => { + const { findByTestId } = renderComponent(); + + const tileGridTitleSkeleton = await findByTestId( + 'tile_grid_title_skeleton', + ); + const tileGridContentSkeleton = await findByTestId( + 'tile_grid_content_skeletons', + ); + expect(tileGridTitleSkeleton).not.toBeNull(); + expect(tileGridContentSkeleton).not.toBeNull(); + }); + + it('should display correct amount of elements', async () => { + mocks.catalog.isLoading = false; + const { getAllByTestId } = renderComponent(); + + expect(getAllByTestId('catalog_products_list').length).toBe(2); + expect(getAllByTestId('catalog_product_item').length).toBe(6); + }); + }); +}); diff --git a/packages/manager/apps/hub-react/src/pages/layout/layout.tsx b/packages/manager/apps/hub-react/src/pages/layout/layout.tsx new file mode 100644 index 000000000000..d72c47e06f9b --- /dev/null +++ b/packages/manager/apps/hub-react/src/pages/layout/layout.tsx @@ -0,0 +1,327 @@ +import { useEffect, useContext, useRef, Suspense, lazy, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { + useOvhTracking, + useRouteSynchro, + ShellContext, +} from '@ovh-ux/manager-react-shell-client'; +import { + OsdsButton, + OsdsSkeleton, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, + ODS_TEXT_COLOR_HUE, + ODS_TEXT_LEVEL, + ODS_TEXT_SIZE, +} from '@ovhcloud/ods-components'; +import { defineCurrentPage } from '@ovh-ux/request-tagger'; +import { useFeatureAvailability } from '@ovh-ux/manager-react-components'; +import { useTranslation } from 'react-i18next'; +import { + features, + BILLING_FEATURE, + SIRET_BANNER_FEATURE, + SIRET_MODAL_FEATURE, +} from '@/pages/layout/layout.constants'; +import { useFetchHubServices } from '@/data/hooks/services/useServices'; +import { useFetchHubLastOrder } from '@/data/hooks/lastOrder/useLastOrder'; +// Components used in Suspense's fallback cannot be lazy loaded (break testing) +import TileGridSkeleton from '@/components/tile-grid-skeleton/TileGridSkeleton.component'; +import TileSkeleton from '@/components/tile-grid-skeleton/tile-skeleton/TileSkeleton.component'; + +const Welcome = lazy(() => import('@/components/welcome/Welcome.component')); +const Banner = lazy(() => import('@/components/banner/Banner.component')); +const Products = lazy(() => import('@/components/products/Products.component')); +const Catalog = lazy(() => import('@/pages/layout/Catalog.component')); +const OrderTracking = lazy(() => + import('@/components/hub-order-tracking/HubOrderTracking.component'), +); +const HubSupport = lazy(() => + import('@/components/hub-support/HubSupport.component'), +); +const BillingSummary = lazy(() => + import('@/pages/layout/BillingSummary.component'), +); +const EnterpriseBillingSummary = lazy(() => + import('@/pages/layout/EnterpriseBillingSummary.component'), +); +const PaymentStatus = lazy(() => + import('@/pages/layout/PaymentStatus.component'), +); +const SiretBanner = lazy(() => import('@/pages/layout/SiretBanner.component')); +const SiretModal = lazy(() => import('@/pages/layout/SiretModal.component')); +const KycIndiaBanner = lazy(() => + import('@/pages/layout/KycIndiaBanner.component'), +); +const KycFraudBanner = lazy(() => + import('@/pages/layout/KycFraudBanner.component'), +); +const NotificationsCarousel = lazy(() => + import('@/pages/layout/NotificationsCarousel.component'), +); + +export default function Layout() { + const location = useLocation(); + const { + shell, + environment: { user }, + } = useContext(ShellContext); + const { trackCurrentPage } = useOvhTracking(); + const { t } = useTranslation(); + const mainContentRef = useRef(null); + const [isAccountSidebarVisible, setIsAccountSidebarVisible] = useState(false); + useRouteSynchro(); + + useEffect(() => { + trackCurrentPage(); + }, [location]); + + useEffect(() => { + const getIsAccountSidebarVisible = async () => { + const newValueIsAccountSidebarVisible = (await shell.ux.isAccountSidebarVisible()) as boolean; + setIsAccountSidebarVisible(() => newValueIsAccountSidebarVisible); + }; + defineCurrentPage(`app.dashboard`); + shell.ux.hidePreloader(); + shell.ux.stopProgress(); + getIsAccountSidebarVisible(); + }, []); + + const { + data: availability, + isPending: isAvailabilityLoading, + } = useFeatureAvailability(features); + const { + data: services, + isPending: areServicesLoading, + } = useFetchHubServices(); + const { + data: lastOrder, + isPending: isLastOrderLoading, + } = useFetchHubLastOrder(); + + function scrollToComponent() { + mainContentRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + + const isLoading = areServicesLoading || isLastOrderLoading; + const isFreshCustomer = !( + services?.data?.count || + (lastOrder?.status === 'OK' && lastOrder?.data) + ); + + return ( + <> +
+ + {t('manager_hub_skip_to_main_content')} + +
+
+
+
+ {/* Skip content target */} + + {/* /Skip content target */} +
+
+
+ + } + > + + +
+ {isLoading && ( + + )} + {!isLoading && !isFreshCustomer && ( + <> + + } + > + + + + } + > + + + + )} +
+ {isLoading && ( + <> + + + + + + )} + {!isLoading && !isFreshCustomer && ( + <> + {availability?.[SIRET_BANNER_FEATURE] && ( + + } + > + + + )} + {availability?.[SIRET_MODAL_FEATURE] && ( + + } + > + + + )} + + )} + {!isLoading && ( + <> + {availability?.['identity-documents'] && ( + + } + > + + + )} + {availability?.['procedures:fraud'] && ( + + } + > + + + )} + + )} + {!isFreshCustomer && ( + + {t('manager_hub_dashboard_overview')} + + )} +
+ {isLoading && } + {!isLoading && !isFreshCustomer && ( + <> +
+ }> + + +
+
+ + } + > + {user.enterprise ? ( + + ) : ( + + )} + +
+ + )} + {!isLoading && ( + <> +
+ + } + > + + +
+ {!isFreshCustomer && ( +
+ + } + > + + +
+ )} + + )} +
+
+ {isLoading && } + {!isLoading && !isFreshCustomer && ( + }> + + + )} + {!isLoading && isFreshCustomer && ( + }> + + + )} +
+
+
+
+
+
+
+ + ); +} diff --git a/packages/manager/apps/hub-react/src/queryClient.ts b/packages/manager/apps/hub-react/src/queryClient.ts new file mode 100644 index 000000000000..cf824f48575c --- /dev/null +++ b/packages/manager/apps/hub-react/src/queryClient.ts @@ -0,0 +1,12 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 300_000, + refetchOnWindowFocus: false, + }, + }, +}); + +export default queryClient; diff --git a/packages/manager/apps/hub-react/src/routes/routes.constant.ts b/packages/manager/apps/hub-react/src/routes/routes.constant.ts new file mode 100644 index 000000000000..8fcb96e6009d --- /dev/null +++ b/packages/manager/apps/hub-react/src/routes/routes.constant.ts @@ -0,0 +1,7 @@ +export const urls = { + root: '/', + onboarding: '/onboarding', + listing: '/', + dashboard: '/:serviceName', + tab2: 'Tab2', +}; diff --git a/packages/manager/apps/hub-react/src/routes/routes.tsx b/packages/manager/apps/hub-react/src/routes/routes.tsx new file mode 100644 index 000000000000..8569e75d7f30 --- /dev/null +++ b/packages/manager/apps/hub-react/src/routes/routes.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { RouteObject } from 'react-router-dom'; +import NotFound from '@/pages/404'; + +const lazyRouteConfig = (importFn: CallableFunction): Partial => { + return { + lazy: async () => { + const { default: moduleDefault, ...moduleExports } = await importFn(); + return { + Component: moduleDefault, + ...moduleExports, + }; + }, + }; +}; + +export const Routes: any = [ + { + path: '/', + ...lazyRouteConfig(() => import('@/pages/layout/layout')), + handle: { + tracking: { + pageName: 'dashboard', + }, + }, + }, + { + path: '*', + element: , + }, +]; diff --git a/packages/manager/apps/hub-react/src/setupTests.ts b/packages/manager/apps/hub-react/src/setupTests.ts new file mode 100644 index 000000000000..39651aefe5e5 --- /dev/null +++ b/packages/manager/apps/hub-react/src/setupTests.ts @@ -0,0 +1,12 @@ +import '@testing-library/jest-dom'; +import 'element-internals-polyfill'; +import { vi } from 'vitest'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (translationKey: string) => translationKey, + i18n: { + changeLanguage: () => new Promise(() => {}), + }, + }), +})); diff --git a/packages/manager/apps/hub-react/src/tracking.constant.ts b/packages/manager/apps/hub-react/src/tracking.constant.ts new file mode 100644 index 000000000000..38584a18f82a --- /dev/null +++ b/packages/manager/apps/hub-react/src/tracking.constant.ts @@ -0,0 +1,20 @@ +export const LEVEL2 = { + EU: { + config: { + level2: '88', + }, + }, + CA: { + config: { + level2: '88', + }, + }, + US: { + config: { + level2: '88', + }, + }, +}; +export const UNIVERSE = 'hub'; +export const SUB_UNIVERSE = 'app'; +export const APP_NAME = ''; diff --git a/packages/manager/apps/hub-react/src/types/apiEnvelope.type.ts b/packages/manager/apps/hub-react/src/types/apiEnvelope.type.ts new file mode 100644 index 000000000000..68aa5f9488aa --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/apiEnvelope.type.ts @@ -0,0 +1,9 @@ +export type ApiEnvelope = { + data: Data; + status: string; +}; + +export type ApiAggregateEnvelope = { + count: number; + data: Data; +}; diff --git a/packages/manager/apps/hub-react/src/types/banner.type.ts b/packages/manager/apps/hub-react/src/types/banner.type.ts new file mode 100644 index 000000000000..d7afa637cc71 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/banner.type.ts @@ -0,0 +1,17 @@ +type BannerImage = { + src: string; + width: number; + height: number; +}; + +type BannerImages = { + default: BannerImage; + responsive: BannerImage; +}; + +export type Banner = { + alt: string; + images: BannerImages; + link: string; + tracker: string; +}; diff --git a/packages/manager/apps/hub-react/src/types/bills.type.ts b/packages/manager/apps/hub-react/src/types/bills.type.ts new file mode 100644 index 000000000000..d2656c8ee51c --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/bills.type.ts @@ -0,0 +1,24 @@ +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +type Currency = { + code: string; + format: string; + symbol: string; +}; + +export type Period = { + from: string; + to: string; +}; + +export type Bills = { + currency: Currency; + period: Period; + total: number; +}; + +type BillsContainer = { + bills: ApiEnvelope; +}; + +export type BillsCapsule = ApiEnvelope; diff --git a/packages/manager/apps/hub-react/src/types/catalog.ts b/packages/manager/apps/hub-react/src/types/catalog.ts new file mode 100644 index 000000000000..e990b334feee --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/catalog.ts @@ -0,0 +1,20 @@ +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export type CatalogItem = { + categories: string[]; + category: string; + description: string; + highlight?: boolean; + id: number; + lang: string; + name: string; + order: string; + productName: string; + regionTags: string[]; + universe: string; + url: string; +}; + +export type CatalogData = { + catalog: ApiEnvelope; +}; diff --git a/packages/manager/apps/hub-react/src/types/debt.type.ts b/packages/manager/apps/hub-react/src/types/debt.type.ts new file mode 100644 index 000000000000..d4c9540d2066 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/debt.type.ts @@ -0,0 +1,13 @@ +type Amount = { + currencyCode: string; + value: number; + text: string; +}; + +export type Debt = { + unmaturedAmount: Amount; + active: boolean; + dueAmount: Amount; + pendingAmount: Amount; + todoAmount: Amount; +}; diff --git a/packages/manager/apps/hub-react/src/types/kyc.type.ts b/packages/manager/apps/hub-react/src/types/kyc.type.ts new file mode 100644 index 000000000000..2beac9bb20e3 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/kyc.type.ts @@ -0,0 +1,15 @@ +export enum KycProcedures { + INDIA = 'identity', + FRAUD = 'fraud', +} + +export enum KycStatuses { + OK = 'ok', + OPEN = 'open', + REQUIRED = 'required', +} + +export type KycStatus = { + status: string; + ticketId?: string; +}; diff --git a/packages/manager/apps/hub-react/src/types/lastOrder.type.ts b/packages/manager/apps/hub-react/src/types/lastOrder.type.ts new file mode 100644 index 000000000000..92cdf5dc1390 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/lastOrder.type.ts @@ -0,0 +1,28 @@ +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +type Price = { + currencyCode: string; + text: string; + value: number; +}; + +export type Order = { + date: string; + expirationDate: string; + orderId: number; + password: string; + pdfUrl: string; + priceWithTax: Price; + priceWithoutTax: Price; + retractionDate: string; + tax: Price; + url: string; +}; + +export type LastOrder = ApiEnvelope; + +type LastOrderData = { + lastOrder: LastOrder; +}; + +export type LastOrderEnvelope = ApiEnvelope; diff --git a/packages/manager/apps/hub-react/src/types/notifications.type.ts b/packages/manager/apps/hub-react/src/types/notifications.type.ts new file mode 100644 index 000000000000..7675aa2febd4 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/notifications.type.ts @@ -0,0 +1,22 @@ +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export enum NotificationType { + Success = 'success', + Error = 'error', + Info = 'info', + Warning = 'warning', +} + +export type Notification = { + data: any; + date: string; + description: string; + id: string; + level: NotificationType; + status: string; + subject: string; +}; + +export type NotificationsList = { + notifications: ApiEnvelope; +}; diff --git a/packages/manager/apps/hub-react/src/types/order.type.ts b/packages/manager/apps/hub-react/src/types/order.type.ts new file mode 100644 index 000000000000..c1e2a46eec2d --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/order.type.ts @@ -0,0 +1,70 @@ +type Price = { + currencyCode: string; + text: string; + value: number; +}; + +export type LastOrder = { + date: string; + expirationDate: string; + orderId: number; + password: string; + pdfUrl: string; + priceWithTax: Price; + priceWithoutTax: Price; + retractionDate: string | null; + tax: Price; + url: string; +}; + +export type OrderResponseData = { + data: LastOrder; + status: string; +}; + +export type OrderStatus = + | 'cancelled' + | 'cancelling' + | 'checking' + | 'delivered' + | 'delivering' + | 'documentsRequested' + | 'notPaid' + | 'unknown'; + +export type OrderDetail = { + domain: string; + quantity: string; + cancelled: boolean; + unitPrice: Price; + detailType: string; + totalPrice: Price; + description: string; + orderDetailId: number; +}; + +export type OrderDetailsResponse = OrderDetail[]; + +export type HistoryEntry = { + date: string; + label: string; + description: string; +}; + +export type FollowUpStep = { + history: HistoryEntry[]; + status: string; + step: string; +}; + +export type OrderFollowUpResponse = FollowUpStep[]; + +export type LastOrderTrackingResponse = LastOrder & { + history: OrderHistory[]; + status: string; +}; + +export type OrderHistory = { + date: string; + label: string; +}; diff --git a/packages/manager/apps/hub-react/src/types/services.type.ts b/packages/manager/apps/hub-react/src/types/services.type.ts new file mode 100644 index 000000000000..ee44f1002112 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/services.type.ts @@ -0,0 +1,42 @@ +import { ApiAggregateEnvelope, ApiEnvelope } from '@/types/apiEnvelope.type'; + +type Service = { + propertyId: string; + resource: { + displayName: string; + name: string; + product?: { + description: string; + name: string; + }; + resellingProvider: 'ovh.ca' | 'ovh.eu' | null; + state: string; + }; + route: { + path: string; + }; + serviceId: number; + serviceType?: string; + universe: Record; + url: string; +}; + +type ServiceList = ApiAggregateEnvelope; + +type Products = Record; + +export type ProductList = ApiAggregateEnvelope; + +type ServicesData = { + services: ApiEnvelope; +}; + +export type ServicesEnvelope = ApiEnvelope; + +export type HubProduct = { + data: Service[]; + count: number; + type: string; + formattedType: string; + link: Promise | null; +}; diff --git a/packages/manager/apps/hub-react/src/types/support.type.ts b/packages/manager/apps/hub-react/src/types/support.type.ts new file mode 100644 index 000000000000..ffa579aaee92 --- /dev/null +++ b/packages/manager/apps/hub-react/src/types/support.type.ts @@ -0,0 +1,17 @@ +import { ApiEnvelope } from '@/types/apiEnvelope.type'; + +export type Ticket = { + serviceName: string; + state: string; + subject: string; + ticketId: string; +}; + +export type SupportDataResponse = { + count: number; + data: Ticket[]; +}; + +export type SupportResponse = { + support: ApiEnvelope; +}; diff --git a/packages/manager/apps/hub-react/src/vite-hmr.ts b/packages/manager/apps/hub-react/src/vite-hmr.ts new file mode 100644 index 000000000000..473d87630039 --- /dev/null +++ b/packages/manager/apps/hub-react/src/vite-hmr.ts @@ -0,0 +1,5 @@ +if (import.meta.hot) { + import.meta.hot.on('iframe-reload', () => { + window.location.reload(); + }); +} diff --git a/packages/manager/apps/hub-react/tailwind.config.js b/packages/manager/apps/hub-react/tailwind.config.js new file mode 100644 index 000000000000..657ab11bb87d --- /dev/null +++ b/packages/manager/apps/hub-react/tailwind.config.js @@ -0,0 +1,14 @@ +import path from 'path'; +import config from '@ovh-ux/manager-tailwind-config'; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + ...config, + content: [ + './src/**/*.{js,jsx,ts,tsx}', + path.join( + path.dirname(require.resolve('@ovh-ux/manager-react-components')), + '**/*.{js,jsx,ts,tsx}', + ), + ], +}; diff --git a/packages/manager/apps/hub-react/tsconfig.json b/packages/manager/apps/hub-react/tsconfig.json new file mode 100644 index 000000000000..8ff43fa18040 --- /dev/null +++ b/packages/manager/apps/hub-react/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "es2020"], + "noEmit": true, + "target": "es2020", + "types": ["vite/client", "node"], + "module": "ES2020", + "moduleResolution": "node", + "removeComments": true, + "outDir": "dist", + "esModuleInterop": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "noImplicitAny": true, + "declaration": true, + "resolveJsonModule": true, + "allowJs": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "types", "src/__tests__"] +} diff --git a/packages/manager/apps/hub-react/tsconfig.test.json b/packages/manager/apps/hub-react/tsconfig.test.json new file mode 100644 index 000000000000..7048c297c8f6 --- /dev/null +++ b/packages/manager/apps/hub-react/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/packages/manager/apps/hub-react/vite.config.mjs b/packages/manager/apps/hub-react/vite.config.mjs new file mode 100644 index 000000000000..f33ab6dc98cd --- /dev/null +++ b/packages/manager/apps/hub-react/vite.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import { getBaseConfig } from '@ovh-ux/manager-vite-config'; +import { resolve } from 'path'; + +export default defineConfig({ + ...getBaseConfig(), + root: resolve(process.cwd()), +}); diff --git a/packages/manager/apps/hub-react/vitest.config.js b/packages/manager/apps/hub-react/vitest.config.js new file mode 100644 index 000000000000..b564e12d8618 --- /dev/null +++ b/packages/manager/apps/hub-react/vitest.config.js @@ -0,0 +1,30 @@ +import path from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + coverage: { + include: ['src'], + exclude: [ + 'src/types', + 'src/vite-*.ts', + 'src/App.tsx', + 'src/i18n.ts', + 'src/index.tsx', + 'src/routes.tsx', + ], + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + mainFields: ['module'], + }, +}); diff --git a/packages/manager/apps/hub/src/translations/Messages_pt_PT.json b/packages/manager/apps/hub/src/translations/Messages_pt_PT.json index f15d9a0db6b2..c339f84c1e7e 100644 --- a/packages/manager/apps/hub/src/translations/Messages_pt_PT.json +++ b/packages/manager/apps/hub/src/translations/Messages_pt_PT.json @@ -1,4 +1,4 @@ { "manager_hub_notification_warning": "{{ content }}", "manager_hub_skip_to_main_content": "Aceder ao conteúdo principal" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 45fbf0febeb2..a8c7dc1778c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9150,6 +9150,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/punycode@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.4.tgz#96f8a47f1ee9fb0d0def5557fe80fac532f966fa" + integrity sha512-trzh6NzBnq8yw5e35f8xe8VTYjqM3NE7bohBtvDVf/dtUer3zYTLK1Ka3DG3p7bdtoaOHZucma6FfVKlQ134pQ== + "@types/qs@*", "@types/qs@^6.9.5": version "6.9.14" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b" @@ -23967,7 +23972,7 @@ punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== -punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==