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 000000000000..413028afad19 Binary files /dev/null and b/packages/manager/apps/hpc-backup-licenses/src/assets/error-banner-oops.png differ 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"