From bf1533688d27d58840590edd580ac030b667423d Mon Sep 17 00:00:00 2001 From: stif59100 Date: Tue, 19 Nov 2024 17:43:10 +0100 Subject: [PATCH] feat(web.office): add datagrid for react app ref:MANAGER-16068 Signed-off-by: stif59100 --- .../server-sidebar/universe/WebSidebar.tsx | 41 +- .../navigation-tree/services/webCloud.ts | 382 +++++++++--------- .../translations/sidebar/Messages_fr_FR.json | 3 +- packages/manager/apps/web-office/cucumber.js | 20 - .../web-office/e2e/features/error.feature | 12 - .../e2e/features/onboarding.feature | 7 - .../e2e/step-definitions/error.step.ts | 53 --- .../e2e/step-definitions/onboarding.step.ts | 32 -- .../apps/web-office/e2e/utils/constants.ts | 7 - .../apps/web-office/e2e/utils/index.tsx | 2 - .../apps/web-office/e2e/utils/network.ts | 28 -- packages/manager/apps/web-office/package.json | 59 +-- .../apps/web-office/playwright.config.ts | 20 - .../{postcss.config.js => postcss.config.cjs} | 0 .../translations/licenses/Messages_fr_FR.json | 14 + .../translations/listing/Messages_fr_FR.json | 4 - .../onboarding/Messages_fr_FR.json | 8 +- .../web-office-365/Messages_fr_FR.json | 2 +- packages/manager/apps/web-office/src/App.tsx | 3 - .../apps/web-office/src/api/_mock_/license.ts | 38 ++ .../apps/web-office/src/api/license/api.ts | 20 + .../apps/web-office/src/api/license/key.ts | 5 + .../apps/web-office/src/api/license/type.ts | 22 + .../apps/web-office/src/api/utils/apiPath.ts | 2 + .../src/components/Breadcrumb/Breadcrumb.tsx | 86 +++- .../web-office/src/components/GuideLink.tsx | 40 ++ .../src/components/Loading/Loading.tsx | 4 +- .../web-office/src/data/api/web-office-365.ts | 52 --- .../apps/web-office/src/guides.constants.ts | 86 ++++ .../src/hooks/breadcrumb/useBreadcrumb.tsx | 49 --- .../src/hooks/guide/useGuideUtils.tsx | 100 ----- .../apps/web-office/src/hooks/index.ts | 3 + .../web-office/src/hooks/useGenerateUrl.ts | 32 ++ .../src/hooks/useOfficeLicenseDetails.ts | 24 ++ .../web-office/src/hooks/useOfficeLicenses.ts | 21 + .../manager/apps/web-office/src/index.scss | 2 + .../manager/apps/web-office/src/index.tsx | 7 +- .../web-office/src/pages/dashboard/index.tsx | 59 +-- .../apps/web-office/src/pages/index.tsx | 4 +- .../apps/web-office/src/pages/layout.tsx | 39 +- .../src/pages/licences/licences.page.tsx | 166 ++++++++ .../src/pages/licences/licences.spec.tsx | 20 + .../web-office/src/pages/listing/index.tsx | 115 ------ .../web-office/src/pages/onboarding/index.tsx | 43 +- .../web-office/src/routes/routes.constant.ts | 7 - .../web-office/src/routes/routes.constants.ts | 8 + .../apps/web-office/src/routes/routes.tsx | 85 ++-- ...king.constant.ts => tracking.constants.ts} | 2 +- .../web-office/src/utils/test.provider.tsx | 97 +++++ .../apps/web-office/src/utils/test.setup.tsx | 93 +++++ ...ice-365.config.ts => web-office.config.ts} | 2 +- .../manager/apps/web-office/tsconfig.json | 9 +- .../manager/apps/web-office/vitest.config.js | 37 ++ yarn.lock | 19 +- 54 files changed, 1160 insertions(+), 935 deletions(-) delete mode 100644 packages/manager/apps/web-office/cucumber.js delete mode 100644 packages/manager/apps/web-office/e2e/features/error.feature delete mode 100644 packages/manager/apps/web-office/e2e/features/onboarding.feature delete mode 100644 packages/manager/apps/web-office/e2e/step-definitions/error.step.ts delete mode 100644 packages/manager/apps/web-office/e2e/step-definitions/onboarding.step.ts delete mode 100644 packages/manager/apps/web-office/e2e/utils/constants.ts delete mode 100644 packages/manager/apps/web-office/e2e/utils/index.tsx delete mode 100644 packages/manager/apps/web-office/e2e/utils/network.ts delete mode 100644 packages/manager/apps/web-office/playwright.config.ts rename packages/manager/apps/web-office/{postcss.config.js => postcss.config.cjs} (100%) create mode 100644 packages/manager/apps/web-office/public/translations/licenses/Messages_fr_FR.json delete mode 100644 packages/manager/apps/web-office/public/translations/listing/Messages_fr_FR.json create mode 100644 packages/manager/apps/web-office/src/api/_mock_/license.ts create mode 100644 packages/manager/apps/web-office/src/api/license/api.ts create mode 100644 packages/manager/apps/web-office/src/api/license/key.ts create mode 100644 packages/manager/apps/web-office/src/api/license/type.ts create mode 100644 packages/manager/apps/web-office/src/api/utils/apiPath.ts create mode 100644 packages/manager/apps/web-office/src/components/GuideLink.tsx delete mode 100644 packages/manager/apps/web-office/src/data/api/web-office-365.ts create mode 100644 packages/manager/apps/web-office/src/guides.constants.ts delete mode 100644 packages/manager/apps/web-office/src/hooks/breadcrumb/useBreadcrumb.tsx delete mode 100644 packages/manager/apps/web-office/src/hooks/guide/useGuideUtils.tsx create mode 100644 packages/manager/apps/web-office/src/hooks/index.ts create mode 100644 packages/manager/apps/web-office/src/hooks/useGenerateUrl.ts create mode 100644 packages/manager/apps/web-office/src/hooks/useOfficeLicenseDetails.ts create mode 100644 packages/manager/apps/web-office/src/hooks/useOfficeLicenses.ts create mode 100644 packages/manager/apps/web-office/src/pages/licences/licences.page.tsx create mode 100644 packages/manager/apps/web-office/src/pages/licences/licences.spec.tsx delete mode 100644 packages/manager/apps/web-office/src/pages/listing/index.tsx delete mode 100644 packages/manager/apps/web-office/src/routes/routes.constant.ts create mode 100644 packages/manager/apps/web-office/src/routes/routes.constants.ts rename packages/manager/apps/web-office/src/{tracking.constant.ts => tracking.constants.ts} (86%) create mode 100644 packages/manager/apps/web-office/src/utils/test.provider.tsx create mode 100644 packages/manager/apps/web-office/src/utils/test.setup.tsx rename packages/manager/apps/web-office/src/{web-office-365.config.ts => web-office.config.ts} (75%) create mode 100644 packages/manager/apps/web-office/vitest.config.js diff --git a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/WebSidebar.tsx b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/WebSidebar.tsx index 20501067a55a..c389d6212cbf 100644 --- a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/WebSidebar.tsx +++ b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/WebSidebar.tsx @@ -7,7 +7,7 @@ import useServiceLoader from "./useServiceLoader"; import OrderTrigger from '../order/OrderTrigger'; import webShopConfig from '../order/shop-config/web'; import { ShopItem } from '../order/OrderPopupContent'; -import getIcon from './GetIcon'; +import getIcon from './GetIcon'; import { useFeatureAvailability } from '@ovh-ux/manager-react-components'; export const webFeatures = [ @@ -23,13 +23,13 @@ export const webFeatures = [ 'emails:delegate', 'emails', 'exchange:web-dashboard', - 'office', 'office-reseller', 'sharepoint', 'web:microsoft', 'web-paas', 'cloud-web', 'cloud-database', + 'web-office', 'zimbra' ]; @@ -259,21 +259,36 @@ export default function WebSidebar() { ...service, })); }, - }, - features.office && { - id: 'office', + }, features['web-office'] && + { + id: 'web-office', label: t('sidebar_license_office'), icon: getIcon('ms-Icon ms-Icon--OfficeLogo'), - routeMatcher: new RegExp(`/office`), + routeMatcher: new RegExp(`^/web-office`), async loader() { const services = await loadServices('/license/office'); - return services.map((service) => ({ - icon: getIcon('ms-Icon ms-Icon--OfficeLogo'), - ...service, - })); + return [ + { + id: 'web_office_list', + label: t('sidebar_license_office_list'), + href: navigation.getURL( + 'web-office', + `#/` + ), + icon: getIcon('oui-icon oui-icon-list'), + ignoreSearch: true, + }, + ...services.map((service) => ({ + ...service, + icon: getIcon('ms-Icon ms-Icon--OfficeLogo'), + href: navigation.getURL( + 'web-office', + `#/license/${service.serviceName}` + ), + })), + ]; }, - } - ], + },] }); } else { if (features['exchange:web-dashboard']) { @@ -324,7 +339,7 @@ export default function WebSidebar() { return menu; }; - const {data: availability} = useFeatureAvailability(webFeatures); + const { data: availability } = useFeatureAvailability(webFeatures); useEffect(() => { if (availability) { diff --git a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/webCloud.ts b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/webCloud.ts index 7e786e8adcea..21bacc2428eb 100644 --- a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/webCloud.ts +++ b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/webCloud.ts @@ -2,7 +2,7 @@ import illustration from '@/assets/images/sidebar/web-cloud.png'; import { Node } from "../node"; import OvhProductName from '@ovh-ux/ovh-product-icons/utils/OvhProductNameEnum'; -const webCloudUniverse : Node = { +const webCloudUniverse: Node = { id: 'web-cloud', idAttr: 'web-cloud-link', translation: 'sidebar_web_cloud', @@ -17,208 +17,208 @@ const webCloudUniverse : Node = { }; webCloudUniverse.children = [ - { - id: 'domain-dns', - idAttr: 'domain-dns-link', - universe: webCloudUniverse.id, - translation: 'sidebar_domain_dns', - features: ['web:domains', 'web:domains:zone'], - count: false, - children: [ - { - id: 'domains-operations', - idAttr: 'domains-operations-link', - universe: webCloudUniverse.id, - translation: 'sidebar_domain_operations', - routing: { - application: 'web', - hash: '#/domain/operation', - }, - count: false, - features: ['web:domains'], + { + id: 'domain-dns', + idAttr: 'domain-dns-link', + universe: webCloudUniverse.id, + translation: 'sidebar_domain_dns', + features: ['web:domains', 'web:domains:zone'], + count: false, + children: [ + { + id: 'domains-operations', + idAttr: 'domains-operations-link', + universe: webCloudUniverse.id, + translation: 'sidebar_domain_operations', + routing: { + application: 'web', + hash: '#/domain/operation', }, - { - id: 'domains', - idAttr: 'domains-link', - universe: webCloudUniverse.id, - translation: 'sidebar_domain', - serviceType: 'DOMAIN', - routing: { - application: 'web', - hash: '#/domain', - }, - features: ['web:domains'], + count: false, + features: ['web:domains'], + }, + { + id: 'domains', + idAttr: 'domains-link', + universe: webCloudUniverse.id, + translation: 'sidebar_domain', + serviceType: 'DOMAIN', + routing: { + application: 'web', + hash: '#/domain', }, - { - id: 'dns', - idAttr: 'dns-link', - universe: webCloudUniverse.id, - translation: 'sidebar_dns', - serviceType: 'DOMAIN_ZONE', - routing: { - application: 'web', - hash: '#/zone', - }, - features: ['web:domains:zone'], + features: ['web:domains'], + }, + { + id: 'dns', + idAttr: 'dns-link', + universe: webCloudUniverse.id, + translation: 'sidebar_dns', + serviceType: 'DOMAIN_ZONE', + routing: { + application: 'web', + hash: '#/zone', }, - ], - }, - { - id: 'web-hosting', - idAttr: 'web-hosting-link', - universe: webCloudUniverse.id, - translation: 'sidebar_web_hosting', - features: ['hosting', 'private-database'], - children: [ - { - id: 'hosting', - idAttr: 'hosting-link', - universe: webCloudUniverse.id, - translation: 'sidebar_hosting', - serviceType: 'HOSTING_WEB', - routing: { - application: 'web', - hash: '#/hosting', - }, - features: ['hosting'], + features: ['web:domains:zone'], + }, + ], + }, + { + id: 'web-hosting', + idAttr: 'web-hosting-link', + universe: webCloudUniverse.id, + translation: 'sidebar_web_hosting', + features: ['hosting', 'private-database'], + children: [ + { + id: 'hosting', + idAttr: 'hosting-link', + universe: webCloudUniverse.id, + translation: 'sidebar_hosting', + serviceType: 'HOSTING_WEB', + routing: { + application: 'web', + hash: '#/hosting', }, - { - id: 'web-databases', - idAttr: 'web-databases-link', - universe: webCloudUniverse.id, - translation: 'sidebar_web_db', - serviceType: 'HOSTING_PRIVATEDATABASE', - routing: { - application: 'web', - hash: '#/private_database', - }, - features: ['private-database'], + features: ['hosting'], + }, + { + id: 'web-databases', + idAttr: 'web-databases-link', + universe: webCloudUniverse.id, + translation: 'sidebar_web_db', + serviceType: 'HOSTING_PRIVATEDATABASE', + routing: { + application: 'web', + hash: '#/private_database', }, - ], - }, - { - id: 'web-paas', - idAttr: 'web-paas-link', - universe: webCloudUniverse.id, - translation: 'sidebar_web_paas', - children: [ - { - id: 'platform-sh', - idAttr: 'platform-sh-link', - universe: webCloudUniverse.id, - translation: 'sidebar_platform_sh', - serviceType: 'WEBPAAS_SUBSCRIPTION', - routing: { - application: 'web', - hash: '#/paas/webpaas/projects', - }, - features: ['web-paas'], + features: ['private-database'], + }, + ], + }, + { + id: 'web-paas', + idAttr: 'web-paas-link', + universe: webCloudUniverse.id, + translation: 'sidebar_web_paas', + children: [ + { + id: 'platform-sh', + idAttr: 'platform-sh-link', + universe: webCloudUniverse.id, + translation: 'sidebar_platform_sh', + serviceType: 'WEBPAAS_SUBSCRIPTION', + routing: { + application: 'web', + hash: '#/paas/webpaas/projects', }, - ], - features: ['web-paas'], - }, - { - id: 'emails', - idAttr: 'emails-link', - universe: webCloudUniverse.id, - translation: 'sidebar_emails', - features: ['email-pro', 'emails:mxplan', 'zimbra'], - children: [ - { - id: 'zimbra', - idAttr: 'zimbra-link', - universe: webCloudUniverse.id, - translation: 'sidebar_zimbra', - routing: { - application: 'zimbra', - hash: '#/', - }, - features: ['zimbra'], - count: false, + features: ['web-paas'], + }, + ], + features: ['web-paas'], + }, + { + id: 'emails', + idAttr: 'emails-link', + universe: webCloudUniverse.id, + translation: 'sidebar_emails', + features: ['email-pro', 'emails:mxplan', 'zimbra'], + children: [ + { + id: 'zimbra', + idAttr: 'zimbra-link', + universe: webCloudUniverse.id, + translation: 'sidebar_zimbra', + routing: { + application: 'zimbra', + hash: '#/', }, - { - id: 'email-pro', - idAttr: 'email-pro-link', - universe: webCloudUniverse.id, - translation: 'sidebar_email_pro', - serviceType: 'EMAIL_PRO', - routing: { - application: 'web', - hash: '#/email_pro', - }, - features: ['email-pro'], + features: ['zimbra'], + count: false, + }, + { + id: 'email-pro', + idAttr: 'email-pro-link', + universe: webCloudUniverse.id, + translation: 'sidebar_email_pro', + serviceType: 'EMAIL_PRO', + routing: { + application: 'web', + hash: '#/email_pro', }, - { - id: 'mxplan', - idAttr: 'mxplan-link', - universe: webCloudUniverse.id, - translation: 'sidebar_mxplan', - serviceType: 'EMAIL_DOMAIN', - routing: { - application: 'web', - hash: '#/email_domain', - }, - features: ['emails:mxplan'], + features: ['email-pro'], + }, + { + id: 'mxplan', + idAttr: 'mxplan-link', + universe: webCloudUniverse.id, + translation: 'sidebar_mxplan', + serviceType: 'EMAIL_DOMAIN', + routing: { + application: 'web', + hash: '#/email_domain', }, - { - id: 'email-delegated', - idAttr: 'email-delegated-link', - universe: webCloudUniverse.id, - translation: 'sidebar_email_delegated', - serviceType: 'EMAIL_DELEGATED', - routing: { - application: 'web', - hash: '#/email_delegate', - }, - features: ['emails:delegate'], + features: ['emails:mxplan'], + }, + { + id: 'email-delegated', + idAttr: 'email-delegated-link', + universe: webCloudUniverse.id, + translation: 'sidebar_email_delegated', + serviceType: 'EMAIL_DELEGATED', + routing: { + application: 'web', + hash: '#/email_delegate', }, - ], - }, - { - id: 'microsoft', - idAttr: 'microsoft-link', - universe: webCloudUniverse.id, - translation: 'sidebar_microsoft', - features: ['office', 'exchange'], - children: [ - { - id: 'office', - idAttr: 'office-link', - universe: webCloudUniverse.id, - translation: 'sidebar_license_office', - serviceType: 'LICENSE_OFFICE', - routing: { - application: 'web', - hash: '#/office/license', - }, - features: ['office'], + features: ['emails:delegate'], + }, + ], + }, + { + id: 'microsoft', + idAttr: 'microsoft-link', + universe: webCloudUniverse.id, + translation: 'sidebar_microsoft', + features: ['web-office', 'exchange'], + children: [ + { + id: 'web-office', + idAttr: 'web-office-link', + universe: webCloudUniverse.id, + translation: 'sidebar_license_office', + serviceType: 'LICENSE_OFFICE', + routing: { + application: 'web', + hash: '#/web-office', }, - { - id: 'exchange', - idAttr: 'exchange-link', - universe: webCloudUniverse.id, - translation: 'sidebar_exchange', - serviceType: 'EMAIL_EXCHANGE_SERVICE', - routing: { - application: 'web', - hash: '#/exchange', - }, - features: ['exchange'], + features: ['web-office'], + }, + { + id: 'exchange', + idAttr: 'exchange-link', + universe: webCloudUniverse.id, + translation: 'sidebar_exchange', + serviceType: 'EMAIL_EXCHANGE_SERVICE', + routing: { + application: 'web', + hash: '#/exchange', }, - { - id: 'sharepoint', - idAttr: 'sharepoint-link', - universe: webCloudUniverse.id, - translation: 'sidebar_sharepoint', - serviceType: 'MSSERVICES_SHAREPOINT', - routing: { - application: 'web', - hash: '#/sharepoint', - }, - features: ['sharepoint'], + features: ['exchange'], + }, + { + id: 'sharepoint', + idAttr: 'sharepoint-link', + universe: webCloudUniverse.id, + translation: 'sidebar_sharepoint', + serviceType: 'MSSERVICES_SHAREPOINT', + routing: { + application: 'web', + hash: '#/sharepoint', }, - ], - }, - ]; + features: ['sharepoint'], + }, + ], + }, +]; export default webCloudUniverse; diff --git a/packages/manager/apps/container/src/public/translations/sidebar/Messages_fr_FR.json b/packages/manager/apps/container/src/public/translations/sidebar/Messages_fr_FR.json index 482a094df48c..45e4261dd439 100644 --- a/packages/manager/apps/container/src/public/translations/sidebar/Messages_fr_FR.json +++ b/packages/manager/apps/container/src/public/translations/sidebar/Messages_fr_FR.json @@ -89,7 +89,7 @@ "sidebar_pci_ssh_keys": "Clés SSH", "sidebar_pci_billing": "Facturation", "sidebar_pci_credits_vouchers": "Crédits & Vouchers", - "sidebar_pci_savings_plan":"Savings Plans", + "sidebar_pci_savings_plan": "Savings Plans", "sidebar_pci_contacts_rights": "Contacts & droits", "sidebar_pci_project_settings": "Project Settings", "sidebar_pci_load_error": "Une erreur est survenue lors de la récupération de la liste de projets", @@ -135,6 +135,7 @@ "sidebar_microsoft": "Microsoft", "sidebar_microsoft_exchange": "Microsoft Exchange", "sidebar_license_office": "Microsoft 365", + "sidebar_license_office_list": "Mes licences", "sidebar_internet": "Accès Internet", "sidebar_packs_xdsl": "Offres Internet", "sidebar_internet_line": "Ligne internet seule", diff --git a/packages/manager/apps/web-office/cucumber.js b/packages/manager/apps/web-office/cucumber.js deleted file mode 100644 index 8e6abbfbca8f..000000000000 --- a/packages/manager/apps/web-office/cucumber.js +++ /dev/null @@ -1,20 +0,0 @@ -const isCI = process.env.CI; - -module.exports = { - default: { - paths: ['e2e/features/**/*.feature'], - require: [ - '../../../../playwright-helpers/bdd-setup.ts', - 'e2e/**/*.step.ts', - ], - requireModule: ['ts-node/register'], - format: [ - 'summary', - isCI ? 'progress' : 'progress-bar', - !isCI && ['html', 'e2e/reports/cucumber-results-report.html'], - !isCI && ['usage-json', 'e2e/reports/cucumber-usage-report.json'], - ].filter(Boolean), - formatOptions: { snippetInterface: 'async-await' }, - retry: 1, - }, -}; diff --git a/packages/manager/apps/web-office/e2e/features/error.feature b/packages/manager/apps/web-office/e2e/features/error.feature deleted file mode 100644 index e20e56f5f592..000000000000 --- a/packages/manager/apps/web-office/e2e/features/error.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: Error - - Scenario Outline: Display an error if request fails - Given The service to fetch the data is - When User navigates to Home page - Then User "" the list of data - Then User sees error - - Examples: - | apiOk | sees | anyError | - | OK | sees | no | - | KO | doesn't see | an | diff --git a/packages/manager/apps/web-office/e2e/features/onboarding.feature b/packages/manager/apps/web-office/e2e/features/onboarding.feature deleted file mode 100644 index 224cd5e54267..000000000000 --- a/packages/manager/apps/web-office/e2e/features/onboarding.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Onboarding page - - Scenario: User wants to find informations related to web-office-365 - Given User has 0 elements in the Listing page - When User navigates to Listing page - Then User gets redirected to Onboarding page - Then User sees 3 guides diff --git a/packages/manager/apps/web-office/e2e/step-definitions/error.step.ts b/packages/manager/apps/web-office/e2e/step-definitions/error.step.ts deleted file mode 100644 index 257d4ea64023..000000000000 --- a/packages/manager/apps/web-office/e2e/step-definitions/error.step.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Given, When, Then } from '@cucumber/cucumber'; -import { expect } from '@playwright/test'; -import { ICustomWorld } from '../../../../../../playwright-helpers'; -import { ConfigParams, getUrl, setupNetwork } from '../utils'; -import { title } from '../../public/translations/listing/Messages_fr_FR.json'; -import { - manager_error_page_title, - manager_error_page_action_home_label, - manager_error_page_action_reload_label, -} from '../../public/translations/web-office-365/error/Messages_fr_FR.json'; - -Given('The service to fetch the data is {word}', function( - this: ICustomWorld, - apiState: 'OK' | 'KO', -) { - this.handlersConfig.isKo = apiState === 'KO'; -}); - -When('User navigates to Home page', async function( - this: ICustomWorld, -) { - await setupNetwork(this); - await this.page.goto(this.testContext.initialUrl || getUrl('root'), { - waitUntil: 'load', - }); -}); - -Then('User {string} the list of data', async function( - this: ICustomWorld, - see: 'sees' | "doesn't see", -) { - if (see === 'sees') { - const titleElement = await this.page.getByText(title); - await expect(titleElement).toBeVisible(); - } -}); - -Then('User sees {word} error', async function( - this: ICustomWorld, - anyError: 'an' | 'no', -) { - if (anyError === 'an') { - await expect(this.page.getByText(manager_error_page_title)).toBeVisible(); - - await expect( - this.page.getByText(manager_error_page_action_home_label), - ).toBeVisible(); - - await expect( - this.page.getByText(manager_error_page_action_reload_label), - ).toBeVisible(); - } -}); diff --git a/packages/manager/apps/web-office/e2e/step-definitions/onboarding.step.ts b/packages/manager/apps/web-office/e2e/step-definitions/onboarding.step.ts deleted file mode 100644 index 67b20b6e52c8..000000000000 --- a/packages/manager/apps/web-office/e2e/step-definitions/onboarding.step.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Given, When, Then } from '@cucumber/cucumber'; -import { expect } from '@playwright/test'; -import { ICustomWorld } from '../../../../../../playwright-helpers'; -import { ConfigParams, getUrl, setupNetwork } from '../utils'; - -Given('User has {int} elements in the Listing page', function( - this: ICustomWorld, - nb: number, -) { - this.handlersConfig.nb = nb; -}); - -When('User navigates to Listing page', async function( - this: ICustomWorld, -) { - await setupNetwork(this); - await this.page.goto(getUrl('listing'), { waitUntil: 'load' }); -}); - -Then('User gets redirected to Onboarding page', async function( - this: ICustomWorld, -) { - await expect(this.page).toHaveURL(getUrl('onboarding')); -}); - -Then('User sees {int} guides', async function( - this: ICustomWorld, - nbGuides: number, -) { - const guides = await this.page.locator('osds-tile'); - await expect(guides).toHaveCount(nbGuides); -}); diff --git a/packages/manager/apps/web-office/e2e/utils/constants.ts b/packages/manager/apps/web-office/e2e/utils/constants.ts deleted file mode 100644 index 866e6dadabe9..000000000000 --- a/packages/manager/apps/web-office/e2e/utils/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { urls } from '../../src/routes/routes.constant'; - -export const appUrl = 'http://localhost:9001/app'; - -export type AppRoute = keyof typeof urls; - -export const getUrl = (route: AppRoute) => `${appUrl}/#${urls[route]}`; diff --git a/packages/manager/apps/web-office/e2e/utils/index.tsx b/packages/manager/apps/web-office/e2e/utils/index.tsx deleted file mode 100644 index 24a453c58aa9..000000000000 --- a/packages/manager/apps/web-office/e2e/utils/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './network'; -export * from './constants'; diff --git a/packages/manager/apps/web-office/e2e/utils/network.ts b/packages/manager/apps/web-office/e2e/utils/network.ts deleted file mode 100644 index 46d894b9a08e..000000000000 --- a/packages/manager/apps/web-office/e2e/utils/network.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BrowserContext } from '@playwright/test'; -import { - ICustomWorld, - toPlaywrightMockHandler, - Handler, -} from '../../../../../../playwright-helpers'; -import { - GetAuthenticationMocks, - getAuthenticationMocks, -} from '../../../../../../playwright-helpers/mocks/auth'; -import { getExampleMocks, GetExampleMocksParams } from '../../mocks'; - -export type ConfigParams = GetAuthenticationMocks & GetExampleMocksParams; - -export const getConfig = (params: ConfigParams): Handler[] => - [getAuthenticationMocks, getExampleMocks].flatMap((getMocks) => - getMocks(params), - ); - -export const setupNetwork = async (world: ICustomWorld) => - Promise.all( - getConfig({ - ...((world?.handlersConfig as ConfigParams) || ({} as ConfigParams)), - isAuthMocked: true, - }) - .reverse() - .map(toPlaywrightMockHandler(world.context as BrowserContext)), - ); diff --git a/packages/manager/apps/web-office/package.json b/packages/manager/apps/web-office/package.json index 262bb8f78e0f..21e05f8a9433 100644 --- a/packages/manager/apps/web-office/package.json +++ b/packages/manager/apps/web-office/package.json @@ -6,7 +6,7 @@ "repository": { "type": "git", "url": "git+https://github.com/ovh/manager.git", - "directory": "packages/manager/apps/web-office-365" + "directory": "packages/manager/apps/web-office" }, "license": "BSD-3-Clause", "author": "OVH SAS", @@ -16,44 +16,51 @@ "start": "lerna exec --stream --scope='@ovh-ux/manager-web-office-app' --include-dependencies -- npm run build --if-present", "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-web-office-app' --include-dependencies -- npm run dev --if-present", "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-web-office-app' --include-dependencies -- npm run dev:watch --if-present", - "test:e2e": "tsc && node ../../../../scripts/run-playwright-bdd.js", - "test:e2e:ci": "tsc && node ../../../../scripts/run-playwright-bdd.js --ci" + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { - "@ovh-ux/manager-config": "^7.5.3-alpha.0", - "@ovh-ux/manager-core-api": "^0.9.0-alpha.0", + "@ovh-ux/manager-config": "^8.0.0", + "@ovh-ux/manager-core-api": "^0.9.0", "@ovh-ux/manager-core-utils": "^0.3.0", - "@ovh-ux/manager-react-components": "^1.41.0-alpha.2", - "@ovh-ux/manager-react-core-application": "^0.10.8-alpha.0", - "@ovh-ux/manager-react-shell-client": "^0.8.0-alpha.0", - "@ovh-ux/manager-tailwind-config": "^0.2.0", - "@ovh-ux/request-tagger": "^0.4.0-alpha.0", - "@ovh-ux/shell": "^3.11.0-alpha.0", - "@ovhcloud/ods-common-core": "17.2.2", - "@ovhcloud/ods-common-theming": "17.2.2", - "@ovhcloud/ods-components": "17.2.2", - "@ovhcloud/ods-theme-blue-jeans": "17.2.2", + "@ovh-ux/manager-module-order": "^0.9.0", + "@ovh-ux/manager-react-components": "2.2.0", + "@ovh-ux/manager-react-core-application": "^0.11.1", + "@ovh-ux/manager-react-shell-client": "^0.8.1", + "@ovh-ux/manager-tailwind-config": "*", + "@ovh-ux/request-tagger": "^0.4.0", + "@ovh-ux/shell": "^4.0.1", + "@ovhcloud/ods-components": "^18.3.1", + "@ovhcloud/ods-themes": "^18.3.1", "@tanstack/react-query": "^5.51.21", - "@tanstack/react-query-devtools": "^5.51.21", - "axios": "^1.1.2", - "clsx": "^1.2.1", + "@tanstack/react-table": "^8.20.1", + "element-internals-polyfill": "^1.3.10", "i18next": "^23.8.2", - "i18next-http-backend": "^2.4.2", + "i18next-http-backend": "2.5.0", + "jsurl": "^0.1.5", "react": "^18.2.0", - "react-dom": "^18.2.0", + "react-dom": "18.2.0", "react-i18next": "^14.0.5", "react-router-dom": "^6.3.0", "tailwindcss": "^3.4.4" }, "devDependencies": { - "@cucumber/cucumber": "^10.3.1", - "@ovh-ux/manager-vite-config": "*", - "@playwright/test": "^1.41.2", - "typescript": "^5.1.6", - "vite": "^5.2.13" + "@ovh-ux/manager-vite-config": "^0.8.2", + "@tanstack/react-query-devtools": "^5.51.21", + "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^4.3.2", + "vite": "^5.2.13", + "vitest": "^2.1.2" }, "regions": [ - "CA", "EU" ] } diff --git a/packages/manager/apps/web-office/playwright.config.ts b/packages/manager/apps/web-office/playwright.config.ts deleted file mode 100644 index feb249bcbe3f..000000000000 --- a/packages/manager/apps/web-office/playwright.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - workers: 3, - fullyParallel: false, - timeout: 30 * 1000, - reporter: [['html', { open: 'on-failure' }]], - expect: { - timeout: 20000, - }, - use: { - // Collect trace when retrying the failed test. - trace: 'retain-on-failure', - }, - testMatch: '**/*.e2e.ts', - webServer: { - command: 'yarn run dev', - url: 'http://localhost:9000/', - }, -}); diff --git a/packages/manager/apps/web-office/postcss.config.js b/packages/manager/apps/web-office/postcss.config.cjs similarity index 100% rename from packages/manager/apps/web-office/postcss.config.js rename to packages/manager/apps/web-office/postcss.config.cjs diff --git a/packages/manager/apps/web-office/public/translations/licenses/Messages_fr_FR.json b/packages/manager/apps/web-office/public/translations/licenses/Messages_fr_FR.json new file mode 100644 index 000000000000..8dd7043f5a6b --- /dev/null +++ b/packages/manager/apps/web-office/public/translations/licenses/Messages_fr_FR.json @@ -0,0 +1,14 @@ +{ + "microsoft_office_licenses_title": "Microsoft 365", + "microsoft_office_licenses_order": "Commander", + "microsoft_office_licenses_servicename": "Nom du service", + "microsoft_office_licenses_firstname": "Prénom", + "microsoft_office_licenses_lastname": "Nom", + "microsoft_office_licenses_zipcode": "Code Postal", + "microsoft_office_licenses_city": "Ville", + "microsoft_office_licenses_address": "Adresse", + "microsoft_office_licenses_phone": "Téléphone", + "microsoft_office_licenses_servicetype": "Type de service", + "microsoft_office_licenses_status": "Statut", + "microsoft_office_licenses_displayName": "Nom d'affichage" +} diff --git a/packages/manager/apps/web-office/public/translations/listing/Messages_fr_FR.json b/packages/manager/apps/web-office/public/translations/listing/Messages_fr_FR.json deleted file mode 100644 index c882bc92cfec..000000000000 --- a/packages/manager/apps/web-office/public/translations/listing/Messages_fr_FR.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Listing page", - "listing_resultats": "résultats" -} diff --git a/packages/manager/apps/web-office/public/translations/onboarding/Messages_fr_FR.json b/packages/manager/apps/web-office/public/translations/onboarding/Messages_fr_FR.json index 3d8110913ef5..96f357a463a2 100644 --- a/packages/manager/apps/web-office/public/translations/onboarding/Messages_fr_FR.json +++ b/packages/manager/apps/web-office/public/translations/onboarding/Messages_fr_FR.json @@ -1,10 +1,10 @@ { - "title": "web-office-365", + "title": "web-office", "description": "Découvrez des services de stockage managés qui s’appuient sur le système de fichiers OpenZFS. Bénéficiez en quelques clics d’espaces de stockage centralisés pour entreposer ou sauvegarder vos données et fichiers.", - "orderButtonLabel": "Commander un web-office-365", - "moreInfoButtonLabel": "En savoir plus sur web-office-365", + "orderButtonLabel": "Commander un web-office", + "moreInfoButtonLabel": "En savoir plus sur web-office", "guideCategory": "Tutoriel", - "guide1Title": "Premiers pas avec un web-office-365", + "guide1Title": "Premiers pas avec un web-office", "guide1Description": "Découvrez comment gérer un NAS-HA depuis l'espace-client OVHcloud", "guide2Title": "Monter votre NAS via un partage NFS", "guide2Description": "Découvrez comment monter un NAS via un partage NFS", diff --git a/packages/manager/apps/web-office/public/translations/web-office-365/Messages_fr_FR.json b/packages/manager/apps/web-office/public/translations/web-office-365/Messages_fr_FR.json index 6277403f1291..7ea7eb873732 100644 --- a/packages/manager/apps/web-office/public/translations/web-office-365/Messages_fr_FR.json +++ b/packages/manager/apps/web-office/public/translations/web-office-365/Messages_fr_FR.json @@ -1,6 +1,6 @@ { "title": "Bienvenue uapp", - "crumb": "web-office-365", + "crumb": "web-office", "tabs_2": "Tabs 2", "onboarding": "Onboarding" } diff --git a/packages/manager/apps/web-office/src/App.tsx b/packages/manager/apps/web-office/src/App.tsx index 9bedb39c2cae..da7cc51b9857 100644 --- a/packages/manager/apps/web-office/src/App.tsx +++ b/packages/manager/apps/web-office/src/App.tsx @@ -1,13 +1,10 @@ import React, { useEffect, useContext } from 'react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { odsSetup } from '@ovhcloud/ods-common-core'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { RouterProvider, createHashRouter } from 'react-router-dom'; import { Routes } from './routes/routes'; -odsSetup(); - const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/packages/manager/apps/web-office/src/api/_mock_/license.ts b/packages/manager/apps/web-office/src/api/_mock_/license.ts new file mode 100644 index 000000000000..5e25b7f39550 --- /dev/null +++ b/packages/manager/apps/web-office/src/api/_mock_/license.ts @@ -0,0 +1,38 @@ +import { LicenseType } from '../license/type'; + +export const licensesMock: LicenseType[] = [ + { + address: '1234 Main St', + city: 'Paris', + creationDate: '2020-01-15T12:00:00+02:00', + displayName: 'user123.o365.ovh.com', + firstName: 'John', + lastName: 'Doe', + phone: '0033123456789', + serviceName: 'user123.o365.ovh.com', + serviceType: 'payAsYouGo', + status: 'active', + zipCode: '75001', + iam: { + id: '12345abc-6789-def0-gh12-3456789ijklm', + urn: 'urn:v1:eu:resource:licenseOffice:user123.o365.ovh.com', + }, + }, + { + address: '12345 Main St', + city: 'Lille', + creationDate: '2020-01-15T12:00:00+02:00', + displayName: 'user123.o365.ovh.com', + firstName: 'Test', + lastName: 'Test', + phone: '0033123456789', + serviceName: 'topuser125.o365.ovh.com', + serviceType: 'payAsYouGo', + status: 'active', + zipCode: '59000', + iam: { + id: '12345abc-6789-def0-gh12-3456789ijklm', + urn: 'urn:v1:eu:resource:licenseOffice:user123.o365.ovh.com', + }, + }, +]; diff --git a/packages/manager/apps/web-office/src/api/license/api.ts b/packages/manager/apps/web-office/src/api/license/api.ts new file mode 100644 index 000000000000..47dd4db24a46 --- /dev/null +++ b/packages/manager/apps/web-office/src/api/license/api.ts @@ -0,0 +1,20 @@ +import { v6 } from '@ovh-ux/manager-core-api'; +import { GetOfficeLicenseServiceParams } from './type'; +import { getApiPath } from '../utils/apiPath'; + +// GET + +export const getlicenseOfficeServiceQueryKey = ( + params: GetOfficeLicenseServiceParams, +) => [`get/license/office/${params.serviceName}`]; + +export const getOfficeLicenseDetails = async (serviceName: string) => { + const { data } = await v6.get(`${getApiPath(serviceName)}`); + return data; +}; + +// POST + +// PUT + +// DELETE diff --git a/packages/manager/apps/web-office/src/api/license/key.ts b/packages/manager/apps/web-office/src/api/license/key.ts new file mode 100644 index 000000000000..09bbd889458e --- /dev/null +++ b/packages/manager/apps/web-office/src/api/license/key.ts @@ -0,0 +1,5 @@ +export const getOfficeLicenseDetailsQueryKey = (serviceName: string) => [ + `get/license/office/${serviceName}`, +]; + +export const getOfficeLicenseQueryKey = () => [`get/license/office`]; diff --git a/packages/manager/apps/web-office/src/api/license/type.ts b/packages/manager/apps/web-office/src/api/license/type.ts new file mode 100644 index 000000000000..26464de3679e --- /dev/null +++ b/packages/manager/apps/web-office/src/api/license/type.ts @@ -0,0 +1,22 @@ +export type LicenseType = { + address: string; + city: string; + creationDate: string; + displayName: string; + firstName: string; + lastName: string; + phone: string; + serviceName: string; + serviceType: string; + status: string; + zipCode: string; + iam: { + id: string; + urn: string; + }; + [key: string]: string | { id: string; urn: string }; +}; + +export type GetOfficeLicenseServiceParams = { + serviceName?: string; +}; diff --git a/packages/manager/apps/web-office/src/api/utils/apiPath.ts b/packages/manager/apps/web-office/src/api/utils/apiPath.ts new file mode 100644 index 000000000000..7e4f8e3aded4 --- /dev/null +++ b/packages/manager/apps/web-office/src/api/utils/apiPath.ts @@ -0,0 +1,2 @@ +export const getApiPath = (serviceName: string) => + `/license/office/${serviceName}/`; diff --git a/packages/manager/apps/web-office/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/apps/web-office/src/components/Breadcrumb/Breadcrumb.tsx index 8b7072cdf723..febcad62ee32 100644 --- a/packages/manager/apps/web-office/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/manager/apps/web-office/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,25 +1,73 @@ -import React from 'react'; -import { OsdsBreadcrumb } from '@ovhcloud/ods-components/react'; +import React, { useMemo } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; import { - useBreadcrumb, - BreadcrumbItem, -} from '@/hooks/breadcrumb/useBreadcrumb'; -import appConfig from '@/web-office-365.config'; + OdsBreadcrumb, + OdsBreadcrumbItem, +} from '@ovhcloud/ods-components/react'; +import { useTranslation } from 'react-i18next'; +import { ODS_LINK_COLOR } from '@ovhcloud/ods-components'; -export interface BreadcrumbProps { - customRootLabel?: string; - appName?: string; - items?: BreadcrumbItem[]; -} +export type BreadcrumbProps = { + items?: { label: string; href?: string }[]; + overviewUrl?: string; +}; -function Breadcrumb({ customRootLabel }: BreadcrumbProps): JSX.Element { - const label = customRootLabel || appConfig.rootLabel; +export const Breadcrumb: React.FC = () => { + const { serviceName } = useParams(); + const { t } = useTranslation('dashboard'); + const location = useLocation(); - const breadcrumbItems = useBreadcrumb({ - rootLabel: label, - appName: 'web-office-365', - }); - return ; -} + const rootUrl = serviceName + ? '#/:serviceName'.replace(':serviceName', '') + : '#/'; + + const breadcrumbItems = useMemo(() => { + const pathParts = location.pathname.split('/').filter(Boolean); + const breadcrumbParts = pathParts.slice(1); + return [ + { + label: 'Microsoft 365', + href: rootUrl, + }, + ...(breadcrumbParts.length === 1 + ? [ + { + label: serviceName, + href: `${rootUrl}`, + }, + ] + : breadcrumbParts.map((_, index) => { + const label = + index === breadcrumbParts.length - 1 + ? serviceName + : t( + `microsoft_office__dashboard_${breadcrumbParts + .slice(0, index + 1) + .join('_')}`, + ); + + const url = `#/${pathParts.slice(0, index + 2).join('/')}`; + return { + label, + href: url, + }; + })), + ].filter(Boolean); + }, [location, serviceName, rootUrl, t]); + + return ( + + {breadcrumbItems.map((item) => ( + + ))} + + ); +}; export default Breadcrumb; diff --git a/packages/manager/apps/web-office/src/components/GuideLink.tsx b/packages/manager/apps/web-office/src/components/GuideLink.tsx new file mode 100644 index 000000000000..1c252dd82142 --- /dev/null +++ b/packages/manager/apps/web-office/src/components/GuideLink.tsx @@ -0,0 +1,40 @@ +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import React, { useContext, useMemo } from 'react'; +import { + IconLinkAlignmentType, + Links, + LinkType, +} from '@ovh-ux/manager-react-components'; +import { ODS_LINK_COLOR } from '@ovhcloud/ods-components'; +import { Guide } from '@/guides.constants'; + +interface GuideLinkProps { + label: string; + guide: string | Guide; +} + +export default function GuideLink({ label, guide }: Readonly) { + const context = useContext(ShellContext); + const { ovhSubsidiary } = context.environment.getUser(); + + const url = useMemo(() => { + if (typeof guide === 'string') { + return guide; + } + + return (typeof guide.url === 'string' + ? guide.url + : guide.url?.[ovhSubsidiary] || guide.url.DEFAULT) as string; + }, [guide, ovhSubsidiary]); + + return ( + + ); +} diff --git a/packages/manager/apps/web-office/src/components/Loading/Loading.tsx b/packages/manager/apps/web-office/src/components/Loading/Loading.tsx index 1f8e45b3d2ed..4c69fd5a5a11 100644 --- a/packages/manager/apps/web-office/src/components/Loading/Loading.tsx +++ b/packages/manager/apps/web-office/src/components/Loading/Loading.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { OsdsSpinner } from '@ovhcloud/ods-components/react'; +import { OdsSpinner } from '@ovhcloud/ods-components/react'; export default function Loading() { return (
- +
); diff --git a/packages/manager/apps/web-office/src/data/api/web-office-365.ts b/packages/manager/apps/web-office/src/data/api/web-office-365.ts deleted file mode 100644 index 59e5672f9be1..000000000000 --- a/packages/manager/apps/web-office/src/data/api/web-office-365.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { fetchIcebergV6, apiClient } from '@ovh-ux/manager-core-api'; - -export type GetlicenseOfficeListParams = { - /** Filter resources on IAM tags */ - iamTags: any; -}; - -export const getlicenseOfficeListQueryKey = ['get/license/office']; - -/** - * Operations about Office services : List available services - */ -export const getlicenseOfficeList = async ( - params: GetlicenseOfficeListParams, -): Promise => apiClient.v6.get('/license/office', { data: params }); - -export type GetlicenseOfficeServiceParams = { - /** Service name */ - serviceName?: any; -}; - -export const getlicenseOfficeServiceQueryKey = ( - params: GetlicenseOfficeServiceParams, -) => [`get/license/office/${params.serviceName}`]; - -/** - * Operations about Office services : Get this object properties - */ -export const getlicenseOfficeService = async ( - params: GetlicenseOfficeServiceParams, -): Promise => apiClient.v6.get(`/license/office/${params.serviceName}`); - -/** - * Get listing with iceberg V6 - */ -export const getListingIcebergV6 = async ({ - pageSize, - page, -}: { - pageSize: number; - page: number; -}) => { - const { data, status, totalCount } = await fetchIcebergV6({ - route: `/license/office`, - pageSize, - page, - }); - if (status > 400) { - throw new Error(); - } - return { data, status, totalCount }; -}; diff --git a/packages/manager/apps/web-office/src/guides.constants.ts b/packages/manager/apps/web-office/src/guides.constants.ts new file mode 100644 index 000000000000..7ade584c7f26 --- /dev/null +++ b/packages/manager/apps/web-office/src/guides.constants.ts @@ -0,0 +1,86 @@ +export interface GuideLinks { + [key: string]: string | Guide; + FR?: string; + GB?: string; + DE?: string; + ES?: string; + IT?: string; + PL?: string; + PT?: string; + IE?: string; + DEFAULT?: string; + MA?: string; + TN?: string; + SN?: string; + IN?: string; +} + +export interface Guide { + key: string; + url: GuideLinks | string; + tracking: string; +} + +const helpRoot = 'https://docs.ovh.com/'; + +export const WEB_OFFICE_ONBOARDING_1: GuideLinks = { + FR: `${helpRoot}fr/microsoft-collaborative-solutions/commander-et-gerer-un-groupe-de-licences-office-365-ovh/`, + DE: `${helpRoot}de/microsoft-collaborative-solutions/bestellung_und_verwaltung_einer_office_365_lizenzgruppe_bei_ovh/`, + ES: `${helpRoot}es/microsoft-collaborative-solutions/contratar_y_gestionar_un_grupo_de_licencias_de_office_365_ovh/`, + IE: `${helpRoot}ie/en/microsoft-collaborative-solutions/manage-office-365-csp1/`, + IT: `${helpRoot}it/microsoft-collaborative-solutions/ordina_e_gestisci_un_gruppo_di_licenze_office_365_ovh/`, + PL: `${helpRoot}pl/microsoft-collaborative-solutions/zamowienie_grupy_licencji_office_365_ovh/`, + PT: `${helpRoot}pt/microsoft-collaborative-solutions/encomendar-et-gerir-um-grupo-de-licenças-office-365-ovh/`, + GB: `${helpRoot}gb/en/microsoft-collaborative-solutions/manage-office-365-csp1/`, + DEFAULT: `${helpRoot}gb/en/microsoft-collaborative-solutions/manage-office-365-csp1/`, +}; +export const WEB_OFFICE_ONBOARDING_2: GuideLinks = { + FR: `${helpRoot}fr/microsoft-collaborative-solutions/commander-et-gerer-un-groupe-de-licences-office-365-revendeur-csp2-ovh/`, + DE: `${helpRoot}de/microsoft-collaborative-solutions/verwaltung_einer_office_365_reseller_lizenzgruppe_csp2/`, + ES: `${helpRoot}es/microsoft-collaborative-solutions/contratar-y-gestionar-un-grupo-de-licencias-office-365-revendedor-csp2-ovh/`, + IE: `${helpRoot}ie/en/microsoft-collaborative-solutions/order-and-manage-a-group-of-ovh-office-365-csp2-reseller-licences/`, + IT: `${helpRoot}it/microsoft-collaborative-solutions/ordina_e_gestisci_un_gruppo_di_licenze_office_365_reseller_ovh_csp2/`, + PL: `${helpRoot}pl/microsoft-collaborative-solutions/zarzadzanie-licencje-office-365-reseller-csp2/`, + PT: `${helpRoot}pt/microsoft-collaborative-solutions/encomendar_e_gerir_um_grupo_de_licencas_office_365_revendedor_csp2_ovh/`, + GB: `${helpRoot}en/microsoft-collaborative-solutions/order-and-manage-a-group-of-ovh-office-365-csp2-reseller-licences/`, + DEFAULT: `${helpRoot}gb/en/microsoft-collaborative-solutions/order-and-manage-a-group-of-ovh-office-365-csp2-reseller-licences/`, +}; + +export const WEB_OFFICE_ONBOARDING_3: GuideLinks = { + DEFAULT: `${helpRoot}gb/en/microsoft-collaborative-solutions/office365-proplus-remote-desktop/`, + DE: `${helpRoot}de/microsoft-collaborative-solutions/office365-proplus-remotedesktopdienste/`, + ES: `${helpRoot}es/microsoft-collaborative-solutions/office365-proplus-escritorio-remoto/`, + IE: `${helpRoot}ie/en/microsoft-collaborative-solutions/office365-proplus-remote-desktop/`, + IT: `${helpRoot}it/microsoft-collaborative-solutions/office365-proplus-desktop-remoto/`, + PL: `${helpRoot}pl/microsoft-collaborative-solutions/office365-proplus-biuro-a-zdalne/`, + PT: `${helpRoot}pt/microsoft-collaborative-solutions/office365-proplus-escritório-a-distância/`, + GB: `${helpRoot}gb/en/microsoft-collaborative-solutions/office365-proplus-remote-desktop/`, + FR: `${helpRoot}fr/microsoft-collaborative-solutions/office365-proplus-bureau-a-distance/`, +}; + +export const CTAS: Record = { + DEFAULT: 'https://ovhcloud.com/en/collaborative-tools/microsoft-365/', + ASIA: 'https://ovhcloud.com/asia/collaborative-tools/microsoft-365/', + IN: 'https://www.ovhcloud.com/en-in/collaborative-tools/microsoft-365/', + DE: 'https://ovhcloud.com/de/collaborative-tools/microsoft-365/', + ES: 'https://ovhcloud.com/es-es/collaborative-tools/microsoft-365/', + IE: 'https://ovhcloud.com/en-ie/collaborative-tools/microsoft-365/', + IT: 'https://ovhcloud.com/it/collaborative-tools/microsoft-365/', + NL: 'https://ovhcloud.com/nl/collaborative-tools/microsoft-365/', + PL: 'https://ovhcloud.com/pl/collaborative-tools/microsoft-365/', + PT: 'https://ovhcloud.com/pt/collaborative-tools/microsoft-365/', + GB: 'https://ovhcloud.com/en-gb/collaborative-tools/microsoft-365/', + CA: 'https://ovhcloud.com/en-ca/collaborative-tools/microsoft-365/', + QC: 'https://ovhcloud.com/fr-ca/collaborative-tools/microsoft-365/', + MA: 'https://ovhcloud.com/fr-ma/collaborative-tools/microsoft-365/', + SN: 'https://ovhcloud.com/fr-sn/collaborative-tools/microsoft-365/', + TN: 'https://ovhcloud.com/fr-tn/collaborative-tools/microsoft-365/', + AU: 'https://ovhcloud.com/en-au/collaborative-tools/microsoft-365/', + SG: 'https://ovhcloud.com/en-sg/collaborative-tools/microsoft-365/', + FR: 'https://ovhcloud.com/fr/collaborative-tools/microsoft-365/', + CZ: 'https://ovhcloud.com/cz-cs/collaborative-tools/microsoft-365/', + FI: 'https://ovhcloud.com/fi/collaborative-tools/microsoft-365/', + LT: 'https://ovhcloud.com/lt/collaborative-tools/microsoft-365/', + WE: 'https://ovhcloud.com/us-en/collaborative-tools/microsoft-365/', + WS: 'https://ovhcloud.com/us-en/collaborative-tools/microsoft-365/', +}; diff --git a/packages/manager/apps/web-office/src/hooks/breadcrumb/useBreadcrumb.tsx b/packages/manager/apps/web-office/src/hooks/breadcrumb/useBreadcrumb.tsx deleted file mode 100644 index d61852b056a4..000000000000 --- a/packages/manager/apps/web-office/src/hooks/breadcrumb/useBreadcrumb.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState, useContext } from 'react'; -import { useLocation } from 'react-router-dom'; -import { ShellContext } from '@ovh-ux/manager-react-shell-client'; - -export type BreadcrumbItem = { - label: string | undefined; - href?: string; -}; - -export interface BreadcrumbProps { - rootLabel?: string; - appName?: string; - projectId?: string; - items?: BreadcrumbItem[]; -} - -export const useBreadcrumb = ({ rootLabel, appName }: BreadcrumbProps) => { - const { shell } = useContext(ShellContext); - const [root, setRoot] = useState([]); - const [paths, setPaths] = useState([]); - const location = useLocation(); - const pathnames = location.pathname.split('/').filter((x) => x); - - useEffect(() => { - const fetchRoot = async () => { - try { - const response = await shell?.navigation.getURL(appName, '#/', {}); - const rootItem = { - label: rootLabel, - href: String(response), - }; - setRoot([rootItem]); - } catch { - // Fetch navigation error - } - }; - fetchRoot(); - }, [rootLabel, appName, shell?.navigation]); - - useEffect(() => { - const pathsTab = pathnames.map((value) => ({ - label: value, - href: `/#/${appName}/${value}`, - })); - setPaths(pathsTab); - }, [location]); - - return [...root, ...paths]; -}; diff --git a/packages/manager/apps/web-office/src/hooks/guide/useGuideUtils.tsx b/packages/manager/apps/web-office/src/hooks/guide/useGuideUtils.tsx deleted file mode 100644 index 2971e624318e..000000000000 --- a/packages/manager/apps/web-office/src/hooks/guide/useGuideUtils.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; -import { CountryCode } from '@ovh-ux/manager-config'; -import { ShellContext } from '@ovh-ux/manager-react-shell-client'; - -const docUrl = 'https://docs.ovh.com'; - -type GuideLinks = { [key in CountryCode]: string }; - -const GUIDE_LIST: { [guideName: string]: Partial } = { - guideLink1: { - DE: '/update-path', - ES: '/update-path', - IE: '/en/update-path', - IT: '/update-path', - PL: '/update-path', - PT: '/update-path', - FR: '/update-path', - GB: '/update-path', - CA: '/update-path', - QC: '/update-path', - WE: '/update-path', - WS: '/update-path', - US: '/update-path', - }, - guideLink2: { - DE: '/guide-link-2-path', - ES: '/guide-link-2-path', - IE: '/en/guide-link-2-path', - IT: '/guide-link-2-path', - PL: '/guide-link-2-path', - PT: '/guide-link-2-path', - FR: '/guide-link-2-path', - GB: '/guide-link-2-path', - CA: '/update-path', - QC: '/update-path', - WE: '/update-path', - WS: '/update-path', - US: '/update-path', - }, - guideLink3: { - DE: '/guide-link-3-path', - ES: '/guide-link-3-path', - IE: '/en/guide-link-3-path', - IT: '/guide-link-3-path', - PL: '/guide-link-3-path', - PT: '/guide-link-3-path', - FR: '/guide-link-3-path', - GB: '/guide-link-3-path', - CA: '/update-path', - QC: '/update-path', - WE: '/update-path', - WS: '/update-path', - US: '/update-path', - }, - /* - addNewGuideLink : { - DEFAULT: '/guide-link-3-path', - DE: '/guide-link-3-path', - ES: '/guide-link-3-path', - ... - } - */ -}; - -type GetGuideLinkProps = { - name?: string; - subsidiary: CountryCode | string; -}; - -function getGuideListLink({ subsidiary }: GetGuideLinkProps) { - const list: { [guideName: string]: string } = {}; - const keys = Object.entries(GUIDE_LIST); - keys.forEach((key) => { - list[key[0]] = docUrl + GUIDE_LIST[key[0]][subsidiary as CountryCode]; - }); - return list; -} - -interface GuideLinkProps { - [guideName: string]: string; -} - -function useGuideUtils() { - const { shell } = useContext(ShellContext); - const { environment } = shell; - const [list, setList] = useState({}); - - useEffect(() => { - const getSubSidiary = async () => { - const env = await environment.getEnvironment(); - const { ovhSubsidiary } = env.getUser(); - const guideList = getGuideListLink({ subsidiary: ovhSubsidiary }); - setList(guideList); - }; - getSubSidiary(); - }, []); - return list as GuideLinkProps; -} - -export default useGuideUtils; diff --git a/packages/manager/apps/web-office/src/hooks/index.ts b/packages/manager/apps/web-office/src/hooks/index.ts new file mode 100644 index 000000000000..6aeb20961971 --- /dev/null +++ b/packages/manager/apps/web-office/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useOfficeLicenses'; +export * from './useOfficeLicenseDetails'; +export * from './useGenerateUrl'; diff --git a/packages/manager/apps/web-office/src/hooks/useGenerateUrl.ts b/packages/manager/apps/web-office/src/hooks/useGenerateUrl.ts new file mode 100644 index 000000000000..31abffed28a4 --- /dev/null +++ b/packages/manager/apps/web-office/src/hooks/useGenerateUrl.ts @@ -0,0 +1,32 @@ +import { useHref } from 'react-router-dom'; +import { useOfficeLicenseDetail } from '@/hooks'; + +export const UseGenerateUrl = ( + baseURL: string, + type: 'path' | 'href' = 'path', + params?: Record, +) => { + const { data: serviceName } = useOfficeLicenseDetail(); + + const URL = baseURL.replace( + ':serviceName', + (params?.serviceName as string) || '', + ); + + const queryParams = { + ...params, + ...(serviceName && { serviceNameDetail: serviceName }), + }; + + const queryString = Object.entries(queryParams) + .filter(([key]) => key !== 'serviceName') + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&'); + + const fullURL = queryString ? `${URL}?${queryString}` : URL; + + if (type === 'href') { + return useHref(fullURL); + } + return fullURL; +}; diff --git a/packages/manager/apps/web-office/src/hooks/useOfficeLicenseDetails.ts b/packages/manager/apps/web-office/src/hooks/useOfficeLicenseDetails.ts new file mode 100644 index 000000000000..895995c467dc --- /dev/null +++ b/packages/manager/apps/web-office/src/hooks/useOfficeLicenseDetails.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import { useSearchParams } from 'react-router-dom'; +import { + getOfficeLicenseDetails, + getlicenseOfficeServiceQueryKey, +} from '@/api/license/api'; + +export const useOfficeLicenseDetail = ( + serviceName?: string, + noCache = false, +) => { + const [searchParams] = useSearchParams(); + const selectedServiceName = searchParams.get('serviceName'); + + return useQuery({ + queryKey: getlicenseOfficeServiceQueryKey({ + serviceName: serviceName || selectedServiceName, + }), + queryFn: () => + getOfficeLicenseDetails(serviceName || selectedServiceName || ''), + enabled: Boolean(serviceName || selectedServiceName), + gcTime: noCache ? 0 : 5000, + }); +}; diff --git a/packages/manager/apps/web-office/src/hooks/useOfficeLicenses.ts b/packages/manager/apps/web-office/src/hooks/useOfficeLicenses.ts new file mode 100644 index 000000000000..a07bd9b712aa --- /dev/null +++ b/packages/manager/apps/web-office/src/hooks/useOfficeLicenses.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; +import { aapi } from '@ovh-ux/manager-core-api'; +import { LicenseType } from '@/api/license/type'; +import { getOfficeLicenseQueryKey } from '@/api/license/key'; + +const getOfficeLicenses = async (): Promise => { + const response = await aapi.get('service', { + params: { + external: false, + type: '/license/office', + }, + }); + return response.data; +}; + +export const useOfficeLicenses = () => { + return useQuery({ + queryKey: [getOfficeLicenseQueryKey], + queryFn: getOfficeLicenses, + }); +}; diff --git a/packages/manager/apps/web-office/src/index.scss b/packages/manager/apps/web-office/src/index.scss index 65dd5f63a7df..6ecdfbab6175 100644 --- a/packages/manager/apps/web-office/src/index.scss +++ b/packages/manager/apps/web-office/src/index.scss @@ -1 +1,3 @@ @tailwind utilities; + +@import '@ovhcloud/ods-themes/default'; diff --git a/packages/manager/apps/web-office/src/index.tsx b/packages/manager/apps/web-office/src/index.tsx index 400db4581f14..10bed59b1305 100644 --- a/packages/manager/apps/web-office/src/index.tsx +++ b/packages/manager/apps/web-office/src/index.tsx @@ -6,11 +6,10 @@ import { initI18n, } from '@ovh-ux/manager-react-shell-client'; import App from './App'; -import '@ovhcloud/ods-theme-blue-jeans/dist/index.css'; +import '@ovhcloud/ods-themes/default'; import './index.scss'; import './vite-hmr'; - -import { UNIVERSE, SUB_UNIVERSE, APP_NAME, LEVEL2 } from './tracking.constant'; +import { UNIVERSE, SUB_UNIVERSE, APP_NAME, LEVEL2 } from './tracking.constants'; const trackingContext = { chapter1: UNIVERSE, @@ -48,4 +47,4 @@ const init = async (appName: string) => { ); }; -init('web-office-365'); +init('web-office'); diff --git a/packages/manager/apps/web-office/src/pages/dashboard/index.tsx b/packages/manager/apps/web-office/src/pages/dashboard/index.tsx index 76d50b57cd58..2ccaddc210a3 100644 --- a/packages/manager/apps/web-office/src/pages/dashboard/index.tsx +++ b/packages/manager/apps/web-office/src/pages/dashboard/index.tsx @@ -1,22 +1,10 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Outlet, - NavLink, - useLocation, - useNavigate, - useParams, - useResolvedPath, -} from 'react-router-dom'; -import { - OsdsTabs, - OsdsTabBar, - OsdsTabBarItem, -} from '@ovhcloud/ods-components/react'; +import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'; import { BaseLayout } from '@ovh-ux/manager-react-components'; -import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; +import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; export type DashboardTabItemProps = { name: string; @@ -35,54 +23,15 @@ export default function DashboardPage() { const navigate = useNavigate(); const { t } = useTranslation('dashboard'); - const tabsList = [ - { - name: 'general_informations', - title: 'Informations générales', - to: useResolvedPath('').pathname, - }, - { - name: 'Tab 2', - title: 'Tab 2', - to: useResolvedPath('Tab2').pathname, - }, - ]; - - useEffect(() => { - const activeTab = tabsList.find((tab) => tab.to === location.pathname); - if (activeTab) { - setActivePanel(activeTab.name); - } else { - setActivePanel(tabsList[0].name); - navigate(`${tabsList[0].to}`); - } - }, [location.pathname]); - const header = { - title: t('title'), + title: serviceName, }; return ( } header={header} - description="Description du web-office-365" - tabs={ - - - {tabsList.map((tab: DashboardTabItemProps) => ( - - - {tab.title} - - - ))} - - - } + description="Description du web-office" > diff --git a/packages/manager/apps/web-office/src/pages/index.tsx b/packages/manager/apps/web-office/src/pages/index.tsx index d1840bebd12c..b6b45e01489f 100644 --- a/packages/manager/apps/web-office/src/pages/index.tsx +++ b/packages/manager/apps/web-office/src/pages/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -export default function WebOffice_365() { - const { t } = useTranslation('web-office-365'); +export default function WebOffice() { + const { t } = useTranslation('web-office'); return (
diff --git a/packages/manager/apps/web-office/src/pages/layout.tsx b/packages/manager/apps/web-office/src/pages/layout.tsx index aec6ed0536c2..c724a59fbea1 100644 --- a/packages/manager/apps/web-office/src/pages/layout.tsx +++ b/packages/manager/apps/web-office/src/pages/layout.tsx @@ -1,31 +1,34 @@ -import React, { useEffect, useContext } from 'react'; -import { defineCurrentPage } from '@ovh-ux/request-tagger'; -import { Outlet, useLocation, useMatches } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { - useOvhTracking, useRouteSynchro, - ShellContext, + useRouting, } from '@ovh-ux/manager-react-shell-client'; +import { useOfficeLicenses } from '@/hooks'; +import Loading from '@/components/Loading/Loading'; export default function Layout() { const location = useLocation(); - const { shell } = useContext(ShellContext); - const matches = useMatches(); - const { trackCurrentPage } = useOvhTracking(); + + const routing = useRouting(); + useRouteSynchro(); - useEffect(() => { - const match = matches.slice(-1); - defineCurrentPage(`app.web-office-365-${match[0]?.id}`); - }, [location]); + const { data, isLoading } = useOfficeLicenses(); useEffect(() => { - trackCurrentPage(); + routing.onHashChange(); }, [location]); - useEffect(() => { - shell.ux.hidePreloader(); - }, []); - - return ; + useRouteSynchro(); + return ( + <> + + {isLoading && } + {!data && !isLoading && } + {data && location.pathname === '/' && location.search === '' && ( + + )} + + ); } diff --git a/packages/manager/apps/web-office/src/pages/licences/licences.page.tsx b/packages/manager/apps/web-office/src/pages/licences/licences.page.tsx new file mode 100644 index 000000000000..66b4a0349310 --- /dev/null +++ b/packages/manager/apps/web-office/src/pages/licences/licences.page.tsx @@ -0,0 +1,166 @@ +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Datagrid, + DatagridColumn, + BaseLayout, + OvhSubsidiary, + Links, + useDatagridSearchParams, +} from '@ovh-ux/manager-react-components'; + +import { + ODS_BUTTON_COLOR, + ODS_BUTTON_VARIANT, + ODS_TEXT_PRESET, +} from '@ovhcloud/ods-components'; +import { OdsButton, OdsText } from '@ovhcloud/ods-components/react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; +import { CTAS } from '@/guides.constants'; +import { urls } from '@/routes/routes.constants'; +import { LicenseType } from '@/api/license/type'; +import { useOfficeLicenses } from '@/hooks'; +import { UseGenerateUrl } from '@/hooks/useGenerateUrl'; +import Loading from '@/components/Loading/Loading'; + +const columns: DatagridColumn[] = [ + { + id: 'serviceName', + cell: (item) => { + const href = UseGenerateUrl(urls.dashboard, 'href', { + serviceName: item.serviceName, + }); + + return ; + }, + label: 'microsoft_office_licenses_servicename', + isSortable: true, + }, + { + id: 'displayName', + cell: (item) => ( + {item.displayName} + ), + label: 'microsoft_office_licenses_displayName', + isSortable: true, + }, + { + id: 'address', + cell: (item) => ( + {item.address} + ), + label: 'microsoft_office_licenses_address', + isSortable: true, + }, + { + id: 'city', + cell: (item) => ( + {item.city} + ), + label: 'microsoft_office_licenses_city', + isSortable: true, + }, + { + id: 'firstname', + cell: (item) => ( + {item.firstName} + ), + label: 'microsoft_office_licenses_firstname', + isSortable: true, + }, + { + id: 'lastname', + cell: (item) => ( + {item.lastName} + ), + label: 'microsoft_office_licenses_lastname', + isSortable: true, + }, + { + id: 'serviceType', + cell: (item) => ( + {item.serviceType} + ), + label: 'microsoft_office_licenses_servicetype', + isSortable: true, + }, + { + id: 'status', + cell: (item) => ( + {item.status} + ), + label: 'microsoft_office_licenses_status', + isSortable: true, + }, + { + id: 'zipCode', + cell: (item) => ( + {item.zipCode} + ), + label: 'microsoft_office_licenses_zipcode', + isSortable: true, + }, +]; + +export default function Licenses() { + const { t } = useTranslation('licenses'); + const { data, isLoading } = useOfficeLicenses(); + const { sorting, setSorting } = useDatagridSearchParams(); + + const context = useContext(ShellContext); + const { ovhSubsidiary } = context.environment.getUser(); + + const goOrder = () => { + const url = CTAS[ovhSubsidiary as OvhSubsidiary] || CTAS.DEFAULT; + window.open(url, '_blank'); + }; + + const header = { + title: t('microsoft_office_licenses_title'), + }; + const sortedData = React.useMemo(() => { + if (!data || data.length === 0 || !sorting) return data; + + const sorted = [...data]; + const { id, desc } = sorting; + + sorted.sort((a, b) => { + if (a[id] < b[id]) return desc ? 1 : -1; + if (a[id] > b[id]) return desc ? -1 : 1; + return 0; + }); + + return sorted; + }, [data, sorting]); + if (isLoading) { + return ; + } + + return ( + +
+ +
+ {columns && ( + ({ + ...column, + label: t(column.label), + }))} + items={sortedData || []} + totalItems={sortedData?.length || 0} + sorting={sorting} + onSortChange={setSorting} + className="mt-4" + /> + )} +
+ ); +} diff --git a/packages/manager/apps/web-office/src/pages/licences/licences.spec.tsx b/packages/manager/apps/web-office/src/pages/licences/licences.spec.tsx new file mode 100644 index 000000000000..3f0c7b172fd1 --- /dev/null +++ b/packages/manager/apps/web-office/src/pages/licences/licences.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { render, screen } from '@/utils/test.provider'; +import Licenses from './licences.page'; + +describe('Licenses Page', () => { + it('should render page with content', async () => { + render(); + + const orderButton = screen.getByTestId('licenses-order-button'); + + expect(orderButton).toBeInTheDocument(); + + expect(orderButton).toHaveAttribute('label', 'Commander'); + + expect(orderButton).toHaveAttribute('color', 'primary'); + expect(orderButton).toHaveAttribute('variant', 'outline'); + const sortedRows = screen.getAllByTestId('header-serviceName'); + expect(sortedRows[0]).toHaveTextContent('Nom du service'); + }); +}); diff --git a/packages/manager/apps/web-office/src/pages/listing/index.tsx b/packages/manager/apps/web-office/src/pages/listing/index.tsx deleted file mode 100644 index 682093eb882f..000000000000 --- a/packages/manager/apps/web-office/src/pages/listing/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; - -import { OsdsLink } from '@ovhcloud/ods-components/react'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { - Datagrid, - DataGridTextCell, - useResourcesIcebergV6, - dataType, - BaseLayout, -} from '@ovh-ux/manager-react-components'; - -import Loading from '@/components/Loading/Loading'; -import ErrorBanner from '@/components/Error/Error'; -import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; - -import appConfig from '@/web-office-365.config'; - -export default function Listing() { - const myConfig = appConfig; - const serviceKey = myConfig.listing?.datagrid?.serviceKey; - const [columns, setColumns] = useState([]); - const navigate = useNavigate(); - const location = useLocation(); - const { t } = useTranslation('listing'); - const { - flattenData, - isError, - error, - totalCount, - hasNextPage, - fetchNextPage, - isLoading, - status, - sorting, - setSorting, - pageIndex, - } = useResourcesIcebergV6({ - route: `/license/office`, - queryKey: ['web-office-365', `/license/office`], - }); - - const navigateToDashboard = (label: string) => { - const path = - location.pathname.indexOf('pci') > -1 ? `${location.pathname}/` : '/'; - navigate(`${path}${label}`); - }; - - useEffect(() => { - if (columns && status === 'success' && flattenData?.length > 0) { - const newColumns = Object.keys(flattenData[0]) - .filter((element) => element !== 'iam') - .map((element) => ({ - id: element, - label: element, - type: dataType(flattenData[0]), - cell: (props: any) => { - const label = props[element] as string; - if (typeof label === 'string' || typeof label === 'number') { - if (serviceKey === element) - return ( - - navigateToDashboard(label)} - > - {label} - - - ); - return {label}; - } - return
-
; - }, - })); - setColumns(newColumns); - } - }, [flattenData]); - - if (isError) { - return ; - } - - if (isLoading && pageIndex === 1) { - return ( -
- -
- ); - } - - const header = { - title: t('title'), - }; - - return ( - } header={header}> - - {columns && flattenData && ( - - )} - - - ); -} diff --git a/packages/manager/apps/web-office/src/pages/onboarding/index.tsx b/packages/manager/apps/web-office/src/pages/onboarding/index.tsx index 93e59c16a128..2a7e699af333 100644 --- a/packages/manager/apps/web-office/src/pages/onboarding/index.tsx +++ b/packages/manager/apps/web-office/src/pages/onboarding/index.tsx @@ -1,44 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components'; -import useGuideUtils from '@/hooks/guide/useGuideUtils'; -import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; +import { OnboardingLayout } from '@ovh-ux/manager-react-components'; +import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import onboardingImgSrc from './onboarding-img.png'; export default function Onboarding() { const { t } = useTranslation('onboarding'); - const link = useGuideUtils(); - - const tileList = [ - { - id: 1, - texts: { - title: t('guide1Title'), - description: t('guide1Description'), - category: t('guideCategory'), - }, - href: link?.guideLink1, - }, - { - id: 2, - texts: { - title: t('guide2Title'), - description: t('guide2Description'), - category: t('guideCategory'), - }, - href: link?.guideLink2, - }, - { - id: 3, - texts: { - title: t('guide3Title'), - description: t('guide3Description'), - category: t('guideCategory'), - }, - href: link?.guideLink3, - }, - ]; - const title: string = t('title'); const description: string = t('description'); const imgSrc = { @@ -56,11 +23,7 @@ export default function Onboarding() { orderHref={t('orderButtonLink')} moreInfoButtonLabel={t('moreInfoButtonLabel')} moreInfoHref={t('moreInfoButtonLink')} - > - {tileList.map((tile) => ( - - ))} - + /> ); } diff --git a/packages/manager/apps/web-office/src/routes/routes.constant.ts b/packages/manager/apps/web-office/src/routes/routes.constant.ts deleted file mode 100644 index 8fcb96e6009d..000000000000 --- a/packages/manager/apps/web-office/src/routes/routes.constant.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const urls = { - root: '/', - onboarding: '/onboarding', - listing: '/', - dashboard: '/:serviceName', - tab2: 'Tab2', -}; diff --git a/packages/manager/apps/web-office/src/routes/routes.constants.ts b/packages/manager/apps/web-office/src/routes/routes.constants.ts new file mode 100644 index 000000000000..80fe50be839e --- /dev/null +++ b/packages/manager/apps/web-office/src/routes/routes.constants.ts @@ -0,0 +1,8 @@ +export const urls = { + root: '', + onboarding: '/onboarding', + listing: '/', + dashboard: '/license/:serviceName', + tab2: '/Tab2', + license: '/license', +}; diff --git a/packages/manager/apps/web-office/src/routes/routes.tsx b/packages/manager/apps/web-office/src/routes/routes.tsx index 7cdcd5e49041..c17d7f2272b8 100644 --- a/packages/manager/apps/web-office/src/routes/routes.tsx +++ b/packages/manager/apps/web-office/src/routes/routes.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { RouteObject } from 'react-router-dom'; +import { RouteObject, Navigate } from 'react-router-dom'; import { PageType } from '@ovh-ux/manager-react-shell-client'; import NotFound from '@/pages/404'; -import { urls } from '@/routes/routes.constant'; +import { urls } from '@/routes/routes.constants'; const lazyRouteConfig = (importFn: CallableFunction): Partial => { return { @@ -18,63 +18,60 @@ const lazyRouteConfig = (importFn: CallableFunction): Partial => { export const Routes: any = [ { - path: urls.root, - ...lazyRouteConfig(() => import('@/pages/layout')), + path: urls.listing, + element: , + }, + { + path: urls.license, + ...lazyRouteConfig(() => import('@/pages/licences/licences.page')), + handle: { + tracking: { + pageName: 'licenses', + pageType: PageType.listing, + }, + }, + }, + { + path: urls.dashboard, + ...lazyRouteConfig(() => import('@/pages/dashboard')), children: [ { - id: 'listing', - path: urls.listing, - ...lazyRouteConfig(() => import('@/pages/listing')), + id: 'dashboard', + path: '', + ...lazyRouteConfig(() => + import('@/pages/dashboard/general-informations'), + ), handle: { tracking: { - pageName: 'listing', - pageType: PageType.listing, + pageName: 'dashboard', + pageType: PageType.dashboard, }, }, }, { - path: urls.dashboard, - ...lazyRouteConfig(() => import('@/pages/dashboard')), - children: [ - { - id: 'dashboard', - path: '', - ...lazyRouteConfig(() => - import('@/pages/dashboard/general-informations'), - ), - handle: { - tracking: { - pageName: 'dashboard', - pageType: PageType.dashboard, - }, - }, - }, - { - id: 'dashboard.tab2', - path: 'Tab2', - ...lazyRouteConfig(() => import('@/pages/dashboard/tab2')), - handle: { - tracking: { - pageName: 'tab2', - pageType: PageType.dashboard, - }, - }, - }, - ], - }, - { - id: 'onboarding', - path: urls.onboarding, - ...lazyRouteConfig(() => import('@/pages/onboarding')), + id: 'dashboard.tab2', + path: 'Tab2', + ...lazyRouteConfig(() => import('@/pages/dashboard/tab2')), handle: { tracking: { - pageName: 'onboarding', - pageType: PageType.onboarding, + pageName: 'tab2', + pageType: PageType.dashboard, }, }, }, ], }, + { + id: 'onboarding', + path: urls.onboarding, + ...lazyRouteConfig(() => import('@/pages/onboarding')), + handle: { + tracking: { + pageName: 'onboarding', + pageType: PageType.onboarding, + }, + }, + }, { path: '*', element: , diff --git a/packages/manager/apps/web-office/src/tracking.constant.ts b/packages/manager/apps/web-office/src/tracking.constants.ts similarity index 86% rename from packages/manager/apps/web-office/src/tracking.constant.ts rename to packages/manager/apps/web-office/src/tracking.constants.ts index 1aaca5cd7615..feb410a9890d 100644 --- a/packages/manager/apps/web-office/src/tracking.constant.ts +++ b/packages/manager/apps/web-office/src/tracking.constants.ts @@ -17,4 +17,4 @@ export const LEVEL2 = { }; export const UNIVERSE = 'WebCloud'; export const SUB_UNIVERSE = 'Web'; -export const APP_NAME = 'web-office-365'; +export const APP_NAME = 'web-office'; diff --git a/packages/manager/apps/web-office/src/utils/test.provider.tsx b/packages/manager/apps/web-office/src/utils/test.provider.tsx new file mode 100644 index 000000000000..758d261c16a7 --- /dev/null +++ b/packages/manager/apps/web-office/src/utils/test.provider.tsx @@ -0,0 +1,97 @@ +import { render, RenderOptions, RenderResult } from '@testing-library/react'; +import i18n from 'i18next'; +import React, { ComponentType } from 'react'; +import { I18nextProvider, initReactI18next } from 'react-i18next'; +import { MemoryRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + ShellContext, + ShellContextType, +} from '@ovh-ux/manager-react-shell-client'; +import userEvent from '@testing-library/user-event'; +import dashboardTranslation from '@/public/translations/dashboard/Messages_fr_FR.json'; +import licensesTranslation from '@/public/translations/licenses/Messages_fr_FR.json'; +import '@testing-library/jest-dom'; +import 'element-internals-polyfill'; + +i18n.use(initReactI18next).init({ + lng: 'fr', + fallbackLng: 'fr', + resources: { + fr: { + dashboard: dashboardTranslation, + licenses: licensesTranslation, + }, + }, + ns: ['dashboard'], +}); + +export const getShellContext = () => { + return { + environment: { + getUser: () => ({ + ovhSubsidiary: 'FR', + }), + getRegion: () => 'EU', + getUserLocale: () => 'fr_FR', + }, + shell: { + routing: { + onHashChange: () => undefined, + stopListenForHashChange: () => undefined, + listenForHashChange: () => undefined, + }, + }, + } as ShellContextType; +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +export const wrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + ); +}; + +export const wrapperWithI18n = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + + + {children} + + + + ); +}; + +const customRender = ( + ui: React.JSX.Element, + options?: Omit, +): RenderResult => + render(ui, { wrapper: wrapperWithI18n as ComponentType, ...options }); + +// We should look into using that +// https://testing-library.com/docs/user-event/intro +export function setup(jsx: React.ReactElement): unknown { + return { + user: userEvent.setup(), + ...customRender(jsx), + }; +} +export * from '@testing-library/react'; +export { setup as render }; diff --git a/packages/manager/apps/web-office/src/utils/test.setup.tsx b/packages/manager/apps/web-office/src/utils/test.setup.tsx new file mode 100644 index 000000000000..e5fde1abf671 --- /dev/null +++ b/packages/manager/apps/web-office/src/utils/test.setup.tsx @@ -0,0 +1,93 @@ +import { vi } from 'vitest'; +import { licensesMock } from '@/api/_mock_/license'; +import { useOfficeLicenses } from '@/hooks'; + +const mocksAxios = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), +})); + +vi.mock('axios', async (importActual) => { + const actual = await importActual(); + + const mockAxios = { + default: { + ...actual.default, + get: mocksAxios.get, + post: mocksAxios.post, + put: mocksAxios.put, + delete: mocksAxios.delete, + create: vi.fn(() => ({ + ...actual.default.create(), + get: mocksAxios.get, + post: mocksAxios.post, + put: mocksAxios.put, + delete: mocksAxios.delete, + })), + }, + }; + + return mockAxios; +}); + +vi.mock('@/hooks', async (importActual) => { + return { + ...(await importActual()), + useGenerateUrl: vi.fn(), + useOfficeLicenses: vi.fn(() => ({ + data: licensesMock, + + totalCount: licensesMock.length, + isLoading: false, + sorting: {}, + setSorting: vi.fn(), + pageIndex: 1, + })), + }; +}); + +vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { + const actual = await importOriginal< + typeof import('@ovh-ux/manager-react-components') + >(); + + return { + ...actual, + useResourcesIcebergV6: vi.fn(() => ({ + flattenData: licensesMock, + isError: false, + error: null, + totalCount: licensesMock.length, + hasNextPage: false, + fetchNextPage: vi.fn(), + isLoading: false, + sorting: {}, + setSorting: vi.fn(), + pageIndex: 1, + })), + }; +}); + +export const navigate = vi.fn(); + +vi.mock('react-router-dom', async (importActual) => { + return { + ...(await importActual()), + useLocation: vi.fn(() => ({ + pathname: '', + search: '', + })), + useResolvedPath: vi.fn(() => ({ + pathname: '', + })), + useNavigate: vi.fn(() => navigate), + useSearchParams: vi.fn(() => [new URLSearchParams(), vi.fn()]), + useMatches: vi.fn(() => []), + }; +}); + +afterEach(() => { + vi.clearAllMocks(); +}); diff --git a/packages/manager/apps/web-office/src/web-office-365.config.ts b/packages/manager/apps/web-office/src/web-office.config.ts similarity index 75% rename from packages/manager/apps/web-office/src/web-office-365.config.ts rename to packages/manager/apps/web-office/src/web-office.config.ts index cc27079385a0..1e5b11b31184 100644 --- a/packages/manager/apps/web-office/src/web-office-365.config.ts +++ b/packages/manager/apps/web-office/src/web-office.config.ts @@ -4,5 +4,5 @@ export default { serviceKey: 'serviceName', }, }, - rootLabel: 'web-office-365', + rootLabel: 'web-office', }; diff --git a/packages/manager/apps/web-office/tsconfig.json b/packages/manager/apps/web-office/tsconfig.json index e2104f471575..478190f4b45e 100644 --- a/packages/manager/apps/web-office/tsconfig.json +++ b/packages/manager/apps/web-office/tsconfig.json @@ -19,9 +19,16 @@ "jsx": "react", "baseUrl": ".", "paths": { + "@/public/*": ["./public/*"], "@/*": ["./src/*"] } }, "include": ["src"], - "exclude": ["node_modules", "dist", "types", "src/__tests__"] + "exclude": [ + "node_modules", + "dist", + "types", + "src/**/*.spec.ts", + "src/**/*.spec.tsx" + ] } diff --git a/packages/manager/apps/web-office/vitest.config.js b/packages/manager/apps/web-office/vitest.config.js new file mode 100644 index 000000000000..b0257231e9d7 --- /dev/null +++ b/packages/manager/apps/web-office/vitest.config.js @@ -0,0 +1,37 @@ +import path from 'path'; +import { defineConfig, coverageConfigDefaults } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + coverage: { + include: ['src'], + exclude: [ + 'src/configInterface.ts', + 'src/api', + 'src/web-office.config.ts', + 'src/vite-*.ts', + 'src/App.tsx', + 'src/hooks/index.ts', + 'src/hooks/types.ts', + 'src/index.tsx', + 'src/routes/routes.tsx', + 'src/utils/index.ts', + 'src/**/*constants.ts', + ...coverageConfigDefaults.exclude, + ], + }, + setupFiles: ['src/utils/test.setup.tsx'], + }, + resolve: { + alias: { + '@/public': path.resolve(__dirname, 'public'), + '@': path.resolve(__dirname, 'src'), + }, + mainFields: ['module'], + }, +}); diff --git a/yarn.lock b/yarn.lock index 8ca769919c16..5fd6670f8d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5933,6 +5933,23 @@ resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz#6d2f812e3b19545bba2d81caffff1204de9a6a58" integrity sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ== +"@ovh-ux/manager-react-components@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-components/-/manager-react-components-2.2.0.tgz#08d506926d23aae28cc6e69583b77e41d72c8899" + integrity sha512-d08/fkmaILeSb4BKmq253EzG3d3OxzIFB4hG3u/OcoqMo3blkR4ScWAFY6yuokA9GyFlL0o4YAdtIZ0KEfI4zw== + dependencies: + "@babel/plugin-proposal-private-property-in-object" "^7.21.11" + "@tanstack/react-query" "^5.51.21" + "@tanstack/react-table" "^8.20.1" + clsx "^2.1.1" + lodash.isdate "^4.0.1" + lodash.isequal "^4.5.0" + react-i18next "^14.0.5" + react-use "^17.5.0" + sass "1.71.0" + tailwindcss "^3.4.4" + uuid "^9.0.1" + "@ovh-ux/manager-react-components@^1.41.1": version "1.41.1" resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-components/-/manager-react-components-1.41.1.tgz#da0195c82fdf26e101f47c11d5b604c90547d810" @@ -5993,7 +6010,7 @@ tailwindcss "^3.4.4" uuid "^9.0.1" -"@ovh-ux/manager-react-shell-client@^0.8.2": +"@ovh-ux/manager-react-shell-client@^0.8.1", "@ovh-ux/manager-react-shell-client@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-shell-client/-/manager-react-shell-client-0.8.2.tgz#b2e1209777126327af37193ec7d9e15593362605" integrity sha512-/uZ991MatKii/bfVzcaaUZNlrQCHhST8ZiaoSH3qcDBXUf6qrNzHDt1aCGjaZG7aEHESWpE9cyUJcJyWcTTYEA==