From 36666af99c778ff6d664cfabffff93745d539473 Mon Sep 17 00:00:00 2001 From: maelthi Date: Tue, 21 Jul 2026 10:32:27 +0200 Subject: [PATCH] feat(backup-licences): initialize micro apps for hpc (#1211) ref: #BKP-1211 Signed-off-by: maelthi Co-authored-by: CDS Translator Agent --- .eslintignore | 1 + .prettierignore | 1 + .stylelintignore | 1 + .../src/playbook/catalog/pnpm-catalog.json | 5 +- .../apps/hpc-backup-licenses/README.md | 19 +++ .../hpc-backup-licenses/eslint.config.mjs | 41 +++++ .../apps/hpc-backup-licenses/index.html | 22 +++ .../apps/hpc-backup-licenses/package.json | 68 +++++++++ .../hpc-backup-licenses/postcss.config.js | 6 + .../onboarding/Messages_de_DE.json | 25 ++++ .../onboarding/Messages_en_GB.json | 25 ++++ .../onboarding/Messages_es_ES.json | 25 ++++ .../onboarding/Messages_fr_CA.json | 25 ++++ .../onboarding/Messages_fr_FR.json | 25 ++++ .../onboarding/Messages_it_IT.json | 25 ++++ .../onboarding/Messages_pl_PL.json | 25 ++++ .../onboarding/Messages_pt_PT.json | 25 ++++ .../hpc-backup-licenses/src/App.constants.ts | 22 +++ .../apps/hpc-backup-licenses/src/App.tsx | 44 ++++++ .../hpc-backup-licenses/src/QueryClient.ts | 11 ++ .../src/Tracking.constants.ts | 12 ++ .../src/assets/error-banner-oops.png | Bin 0 -> 60234 bytes .../src/components/.gitkeep | 0 .../hpc-backup-licenses/src/data/.gitkeep | 0 .../onboarding/useOnboardingData.spec.tsx | 87 +++++++++++ .../hooks/onboarding/useOnboardingData.tsx | 64 ++++++++ .../apps/hpc-backup-licenses/src/i18n.ts | 27 ++++ .../apps/hpc-backup-licenses/src/index.scss | 8 + .../apps/hpc-backup-licenses/src/index.tsx | 62 ++++++++ .../src/pages/Main.layout.tsx | 42 ++++++ .../src/pages/not-found/404.page.tsx | 7 + .../src/pages/onboarding/Onboarding.page.tsx | 83 +++++++++++ .../onboarding/__tests__/Onboarding.spec.tsx | 140 ++++++++++++++++++ .../src/routes/Routes.constants.ts | 14 ++ .../hpc-backup-licenses/src/routes/Routes.tsx | 40 +++++ .../src/types/Onboarding.type.ts | 29 ++++ .../src/utils/Test.utils.tsx | 29 ++++ .../src/utils/tests/TestApp.tsx | 19 +++ .../src/utils/tests/i18ntest.utils.ts | 45 ++++++ .../src/utils/tests/setupMsw.ts | 1 + .../src/utils/tests/testWrapperBuilder.tsx | 53 +++++++ .../src/utils/tests/testWrapperProviders.tsx | 55 +++++++ .../apps/hpc-backup-licenses/src/vite-hmr.ts | 5 + .../hpc-backup-licenses/tailwind.config.mjs | 25 ++++ .../apps/hpc-backup-licenses/tsconfig.json | 14 ++ .../apps/hpc-backup-licenses/vite.config.mjs | 9 ++ .../apps/hpc-backup-licenses/vitest.config.js | 43 ++++++ .../src/test-utils/uiTestHelpers.tsx | 13 +- .../manager/apps/network-vrack/package.json | 2 + .../manager/apps/pci-workflow/package.json | 1 + .../manager/apps/web-domains/package.json | 4 +- .../apps/web-ongoing-operations/package.json | 4 +- yarn.lock | 12 +- 53 files changed, 1373 insertions(+), 17 deletions(-) create mode 100644 packages/manager/apps/hpc-backup-licenses/README.md create mode 100644 packages/manager/apps/hpc-backup-licenses/eslint.config.mjs create mode 100644 packages/manager/apps/hpc-backup-licenses/index.html create mode 100644 packages/manager/apps/hpc-backup-licenses/package.json create mode 100644 packages/manager/apps/hpc-backup-licenses/postcss.config.js create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json create mode 100644 packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json create mode 100644 packages/manager/apps/hpc-backup-licenses/src/App.constants.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/App.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png create mode 100644 packages/manager/apps/hpc-backup-licenses/src/components/.gitkeep create mode 100644 packages/manager/apps/hpc-backup-licenses/src/data/.gitkeep create mode 100644 packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/i18n.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/index.scss create mode 100644 packages/manager/apps/hpc-backup-licenses/src/index.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx create mode 100644 packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts create mode 100644 packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs create mode 100644 packages/manager/apps/hpc-backup-licenses/tsconfig.json create mode 100644 packages/manager/apps/hpc-backup-licenses/vite.config.mjs create mode 100644 packages/manager/apps/hpc-backup-licenses/vitest.config.js diff --git a/.eslintignore b/.eslintignore index 55d3639e01d9..2ec565fad6f1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -74,3 +74,4 @@ packages/manager/apps/pci-project packages/manager/apps/hub packages/manager/apps/pci-file-storage packages/manager/apps/network-vrack +packages/manager/apps/hpc-backup-licenses diff --git a/.prettierignore b/.prettierignore index 4708621fb211..b41f0afc4376 100644 --- a/.prettierignore +++ b/.prettierignore @@ -36,3 +36,4 @@ packages/manager/apps/pci-project packages/manager/apps/hub packages/manager/apps/pci-file-storage packages/manager/apps/network-vrack +packages/manager/apps/hpc-backup-licenses diff --git a/.stylelintignore b/.stylelintignore index b8b98166a193..bbc8fcd4e433 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -18,3 +18,4 @@ packages/manager/apps/zimbra packages/manager/apps/hub packages/manager/apps/pci-file-storage packages/manager/apps/network-vrack +packages/manager/apps/hpc-backup-licenses diff --git a/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json b/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json index 4599e6cabcb2..438320684c17 100644 --- a/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json +++ b/packages/manager-tools/manager-pm/src/playbook/catalog/pnpm-catalog.json @@ -68,5 +68,6 @@ "packages/manager/apps/web-domains", "packages/manager/core/shell-client", "packages/manager/modules/logs-to-customer", - "packages/manager/modules/metrics-to-customer" -] + "packages/manager/modules/metrics-to-customer", + "packages/manager/apps/hpc-backup-licenses" +] \ No newline at end of file diff --git a/packages/manager/apps/hpc-backup-licenses/README.md b/packages/manager/apps/hpc-backup-licenses/README.md new file mode 100644 index 000000000000..1cbef84fe8b0 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/README.md @@ -0,0 +1,19 @@ +# hpc-backup-licenses — OVHcloud Manager Application + +## Overview + +The hpc-backup-licenses app lets customers choose and link a Veeam licence to a VBR server. Customers can manage multiple VBRs, each with multiple vaults to store their backups. + +--- + +## 📚 Useful Links + +- Forge CLI: https://github.com/ovh/manager/tree/master/packages/manager-tools/manager-forge-cli#readme +- Manager React Shell Client: https://github.com/ovh/manager +- React Router Docs: https://reactrouter.com/ +- Iceberg API Guide: https://github.com/ovh/manager-core-api +- OVHcloud Public API Explorer: https://api.ovh.com/ + +--- + +**Generated with ❤️ by OVHcloud Forge CLI** diff --git a/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs b/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs new file mode 100644 index 000000000000..77c69c2828b1 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/eslint.config.mjs @@ -0,0 +1,41 @@ +import { a11yEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/a11y'; +import { + complexityJsxTsxConfig, + complexityTsJsConfig, +} from '@ovh-ux/manager-static-analysis-kit/eslint/complexity'; +import { htmlEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/html'; +import { javascriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/javascript'; +import { prettierEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/prettier'; +import { reactEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/react'; +import { tailwindJsxConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tailwind-jsx'; +import { tanStackQueryEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tanstack'; +import { vitestEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tests'; +import { typescriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/typescript'; + +export default [ + javascriptEslintConfig, + typescriptEslintConfig, + reactEslintConfig, + a11yEslintConfig, + htmlEslintConfig, + tailwindJsxConfig, + tanStackQueryEslintConfig, + vitestEslintConfig, + prettierEslintConfig, + complexityJsxTsxConfig, + complexityTsJsConfig, + { + files: ['**/__tests__/**/*.{js,jsx,ts,tsx}'], + rules: { + ...Object.fromEntries( + Object.keys({ ...complexityJsxTsxConfig.rules, ...complexityTsJsConfig.rules }).map( + (rule) => [rule, 'off'], + ), + ), + 'react/no-multi-comp': 'off', + }, + }, + { + ignores: ['**/*.md', '**/coverage/**', '**/dist/**', '**/*.d.ts'], + }, +]; diff --git a/packages/manager/apps/hpc-backup-licenses/index.html b/packages/manager/apps/hpc-backup-licenses/index.html new file mode 100644 index 000000000000..ec4e92d05007 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/index.html @@ -0,0 +1,22 @@ + + + + + + + + OVHcloud + + + + + +
+ + + diff --git a/packages/manager/apps/hpc-backup-licenses/package.json b/packages/manager/apps/hpc-backup-licenses/package.json new file mode 100644 index 000000000000..e861cd089b07 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/package.json @@ -0,0 +1,68 @@ +{ + "name": "@ovh-ux/manager-hpc-backup-licenses-app", + "version": "0.0.0", + "private": true, + "description": "The hpc-backup-licenses app lets customers choose and link a Veeam licence to a VBR server. Customers can manage multiple VBRs, each with multiple vaults to store their backups.", + "repository": { + "type": "git", + "url": "git+https://github.com/ovh/manager.git", + "directory": "packages/manager/apps/hpc-backup-licenses" + }, + "license": "BSD-3-Clause", + "author": "OVH SAS", + "scripts": { + "build": "tsc && vite build", + "lint": "manager-lint --config eslint.config.mjs ./src", + "lint:fix": "manager-lint --fix --config eslint.config.mjs ./src", + "start": "vite", + "test": "manager-test run", + "test:coverage": "manager-test run --coverage" + }, + "dependencies": { + "@hookform/resolvers": "5.2.1", + "@ovh-ux/manager-common-translations": "*", + "@ovh-ux/manager-config": "*", + "@ovh-ux/manager-core-api": "*", + "@ovh-ux/manager-core-utils": "*", + "@ovh-ux/manager-react-components": "2.42.4", + "@ovh-ux/manager-react-core-application": "*", + "@ovh-ux/manager-react-shell-client": "*", + "@ovh-ux/request-tagger": "*", + "@ovh-ux/shell": "^4.5.7", + "@ovhcloud/ods-components": "~18.6.2", + "@ovhcloud/ods-themes": "~18.6.2", + "@tanstack/react-query": "5.51.21", + "axios": "^1.13.2", + "element-internals-polyfill": "^3.0.0", + "i18next": "23.8.2", + "i18next-http-backend": "2.4.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "7.54.2", + "react-i18next": "14.1.0", + "react-router-dom": "6.16.0", + "tailwindcss": "^4.1.17", + "zod": "4.1.12" + }, + "devDependencies": { + "@ovh-ux/manager-static-analysis-kit": "0.12.0", + "@ovh-ux/manager-tailwind-config": "*", + "@ovh-ux/manager-tests-setup": "^0.6.1", + "@ovh-ux/manager-vite-config": "*", + "@tailwindcss/postcss": "^4.1.17", + "@tanstack/react-query-devtools": "5.51.21", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.3.0", + "msw": "2.12.1", + "typescript": "5.9.3", + "vite": "7.2.2", + "vitest": "3.2.4" + }, + "regions": [ + "CA", + "EU", + "US" + ], + "universes": [] +} \ No newline at end of file diff --git a/packages/manager/apps/hpc-backup-licenses/postcss.config.js b/packages/manager/apps/hpc-backup-licenses/postcss.config.js new file mode 100644 index 000000000000..b03b828ab3b1 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/postcss.config.js @@ -0,0 +1,6 @@ +import tailwindcss from '@tailwindcss/postcss'; +import autoprefixer from 'autoprefixer'; + +export default { + plugins: [tailwindcss(), autoprefixer()], +}; diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json new file mode 100644 index 000000000000..89722e847929 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_de_DE.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Willkommen bei {{productName}}", + "description_part1": "Entdecken Sie, wie Sie {{productName}} optimal nutzen können.", + "description_part2": "Befolgen Sie unsere Guides für einen schnellen Einstieg.", + "guides": { + "guide1": { + "category": "Entdecken", + "title": "Erste Schritte mit {{productName}}", + "description": "Lernen Sie die Grundlagen für den Einstieg.", + "cta": "Jetzt entdecken" + }, + "guide2": { + "category": "Tutorials", + "title": "Umgebung konfigurieren", + "description": "Folgen Sie unserem Guide Schritt für Schritt.", + "cta": "Zum Tutorial" + }, + "guide3": { + "category": "FAQ", + "title": "Häufig gestellte Fragen", + "description": "Finden Sie Antworten auf häufig gestellte Fragen.", + "cta": "FAQs lesen" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json new file mode 100644 index 000000000000..b5495cfce41b --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_en_GB.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Welcome to {{productName}}", + "description_part1": "Find out how to get the most out of {{productName}}.", + "description_part2": "Follow our guides to get started quickly.", + "guides": { + "guide1": { + "category": "Discovery", + "title": "Get started with {{productName}}", + "description": "Learn the basics to get started.", + "cta": "Get started" + }, + "guide2": { + "category": "Tutorials", + "title": "Configure your environment", + "description": "Follow our step-by-step guide.", + "cta": "See tutorial" + }, + "guide3": { + "category": "FAQs", + "title": "Frequently asked questions", + "description": "Find answers to frequently asked questions.", + "cta": "Read the FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json new file mode 100644 index 000000000000..ad45f9fcc201 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_es_ES.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Bienvenido/a a {{productName}}", + "description_part1": "Descubra cómo sacar el máximo partido de {{productName}}.", + "description_part2": "Siga nuestras guías para empezar rápidamente.", + "guides": { + "guide1": { + "category": "Discovery", + "title": "Empezar con {{productName}}", + "description": "Descubra los aspectos clave para empezar.", + "cta": "Descubrir" + }, + "guide2": { + "category": "Guías", + "title": "Configurar su entorno", + "description": "Siga nuestra guía paso a paso.", + "cta": "Ver el tutorial" + }, + "guide3": { + "category": "FAQ", + "title": "Preguntas frecuentes", + "description": "Encuentre la respuesta a sus preguntas.", + "cta": "Leer las FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json new file mode 100644 index 000000000000..b0b931acb675 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_CA.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Bienvenue sur {{productName}}", + "description_part1": "Découvrez comment tirer le meilleur parti de {{productName}}.", + "description_part2": "Suivez nos guides pour démarrer rapidement.", + "guides": { + "guide1": { + "category": "Découverte", + "title": "Commencer avec {{productName}}", + "description": "Apprenez les bases pour démarrer.", + "cta": "Découvrir" + }, + "guide2": { + "category": "Tutoriels", + "title": "Configurer votre environnement", + "description": "Suivez notre guide pas à pas.", + "cta": "Voir le tutoriel" + }, + "guide3": { + "category": "FAQ", + "title": "Questions fréquentes", + "description": "Trouvez les réponses aux questions courantes.", + "cta": "Lire la FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json new file mode 100644 index 000000000000..048e6d532c02 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_fr_FR.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Bienvenue sur {{productName}}.", + "description_part1": "Découvrez comment tirer le meilleur parti de {{productName}}.", + "description_part2": "Suivez nos guides pour démarrer rapidement.", + "guides": { + "guide1": { + "category": "Découverte", + "title": "Commencer avec {{productName}}", + "description": "Apprenez les bases pour démarrer.", + "cta": "Découvrir" + }, + "guide2": { + "category": "Tutoriels", + "title": "Configurer votre environnement", + "description": "Suivez notre guide pas à pas.", + "cta": "Voir le tutoriel" + }, + "guide3": { + "category": "FAQ", + "title": "Questions fréquentes", + "description": "Trouvez les réponses aux questions courantes.", + "cta": "Lire la FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json new file mode 100644 index 000000000000..990c4afbb3a3 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_it_IT.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Benvenuto su {{productName}}", + "description_part1": "Scopri come ottenere il massimo da {{productName}}.", + "description_part2": "Segui le nostre guide per iniziare rapidamente.", + "guides": { + "guide1": { + "category": "Discovery", + "title": "Iniziare a utilizzare {{productName}}", + "description": "Concetti base per iniziare.", + "cta": "Scopri" + }, + "guide2": { + "category": "Tutorial", + "title": "Configurare il tuo ambiente", + "description": "Segui la nostra guida passo per passo.", + "cta": "Guarda il tutorial" + }, + "guide3": { + "category": "FAQ", + "title": "Domande frequenti", + "description": "Trova le risposte alle domande più frequenti.", + "cta": "Leggi le FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json new file mode 100644 index 000000000000..05e52b47e7ec --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pl_PL.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Witamy w {{productName}}", + "description_part1": "Dowiedz się, jak najlepiej wykorzystać {{productName}}.", + "description_part2": "Zapoznaj się z naszymi przewodnikami, aby szybko wystartować.", + "guides": { + "guide1": { + "category": "Pierwsze kroki", + "title": "Rozpocznij pracę z {{productName}}", + "description": "Poznaj podstawy i przejdź do działania.", + "cta": "Sprawdź" + }, + "guide2": { + "category": "Tutoriale", + "title": "Konfiguracja środowiska", + "description": "Postępuj zgodnie z instrukcjami z naszego przewodnika", + "cta": "Sprawdź tutorial" + }, + "guide3": { + "category": "FAQ", + "title": "Najczęściej zadawane pytania", + "description": "Znajdź odpowiedzi na często zadawane pytania.", + "cta": "Przeczytaj FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json new file mode 100644 index 000000000000..823ef421f8b8 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/public/translations/onboarding/Messages_pt_PT.json @@ -0,0 +1,25 @@ +{ + "title_fallback": "Bem-vindo ao {{productName}}", + "description_part1": "Saiba como tirar o máximo partido da {{productName}}.", + "description_part2": "Siga os nossos guias para começar rapidamente.", + "guides": { + "guide1": { + "category": "Descoberta", + "title": "Começar com {{productName}}", + "description": "Aprenda o básico para começar.", + "cta": "Descobrir" + }, + "guide2": { + "category": "Tutoriais", + "title": "Configurar o seu ambiente", + "description": "Siga o nosso guia passo a passo.", + "cta": "Consultar o tutorial" + }, + "guide3": { + "category": "FAQ", + "title": "Perguntas frequentes", + "description": "Encontre as respostas para as perguntas mais habituais.", + "cta": "Ler a FAQ" + } + } +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts new file mode 100644 index 000000000000..9fabfa0244d0 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/App.constants.ts @@ -0,0 +1,22 @@ +import { OnboardingConfigType } from '@/types/Onboarding.type'; + +export const appName = 'hpc-backup-licenses'; +export const productName = 'Veeam Backup Licenses'; + +const docUrl = 'https://docs.ovh.com'; + +export const ONBOARDING_CONFIG: OnboardingConfigType = { + productName, + productCategory: 'Backup', + brand: 'OVHcloud', + tiles: [ + { id: 1, key: 'guide1', linkKey: 'discover' }, + { id: 2, key: 'guide2', linkKey: 'tutorial' }, + { id: 3, key: 'guide3', linkKey: 'faq' }, + ], + links: { + discover: docUrl, + tutorial: docUrl, + faq: docUrl, + }, +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/App.tsx b/packages/manager/apps/hpc-backup-licenses/src/App.tsx new file mode 100644 index 000000000000..4de4224775ba --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/App.tsx @@ -0,0 +1,44 @@ +import React, { Suspense, useContext, useEffect } from 'react'; + +import { RouterProvider, createHashRouter, createRoutesFromElements } from 'react-router-dom'; + +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; + +import Routes from '@/routes/Routes'; + +import queryClient from './QueryClient'; + +function App() { + const { shell } = useContext(ShellContext); + + const router = createHashRouter(createRoutesFromElements(Routes)); + + useEffect(() => { + const hidePreloader = async () => { + try { + await shell?.ux?.hidePreloader?.(); + } catch (err) { + console.error('Failed to hide preloader:', err); + } + }; + + void hidePreloader(); + }, [shell]); + + return ( + + {/* Suspense ensures lazy-loaded route components show a fallback UI */} + Loading routes ...}> + + + + {/* React Query Devtools are included for development and debugging */} + + + ); +} + +export default App; diff --git a/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts b/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts new file mode 100644 index 000000000000..94100b5328ad --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/QueryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 300_000, + }, + }, +}); + +export default queryClient; diff --git a/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts new file mode 100644 index 000000000000..8e2290cdafca --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/Tracking.constants.ts @@ -0,0 +1,12 @@ +import { appName } from '@/App.constants'; + +export const LEVEL2 = { + EU: { config: { level2: '120' } }, + CA: { config: { level2: '120' } }, + US: { config: { level2: '120' } }, +} as const; + +export const UNIVERSE = 'HostedPrivatedCloud' as const; +export const SUB_UNIVERSE = 'HostedPrivatedCloud' as const; + +export const APP_NAME = appName; diff --git a/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png b/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png new file mode 100644 index 0000000000000000000000000000000000000000..413028afad19a473424e11656d3c6b0400a79fdf GIT binary patch literal 60234 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 literal 0 HcmV?d00001 diff --git a/packages/manager/apps/hpc-backup-licenses/src/components/.gitkeep b/packages/manager/apps/hpc-backup-licenses/src/components/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/manager/apps/hpc-backup-licenses/src/data/.gitkeep b/packages/manager/apps/hpc-backup-licenses/src/data/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx new file mode 100644 index 000000000000..743df65f71b5 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.spec.tsx @@ -0,0 +1,87 @@ +import type { ReactNode } from 'react'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { OnboardingConfigType } from '@/types/Onboarding.type'; + +import { useGuideLinks, useOnboardingContent } from './useOnboardingData'; + +// --- Mock App.constants --- +vi.mock('@/App.constants', () => { + const mockConfig: OnboardingConfigType = { + productName: 'MyProduct', + productCategory: 'CategoryX', + brand: 'BrandY', + title: 'Welcome to MyProduct', + heroImage: { src: '/hero.png', alt: 'Hero' }, + tiles: [{ id: 1, key: 'discover', linkKey: 'discover' }], + links: { discover: '/discover', tutorial: '/tutorial', faq: '/faq' }, + }; + return { ONBOARDING_CONFIG: mockConfig }; +}); + +// --- Mock translation --- +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, opts?: { productName?: string }) => + `${key}${opts?.productName ? `:${opts.productName}` : ''}`, + }), +})); + +// --- Helper wrapper with React Query --- +const createWrapper = + () => + // eslint-disable-next-line react/display-name + ({ children }: { children: ReactNode }) => { + const client = new QueryClient(); + return {children}; + }; + +describe('useOnboardingContent', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns onboarding content with hero image and tiles', async () => { + const { result } = renderHook(() => useOnboardingContent(), { wrapper: createWrapper() }); + + await waitFor(() => { + expect(result.current.productName).toBe('MyProduct'); + expect(result.current.productCategory).toBe('CategoryX'); + expect(result.current.brand).toBe('BrandY'); + expect(result.current.heroImage?.src).toBe('/hero.png'); + expect(result.current.tiles).toHaveLength(1); + }); + }); + + it('falls back when productName is missing', async () => { + vi.doMock('@/App.constants', () => ({ + ONBOARDING_CONFIG: { + productCategory: 'X', + brand: 'Y', + tiles: [], + links: {}, + } as unknown as Partial, + })); + + const { result } = renderHook(() => useOnboardingContent(), { wrapper: createWrapper() }); + + await waitFor(() => { + expect(result.current.productName).toContain('onboarding:productDefaultName'); + }); + }); +}); + +describe('useGuideLinks', () => { + it('returns guide links correctly', async () => { + const { result } = renderHook(() => useGuideLinks(), { wrapper: createWrapper() }); + + await waitFor(() => { + expect(result.current.discover).toBe('/discover'); + expect(result.current.tutorial).toBe('/tutorial'); + expect(result.current.faq).toBe('/faq'); + }); + }); +}); diff --git a/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx new file mode 100644 index 000000000000..6d37355792ae --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/hooks/onboarding/useOnboardingData.tsx @@ -0,0 +1,64 @@ +import { useMemo } from 'react'; + +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import * as AppConfig from '@/App.constants'; +import type { + OnboardingConfigType, + OnboardingContentType, + OnboardingImage, + OnboardingLinksType, +} from '@/types/Onboarding.type'; + +const QUERY_KEY = ['onboarding-config'] as const; + +async function getOnboardingConfig(): Promise { + return new Promise((resolve) => { + resolve(AppConfig.ONBOARDING_CONFIG); + }); +} + +export function useOnboardingContent(): OnboardingContentType { + const { t } = useTranslation(['onboarding', 'common']); + const { data } = useQuery({ + queryKey: QUERY_KEY, + queryFn: () => getOnboardingConfig(), + }); + + return useMemo(() => { + const productName = data?.productName ?? t('onboarding:productDefaultName', 'Product'); + + const heroImage: OnboardingImage | undefined = data?.heroImage?.src + ? { + src: data.heroImage.src, + alt: data.heroImage.alt ?? t('onboarding:heroAlt', { productName }), + } + : undefined; + + return { + productName, + productCategory: data?.productCategory, + brand: data?.brand, + title: data?.title, + heroImage, + tiles: Array.isArray(data?.tiles) ? data.tiles : [], + }; + }, [data, t]); +} + +export function useGuideLinks(): OnboardingLinksType { + const { data } = useQuery({ + queryKey: QUERY_KEY, + queryFn: () => getOnboardingConfig(), + }); + + return useMemo( + () => ({ + discover: data?.links?.discover ?? '', + tutorial: data?.links?.tutorial ?? '', + faq: data?.links?.faq ?? '', + }), + [data], + ); +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/i18n.ts b/packages/manager/apps/hpc-backup-licenses/src/i18n.ts new file mode 100644 index 000000000000..3dfc6a85f8fe --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/i18n.ts @@ -0,0 +1,27 @@ +import i18n from 'i18next'; +import I18NextHttpBackend from 'i18next-http-backend'; +import { initReactI18next } from 'react-i18next'; + +export default async function initI18n(locale = 'fr_FR', availablesLocales = ['fr_FR']) { + await i18n + .use(initReactI18next) + .use(I18NextHttpBackend) + .use({ + type: 'postProcessor', + name: 'normalize', + process: (value: string) => (value ? value.replace(/&/g, '&') : value), + }) + .init({ + lng: locale, + fallbackLng: 'fr_FR', + supportedLngs: availablesLocales, + defaultNS: 'common', + ns: ['common'], // namespaces to load by default + backend: { + loadPath: (lngs: string[], namespaces: string[]) => + `${import.meta.env.BASE_URL}translations/${namespaces[0]}/Messages_${lngs[0]}.json`, + }, + postProcess: 'normalize', + }); + return i18n; +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/index.scss b/packages/manager/apps/hpc-backup-licenses/src/index.scss new file mode 100644 index 000000000000..a5c684d9dcdd --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/index.scss @@ -0,0 +1,8 @@ +@tailwind utilities; + +@import '@ovhcloud/ods-themes/default'; +@import '@ovh-ux/manager-react-components/dist/style.css'; + +html { + font-family: var(--ods-font-family-default); +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/index.tsx b/packages/manager/apps/hpc-backup-licenses/src/index.tsx new file mode 100644 index 000000000000..1a4b0f0b63d3 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/index.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import ReactDOM from 'react-dom/client'; + +import { ShellContext, initI18n, initShellContext } from '@ovh-ux/manager-react-shell-client'; + +import { appName } from '@/App.constants'; + +import App from './App'; +import { APP_NAME, LEVEL2, SUB_UNIVERSE, UNIVERSE } from './Tracking.constants'; +import './index.scss'; +import './vite-hmr'; + +const trackingContext = { + chapter1: UNIVERSE, + chapter2: SUB_UNIVERSE, + chapter3: APP_NAME, + appName: APP_NAME, + pageTheme: UNIVERSE, + level2Config: LEVEL2, +}; + +const init = async (appName: string) => { + // Initialize shell context: auth, env, tracking, APIs + const context = await initShellContext(appName, trackingContext); + + // Initialize i18n with default namespaces + await initI18n({ + context, + reloadOnLocaleChange: true, + defaultNS: appName, + ns: ['listing', 'dashboard', 'onboarding'], + }); + + // Configure tracking per region + const region = context.environment.getRegion(); + context.shell.tracking.setConfig(region, LEVEL2); + + // Try loading region-specific runtime config (optional) + try { + await import(`./config-${region}.js`); + } catch { + // No-op when region config is absent + } + + // Mount React app + const rootElement = document.getElementById('root'); + if (!rootElement) { + throw new Error('Root element #root not found'); + } + + ReactDOM.createRoot(rootElement).render( + + + + + , + ); +}; + +// Start the app +void init(appName); diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx new file mode 100644 index 000000000000..6014bcdf9ffd --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/pages/Main.layout.tsx @@ -0,0 +1,42 @@ +import { Suspense, useContext, useEffect } from 'react'; + +import { Outlet, useLocation, useMatches } from 'react-router-dom'; + +import { ShellContext, useOvhTracking, useRouteSynchro } from '@ovh-ux/manager-react-shell-client'; +import { defineCurrentPage } from '@ovh-ux/request-tagger'; + +import { appName } from '@/App.constants'; + +export default function MainLayout() { + const location = useLocation(); + const matches = useMatches(); + const { shell } = useContext(ShellContext); + const { trackCurrentPage } = useOvhTracking(); + + // Keep shell route state in sync with React Router + useRouteSynchro(); + + // Tag the current page for request correlation + useEffect(() => { + const lastMatch = matches.at(-1); // most specific route + if (lastMatch?.id) { + defineCurrentPage(`app.${appName}-${lastMatch.id}`); + } + }, [matches]); + + // Track page views on route change + useEffect(() => { + trackCurrentPage(); + }, [trackCurrentPage, location.pathname]); + + // Hide shell preloader on mount + useEffect(() => { + void shell?.ux.hidePreloader(); + }, [shell]); + + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx new file mode 100644 index 000000000000..d052f1ebcbbf --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/pages/not-found/404.page.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function NotFound() { + // @TODO: add a redirection here in order to catch /:serviceName given from iframe + + return

404 - route not found

; +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx new file mode 100644 index 000000000000..9fba1e655118 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/Onboarding.page.tsx @@ -0,0 +1,83 @@ +import React, { useMemo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +import { OdsText } from '@ovhcloud/ods-components/react'; + +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components'; + +import { useGuideLinks, useOnboardingContent } from '@/hooks/onboarding/useOnboardingData'; +import { OnboardingLinksType } from '@/types/Onboarding.type'; + +export default function OnboardingPage() { + const { t } = useTranslation(['onboarding', NAMESPACES.ACTIONS, NAMESPACES.ONBOARDING]); + const { productName, productCategory, brand, title, heroImage, tiles } = useOnboardingContent(); + const links: OnboardingLinksType = useGuideLinks(); + + // Build localized description paragraph. + const description = useMemo( + () => ( +
+ + {t('onboarding:description_part1', { + productName, + productCategory, + brand, + })} + + {t('onboarding:description_part2', { productName })} +
+ ), + [t, productName, productCategory, brand], + ); + + // Build hero image object with fallback alt text. + const img = useMemo( + () => + heroImage + ? { + src: heroImage.src, + alt: heroImage.alt ?? t('onboarding:hero_alt', { productName }), + } + : undefined, + [heroImage, t, productName], + ); + + // Filter tiles to include only those with matching guide links. + const validTiles = useMemo( + () => tiles.filter(({ linkKey }) => Boolean(links[linkKey])), + [tiles, links], + ); + + return ( + {}} + moreInfoButtonLabel={t(`${NAMESPACES.ONBOARDING}:more_infos`)} + > + {validTiles.map(({ id, key, linkKey }) => { + const href = links[linkKey]; + if (!href) return null; + + return ( + + ); + })} + + ); +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx new file mode 100644 index 000000000000..d76df62ce498 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/pages/onboarding/__tests__/Onboarding.spec.tsx @@ -0,0 +1,140 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { CardProps, OnboardingLayoutProps } from '@ovh-ux/manager-react-components'; + +import type { OnboardingContentType, OnboardingLinksType } from '@/types/Onboarding.type'; + +import OnboardingPage from '../Onboarding.page'; + +type MockUseOnboardingDataModule = { + useOnboardingContent: () => OnboardingContentType; + useGuideLinks: () => OnboardingLinksType; +}; + +vi.mock('react-i18next', () => ({ + useTranslation: (): { + t: (key: string, vars?: Record) => string; + } => ({ + t: (key: string, vars?: Record) => `${key}${vars ? JSON.stringify(vars) : ''}`, + }), +})); + +vi.mock( + '@/hooks/onboarding/useOnboardingData', + (): MockUseOnboardingDataModule => ({ + useOnboardingContent: () => ({ + productName: 'Web Hosting', + productCategory: 'Hosting', + brand: 'OVHcloud', + title: 'Welcome to Web Hosting', + heroImage: { src: 'hero.jpg', alt: 'Hero Image' }, + tiles: [ + { id: 1, key: 'discover', linkKey: 'discover' }, + { id: 2, key: 'tutorial', linkKey: 'tutorial' }, + { id: 3, key: 'faq', linkKey: 'faq' }, + ], + }), + useGuideLinks: () => ({ + discover: 'https://guides.ovh.com/discover', + tutorial: 'https://guides.ovh.com/tutorial', + faq: 'https://guides.ovh.com/faq', + }), + }), +); + +vi.mock('@ovh-ux/manager-react-components', () => { + const OnboardingLayout = ({ + title, + img, + description, + children, + }: OnboardingLayoutProps): JSX.Element => ( +
+

{title}

+ {img && {img.alt}} +
{description}
+
{children}
+
+ ); + + const Card = ({ href, texts }: CardProps): JSX.Element => ( + +

{texts.title}

+

{texts.description}

+ {texts.category} +
+ ); + + return { OnboardingLayout, Card }; +}); + +describe('OnboardingPage', () => { + afterEach(() => { + vi.resetModules(); + }); + + it('renders layout with translated title, description, hero image and link cards', () => { + render(); + + // Layout + title + const layout = screen.getByTestId('layout'); + expect(layout).toBeInTheDocument(); + + const title = screen.getByRole('heading', { level: 1 }); + expect(title).toHaveTextContent('Welcome to Web Hosting'); + + // Hero image + const img = screen.getByTestId('hero-img'); + expect(img).toHaveAttribute('src', 'hero.jpg'); + expect(img).toHaveAttribute('alt', 'Hero Image'); + + // Description section + expect(screen.getByTestId('description')).toBeInTheDocument(); + + // Link cards + const cards = screen.getAllByTestId('link-card'); + expect(cards).toHaveLength(3); + expect(cards[0]).toHaveAttribute('href', 'https://guides.ovh.com/discover'); + expect(cards[1]).toHaveAttribute('href', 'https://guides.ovh.com/tutorial'); + expect(cards[2]).toHaveAttribute('href', 'https://guides.ovh.com/faq'); + }); + + it('renders fallback title and no hero image when data missing', async () => { + vi.resetModules(); // clear previous imports + + vi.doMock( + '@/hooks/onboarding/useOnboardingData', + (): MockUseOnboardingDataModule => ({ + useOnboardingContent: () => ({ + productName: 'Cloud', + productCategory: undefined, + brand: undefined, + title: undefined, + heroImage: undefined, + tiles: [], + }), + useGuideLinks: () => ({ + discover: '', + tutorial: '', + faq: '', + }), + }), + ); + + // 👇 re-import component after mocks are applied + const { default: OnboardingPageDynamic } = await import('../Onboarding.page'); + render(); + + const layout = screen.getByTestId('layout'); + expect(layout).toBeInTheDocument(); + + const title = screen.getByRole('heading', { level: 1 }); + expect(title).toHaveTextContent('onboarding:title_fallback{"productName":"Cloud"}'); + + expect(screen.queryByTestId('hero-img')).not.toBeInTheDocument(); + expect(screen.queryAllByTestId('link-card')).toHaveLength(0); + }); +}); diff --git a/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts new file mode 100644 index 000000000000..ab5780a00cc6 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.constants.ts @@ -0,0 +1,14 @@ +import { appName } from '@/App.constants'; + +const ROOT_URL = `/`; + +export const subRoutes = { + onboarding: 'onboarding' as const, +} as const; + +export const urls = { + root: ROOT_URL, + onboarding: `${ROOT_URL}${subRoutes.onboarding}`, +} as const; + +export const redirectionApp = appName; diff --git a/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx new file mode 100644 index 000000000000..b0bad7c57cec --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/routes/Routes.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { Navigate, Route } from 'react-router-dom'; + +import { ErrorBoundary } from '@ovh-ux/manager-react-components'; +import { PageType } from '@ovh-ux/manager-react-shell-client'; + +import NotFound from '@/pages/not-found/404.page'; + +import { redirectionApp, subRoutes, urls } from './Routes.constants'; + +const MainLayoutPage = React.lazy(() => import('@/pages/Main.layout')); +const OnboardingPage = React.lazy(() => import('@/pages/onboarding/Onboarding.page')); + +export default ( + <> + } /> + + } + > + + } /> + + +); diff --git a/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts b/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts new file mode 100644 index 000000000000..ca34ad80483e --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/types/Onboarding.type.ts @@ -0,0 +1,29 @@ +export type OnboardingLinksType = { + discover: string; + tutorial: string; + faq: string; +}; + +export type OnboardingTile = { + id: number; + key: string; + linkKey: keyof OnboardingLinksType; +}; + +export type OnboardingImage = { + src: string; + alt?: string; +}; + +export type OnboardingContentType = { + productName: string; + productCategory?: string; + brand?: string; + title?: string; + heroImage?: OnboardingImage; + tiles: OnboardingTile[]; +}; + +export type OnboardingConfigType = OnboardingContentType & { + links: OnboardingLinksType; +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx new file mode 100644 index 000000000000..e30c73f397f0 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/Test.utils.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { render } from '@testing-library/react'; + +import { urls } from '@/routes/Routes.constants'; +import { TestApp } from '@/utils/tests/TestApp'; +import { setupMswMock } from '@/utils/tests/setupMsw'; +import { testWrapperBuilder } from '@/utils/tests/testWrapperBuilder'; + +const formatSafePath = (url?: string) => + url && url.startsWith(urls.root) ? url : `${urls.root}${url ?? ''}`; + +export const renderTest = async ({ + initialRoute, +}: { + initialRoute?: string; +} = {}): Promise> => { + setupMswMock(); + + const wrapper = await testWrapperBuilder() + .withQueryClient() + .withI18next() + .withShellContext() + .build(); + + return render(, { + wrapper, + }) as unknown as ReturnType; +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx new file mode 100644 index 000000000000..848050a8e935 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/TestApp.tsx @@ -0,0 +1,19 @@ +import React, { Suspense } from 'react'; + +import { RouterProvider, createMemoryRouter, createRoutesFromElements } from 'react-router-dom'; + +import Routes from '@/routes/Routes'; + +export function TestApp({ initialRoute = '/' }: { initialRoute: string }) { + const routes = createRoutesFromElements(Routes); + const router = createMemoryRouter(routes, { + initialEntries: [initialRoute], + initialIndex: 0, + }); + + return ( + + + + ); +} diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts new file mode 100644 index 000000000000..8bb6162f2e76 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/i18ntest.utils.ts @@ -0,0 +1,45 @@ +import i18next, { InitOptions, i18n } from 'i18next'; + +import { appName } from '@/App.constants'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import onboarding from '../../../public/translations/onboarding/Messages_fr_FR.json'; + +export const defaultLocale = 'fr_FR'; +export const defaultAvailableLocales = [defaultLocale]; +function addTranslations() { + i18next.addResources(defaultLocale, 'onboarding', onboarding).use({ + type: 'postProcessor', + name: 'normalize', + process: (value: string) => (value ? value.replace(/&/g, '&') : value), + }); +} + +export const getTesti18nParams = (): InitOptions => ({ + lng: defaultLocale, + defaultNS: appName, + ns: [], + supportedLngs: defaultAvailableLocales, + postProcess: 'normalize', + interpolation: { + escapeValue: false, + }, +}); + +export const initTestI18n = () => + new Promise((resolve) => { + void i18next.init(getTesti18nParams()); + if (i18next.isInitialized) { + addTranslations(); + } else { + i18next.on('initialized', () => { + addTranslations(); + resolve(i18next); + }); + } + }); + +export const labels = { + onboarding, +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts new file mode 100644 index 000000000000..ea5d6633a88c --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/setupMsw.ts @@ -0,0 +1 @@ +export const setupMswMock = () => {}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx new file mode 100644 index 000000000000..a181c0aa3649 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperBuilder.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { + TestProvider, + addI18nextProvider, + addQueryClientProvider, + addShellContextProvider, + createProviderWrapper, +} from './testWrapperProviders'; + +type BuilderConfig = { + withI18next: boolean; + withQueryClient: boolean; + withShellContext: boolean; +}; + +type TestWrapperBuilder = { + withI18next: () => TestWrapperBuilder; + withQueryClient: () => TestWrapperBuilder; + withShellContext: () => TestWrapperBuilder; + build: () => Promise>; +}; + +export const testWrapperBuilder = (): TestWrapperBuilder => { + const config: BuilderConfig = { + withI18next: false, + withQueryClient: false, + withShellContext: false, + }; + const build = async (): Promise> => { + const providers: TestProvider[] = []; + if (config.withI18next) await addI18nextProvider(providers); + if (config.withQueryClient) addQueryClientProvider(providers); + if (config.withShellContext) await addShellContextProvider(providers); + return createProviderWrapper(providers); + }; + const builder = { + withI18next: () => { + config.withI18next = true; + return builder; + }, + withQueryClient: () => { + config.withQueryClient = true; + return builder; + }, + withShellContext: () => { + config.withShellContext = true; + return builder; + }, + build, + }; + return builder; +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx new file mode 100644 index 000000000000..aa12cdbeb5e9 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/src/utils/tests/testWrapperProviders.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { i18n } from 'i18next'; +import { I18nextProvider } from 'react-i18next'; + +import { + ShellContext, + ShellContextType, + initShellContext, +} from '@ovh-ux/manager-react-shell-client'; + +import { appName } from '@/App.constants'; + +import { initTestI18n } from './i18ntest.utils'; + +export type TestProvider = (props: React.PropsWithChildren) => React.ReactNode; + +let context: ShellContextType; +let i18nState: i18n; + +export const createProviderWrapper = ( + providers: TestProvider[], +): React.FC => { + return ({ children }) => + providers.reduceRight((acc, provider) => { + const ProviderComponent = provider; + return {acc}; + }, children); +}; + +export const addI18nextProvider = async (providers: TestProvider[]) => { + if (!i18nState) { + i18nState = await initTestI18n(); + } + providers.push(({ children }) => {children}); +}; + +export const addQueryClientProvider = (providers: TestProvider[]) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + providers.push(({ children }) => ( + {children} + )); +}; + +export const addShellContextProvider = async (providers: TestProvider[]) => { + if (!context) { + context = await initShellContext(appName); + } + providers.push(({ children }) => ( + {children} + )); +}; diff --git a/packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts b/packages/manager/apps/hpc-backup-licenses/src/vite-hmr.ts new file mode 100644 index 000000000000..473d87630039 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/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/hpc-backup-licenses/tailwind.config.mjs b/packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs new file mode 100644 index 000000000000..00ed2ec80677 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/tailwind.config.mjs @@ -0,0 +1,25 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import baseConfig from '@ovh-ux/manager-tailwind-config'; + +const require = createRequire(import.meta.url); +const pkgDir = (name) => path.dirname(require.resolve(`${name}/package.json`)); +const toGlob = (dir) => `${dir.replace(/\\/g, '/')}/**/*.{js,jsx,ts,tsx}`; + +const reactComponentsDir = pkgDir('@ovh-ux/muk'); + +const baseTailwindConfig = [ + ...(baseConfig.content ?? []), + './src/**/*.{js,jsx,ts,tsx}', + toGlob(reactComponentsDir), +]; + +/** @type {import('tailwindcss').Config} */ +export default { + ...baseConfig, + content: baseTailwindConfig, + corePlugins: { + preflight: false, + }, +}; diff --git a/packages/manager/apps/hpc-backup-licenses/tsconfig.json b/packages/manager/apps/hpc-backup-licenses/tsconfig.json new file mode 100644 index 000000000000..dbe36ca19aa8 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@ovh-ux/manager-static-analysis-kit/tsconfig/react-strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/public/*": ["./public/*"], + "@/*": ["./src/*"] + }, + "outDir": "dist", + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "types", "src/__tests__"] +} diff --git a/packages/manager/apps/hpc-backup-licenses/vite.config.mjs b/packages/manager/apps/hpc-backup-licenses/vite.config.mjs new file mode 100644 index 000000000000..57f62cb969c5 --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/vite.config.mjs @@ -0,0 +1,9 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +import { getBaseConfig } from '@ovh-ux/manager-vite-config'; + +export default defineConfig({ + ...getBaseConfig(), + root: resolve(process.cwd()), +}); diff --git a/packages/manager/apps/hpc-backup-licenses/vitest.config.js b/packages/manager/apps/hpc-backup-licenses/vitest.config.js new file mode 100644 index 000000000000..50b7c0fd7acc --- /dev/null +++ b/packages/manager/apps/hpc-backup-licenses/vitest.config.js @@ -0,0 +1,43 @@ +/* eslint-disable no-undef */ +import path from 'node:path'; + +import { + createConfig, + defaultDedupedDependencies, + defaultExcludedFiles, + mergeConfig, + sharedConfig, + sharedCssConfig, +} from '@ovh-ux/manager-tests-setup'; + +export default mergeConfig( + sharedConfig, + createConfig({ + test: { + setupFiles: ['@ovh-ux/manager-core-test-utils/src/utils/setup-test.ts'], + ...sharedCssConfig, + coverage: { + exclude: [ + ...defaultExcludedFiles, + // App-specific exclusions (not in shared config): + 'vite-*.ts', + 'App.tsx', + '__mocks__', + '**/Routes.tsx', + '**/QueryClient.ts', + '**/*.type.ts', + '**/404.page.tsx', + '**/utils/tests/**', + '**/Test.utils.tsx', + ], + }, + }, + resolve: { + dedupe: [...defaultDedupedDependencies], + alias: { + '@/public': path.resolve(__dirname, 'public'), + '@': path.resolve(__dirname, 'src'), + }, + }, + }), +); diff --git a/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx b/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx index d5e9ca88f211..616b9e985f23 100644 --- a/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx +++ b/packages/manager/apps/identity-access-management/src/test-utils/uiTestHelpers.tsx @@ -61,16 +61,15 @@ export const getOdsCheckbox = async ({ disabled?: boolean; nth?: number; }) => { - const checkbox: HTMLOdsCheckboxElement = Array.from( - container.querySelectorAll('ods-checkbox'), - )?.[nth] as HTMLOdsCheckboxElement; + // `OdsCheckbox` is mocked as a native `` in setupTests.tsx + const checkbox: HTMLInputElement = Array.from( + container.querySelectorAll('input[type="checkbox"]'), + )?.[nth] as HTMLInputElement; expect(checkbox).toBeDefined(); - const disabledAttribute = checkbox.getAttribute('is-disabled') === 'true'; - - if (disabled) expect(disabledAttribute).toBeTruthy(); - else expect(disabledAttribute).toBeFalsy(); + if (disabled) expect(checkbox.disabled).toBeTruthy(); + else expect(checkbox.disabled).toBeFalsy(); return checkbox; }; diff --git a/packages/manager/apps/network-vrack/package.json b/packages/manager/apps/network-vrack/package.json index 8febc09106fa..9b00b9ef5a52 100644 --- a/packages/manager/apps/network-vrack/package.json +++ b/packages/manager/apps/network-vrack/package.json @@ -49,6 +49,8 @@ "@ovh-ux/manager-tests-setup": "^0.5.0", "@ovh-ux/manager-vite-config": "*", "@tanstack/react-query-devtools": "5.51.21", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.0", "autoprefixer": "10.4.24", "msw": "2.1.7", "tailwindcss": "^3.4.4", diff --git a/packages/manager/apps/pci-workflow/package.json b/packages/manager/apps/pci-workflow/package.json index ab1793908ce6..279521bc3f5b 100644 --- a/packages/manager/apps/pci-workflow/package.json +++ b/packages/manager/apps/pci-workflow/package.json @@ -32,6 +32,7 @@ "@tanstack/react-query": "5.51.21", "@tanstack/react-query-devtools": "5.51.21", "@tanstack/react-table": "8.20.1", + "axios": "1.12.0", "date-fns": "^3.6.0", "element-internals-polyfill": "^1.3.11", "i18next": "23.8.2", diff --git a/packages/manager/apps/web-domains/package.json b/packages/manager/apps/web-domains/package.json index 897398e86de7..84fbaa3dd15f 100644 --- a/packages/manager/apps/web-domains/package.json +++ b/packages/manager/apps/web-domains/package.json @@ -57,7 +57,9 @@ "@ovh-ux/manager-tests-setup": "^0.8.0", "@ovh-ux/manager-vite-config": "^0.15.6", "@tanstack/react-query-devtools": "^5.90.2", - "@types/punycode": "^2.1.4" + "@types/punycode": "^2.1.4", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.0" }, "regions": [ "EU", diff --git a/packages/manager/apps/web-ongoing-operations/package.json b/packages/manager/apps/web-ongoing-operations/package.json index b144e0b845c3..fb7f1ce1b805 100644 --- a/packages/manager/apps/web-ongoing-operations/package.json +++ b/packages/manager/apps/web-ongoing-operations/package.json @@ -51,7 +51,9 @@ "@ovh-ux/manager-static-analysis-kit": "^0.15.0", "@ovh-ux/manager-tests-setup": "^0.8.0", "@ovh-ux/manager-vite-config": "^0.15.6", - "@tanstack/react-query-devtools": "^5.90.2" + "@tanstack/react-query-devtools": "^5.90.2", + "@types/react": "18.3.18", + "@types/react-dom": "18.3.0" }, "regions": [ "EU", diff --git a/yarn.lock b/yarn.lock index 5887691fe87c..b00b8ec98552 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6655,15 +6655,15 @@ i18next "^23.8.2" i18next-http-backend "^2.4.2" -"@ovh-ux/manager-react-shell-client@^1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-shell-client/-/manager-react-shell-client-1.2.3.tgz#31b7b1297eb192099e1e3d799b02dc5c04c1db49" - integrity sha512-n/FcrTcrki46yzP++tXmrp5+rlOFMl+BaBHrRbf6bvA+usBiUj9GbG5dewigIbnUDOegNzC2Ksrrx2EDU8SGzA== +"@ovh-ux/manager-react-shell-client@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-shell-client/-/manager-react-shell-client-1.2.4.tgz#336ae9acfd5d7e676d9cd0d1f772fc8c25702469" + integrity sha512-3tOTpvQvrT+/4H1ONbpO1V3OiS3Uha03ueAmljWH7AvNQRC+KAa4MVYoCFtHvkxTE0EyFet9YSwhrX33ZdH1ew== dependencies: "@ovh-ux/manager-config" "^8.9.0" - "@ovh-ux/ovh-at-internet" "^0.29.4" + "@ovh-ux/ovh-at-internet" "^0.30.0" "@ovh-ux/request-tagger" "^0.6.0" - "@ovh-ux/shell" "^4.10.6" + "@ovh-ux/shell" "^4.10.7" "@ovh-ux/url-builder" "^2.4.3" i18next "^23.8.2" i18next-http-backend "^2.4.2"