diff --git a/packages/manager/apps/catalog/.gitignore b/packages/manager/apps/catalog/.gitignore new file mode 100644 index 000000000000..e06831cfdeb4 --- /dev/null +++ b/packages/manager/apps/catalog/.gitignore @@ -0,0 +1,2 @@ +e2e/reports +e2e/coverage diff --git a/packages/manager/apps/catalog/cucumber.js b/packages/manager/apps/catalog/cucumber.js new file mode 100644 index 000000000000..05b68e273261 --- /dev/null +++ b/packages/manager/apps/catalog/cucumber.js @@ -0,0 +1,19 @@ +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' }, + }, +}; diff --git a/packages/manager/apps/catalog/e2e/Error.e2e.ts b/packages/manager/apps/catalog/e2e/Error.e2e.ts deleted file mode 100644 index d35493c68753..000000000000 --- a/packages/manager/apps/catalog/e2e/Error.e2e.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { test, expect } from '@playwright/test'; -import '@playwright-helpers/login'; -import * as translationsError from '../src/public/translations/catalog/error/Messages_fr_FR.json'; - -test('should display Error component if fetch fails', async ({ page }) => { - await page.route('*/**/2api/hub/catalog', (route, request) => { - route.fulfill({ - status: 500, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - await page.reload(); - - await page.waitForSelector('.manager-error-page'); - - await expect(page.locator('osds-text')).toContainText( - translationsError.manager_error_page_title, - ); - - await expect( - page.locator('.manager-error-page osds-message'), - ).toHaveAttribute('type', 'error'); - - await expect(page.locator('osds-button:nth-child(1)')).toContainText( - translationsError.manager_error_page_action_home_label, - ); - - await expect(page.locator('osds-button:nth-child(2)')).toContainText( - translationsError.manager_error_page_action_reload_label, - ); -}); diff --git a/packages/manager/apps/catalog/e2e/FilterAndSearch.e2e.ts b/packages/manager/apps/catalog/e2e/FilterAndSearch.e2e.ts deleted file mode 100644 index a99d6fdaeb72..000000000000 --- a/packages/manager/apps/catalog/e2e/FilterAndSearch.e2e.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect, Page, ElementHandle } from '@playwright/test'; -import '@playwright-helpers/login'; -import * as translation from '../src/public/translations/catalog/Messages_en_GB.json'; -import * as filterTranslation from '../src/public/translations/catalog/filters/Messages_en_GB.json'; -import * as searchTranslation from '../src/public/translations/catalog/search/Messages_en_GB.json'; - -const clickCheckboxByName = async (page: Page, name: string): Promise => { - const checkboxes = await page.$$('osds-checkbox'); - - const targetCheckbox = await Promise.all( - checkboxes.map(async (checkbox: ElementHandle) => { - const checkboxName = await checkbox.evaluate((el: Element) => - el.getAttribute('name'), - ); - return checkboxName === name ? checkbox : null; - }), - ).then((results) => results.find(Boolean)); - - if (targetCheckbox) { - await targetCheckbox.click(); - } else { - // eslint-disable-next-line no-console - console.warn(`Checkbox with name "${name}" not found.`); - } -}; - -const waitForProducts = async ( - page: Page, - expectedProductCount: number, -): Promise => { - await page.waitForFunction( - ({ selector, expectedCount }) => - document.querySelectorAll(selector).length === expectedCount, - { selector: 'osds-tile', expectedCount: expectedProductCount }, - ); - return page.$$('osds-tile'); -}; - -const validateProductCategories = async ( - products: ElementHandle[], - expectedCategories: string[], -): Promise => { - const checks = products.map(async (product, index) => { - const tileTypeElement = await product.$$('osds-text'); - expect(await tileTypeElement[0].textContent()).toBe( - expectedCategories[index], - ); - }); - - await Promise.all(checks); -}; - -test('should filter results based on Bare Metal Cloud universe', async ({ - page, -}) => { - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - await clickCheckboxByName(page, 'checkbox-universe-Bare_Metal_Cloud'); - await page - .getByText(filterTranslation.manager_catalog_filters_button_apply) - .click(); - const products = await waitForProducts(page, 5); - await validateProductCategories(products, [ - 'Dedicated Servers', - 'Virtual Private Servers', - 'Managed Bare Metal', - 'Storage and Backup', - 'Storage and Backup', - ]); -}); - -test('should clear all filters when Clear All button is clicked', async ({ - page, -}) => { - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - await clickCheckboxByName(page, 'checkbox-universe-Bare_Metal_Cloud'); - await page - .getByText(filterTranslation.manager_catalog_filters_button_apply) - .click(); - - await waitForProducts(page, 5); - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - - const links = await page.$$('osds-link'); - const resetFilterLink = links[0]; - if (resetFilterLink) await resetFilterLink.click(); - - await waitForProducts(page, 41); -}); - -// Scenario: No results after filtering -test('should show "No results found" when filters match no products', async ({ - page, -}) => { - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - await clickCheckboxByName(page, 'checkbox-universe-Bare_Metal_Cloud'); - await clickCheckboxByName(page, 'checkbox-category-AI_&_machine_learning'); - await page - .getByText(filterTranslation.manager_catalog_filters_button_apply) - .click(); - - await waitForProducts(page, 0); - - await page.waitForSelector('osds-text.text-center'); - - const elements = await page.$$('osds-text.text-center'); - expect(await elements[0].textContent()).toBe(translation.no_result); -}); - -test('should display chips when filter is enabled', async ({ page }) => { - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - await clickCheckboxByName(page, 'checkbox-universe-Bare_Metal_Cloud'); - await page - .getByText(filterTranslation.manager_catalog_filters_button_apply) - .click(); - - await waitForProducts(page, 5); - const elements = await page.$$('osds-chip'); - - expect(await elements[0].textContent()).toBe('Bare Metal Cloud'); -}); - -test('should clear all filters when chip is remove', async ({ page }) => { - await page - .getByText(searchTranslation.manager_catalog_search_filter_button) - .click(); - await clickCheckboxByName(page, 'checkbox-universe-Bare_Metal_Cloud'); - await page - .getByText(filterTranslation.manager_catalog_filters_button_apply) - .click(); - - await waitForProducts(page, 5); - - const element = page.locator('osds-chip osds-icon'); - if (element) await element.click(); - - await waitForProducts(page, 41); -}); diff --git a/packages/manager/apps/catalog/e2e/features/error.feature b/packages/manager/apps/catalog/e2e/features/error.feature new file mode 100644 index 000000000000..1413d294afb9 --- /dev/null +++ b/packages/manager/apps/catalog/e2e/features/error.feature @@ -0,0 +1,12 @@ +Feature: Error + + Scenario Outline: Display an error if request fails + Given The service to fetch the catalog is + When User navigates to catalog + Then User "" the list of products + Then User sees error + + Examples: + | apiOk | sees | anyError | + | OK | sees | no | + | KO | doesn't see | an | diff --git a/packages/manager/apps/catalog/e2e/features/filter-and-search.feature b/packages/manager/apps/catalog/e2e/features/filter-and-search.feature new file mode 100644 index 000000000000..a38ef15a6870 --- /dev/null +++ b/packages/manager/apps/catalog/e2e/features/filter-and-search.feature @@ -0,0 +1,23 @@ +Feature: Associate a vRack to a vRack Services + + Scenario Outline: Filter results based on Universe + Given User wants to filter products by universes "" + When User selects the universes in the filters + And User apply the search + Then User sees products corresponding to the universe + + Examples: + | universes | nbProducts | + | Bare Metal Cloud | 5 | + | Bare Metal Cloud,Web PaaS | 0 | + + Scenario: Clears all filters + Given User filtered the products with an universe + When User clicks on the Clear All button + Then User sees all the products of the catalog + + Scenario: Clear filter when chip is removed + Given User filtered the products with an universe + When User click on the remove button of a filter + Then The corresponding chip is removed + Then User sees all the products of the catalog diff --git a/packages/manager/apps/catalog/e2e/step-definitions/error.step.ts b/packages/manager/apps/catalog/e2e/step-definitions/error.step.ts new file mode 100644 index 000000000000..115177015b5b --- /dev/null +++ b/packages/manager/apps/catalog/e2e/step-definitions/error.step.ts @@ -0,0 +1,60 @@ +import { Given, When, Then } from '@cucumber/cucumber'; +import { expect } from '@playwright/test'; +import { ICustomWorld, sleep } from '../../../../../../playwright-helpers'; +import { ConfigParams, getUrl, setupNetwork } from '../utils'; +import { title } from '../../src/public/translations/catalog/Messages_fr_FR.json'; +import { + manager_error_page_title, + manager_error_page_action_home_label, + manager_error_page_action_reload_label, +} from '../../src/public/translations/catalog/error/Messages_fr_FR.json'; + +Given('The service to fetch the catalog is {word}', function( + this: ICustomWorld, + apiState: 'OK' | 'KO', +) { + this.handlersConfig.isKo = apiState === 'KO'; +}); + +When('User navigates to catalog', async function( + this: ICustomWorld, +) { + await setupNetwork(this); + await this.page.goto(this.testContext.initialUrl || getUrl('root'), { + waitUntil: 'load', + }); +}); + +Then('User {string} the list of products', async function( + this: ICustomWorld, + see: 'sees' | "doesn't see", +) { + if (see === 'sees') { + await sleep(1000); + + const titleElement = await this.page.locator('osds-text', { + hasText: title, + }); + await expect(titleElement).toBeVisible(); + + const products = await this.page.locator('osds-tile').all(); + await expect(products).toHaveLength(40); + } +}); + +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/catalog/e2e/step-definitions/filter-and-search.step.ts b/packages/manager/apps/catalog/e2e/step-definitions/filter-and-search.step.ts new file mode 100644 index 000000000000..3a948a62ad30 --- /dev/null +++ b/packages/manager/apps/catalog/e2e/step-definitions/filter-and-search.step.ts @@ -0,0 +1,80 @@ +import { expect } from '@playwright/test'; +import { Given, When, Then } from '@cucumber/cucumber'; +import { ICustomWorld, sleep } from '../../../../../../playwright-helpers'; +import { no_result } from '../../src/public/translations/catalog/Messages_fr_FR.json'; +import { + manager_catalog_filters_button_apply, + manager_catalog_filters_reset, +} from '../../src/public/translations/catalog/filters/Messages_fr_FR.json'; +import { manager_catalog_search_filter_button } from '../../src/public/translations/catalog/search/Messages_fr_FR.json'; +import { ConfigParams, selectFilters, setupE2eCatalogApp } from '../utils'; + +Given('User wants to filter products by universes {string}', function( + this: ICustomWorld, + universes: string, +) { + this.testContext.data.filters = universes + .split(',') + .map((universe) => universe.replace(/\s/gm, '_')); +}); + +Given('User filtered the products with an universe', async function( + this: ICustomWorld, +) { + this.testContext.data.filters = ['Bare_Metal_Cloud']; + + await setupE2eCatalogApp(this); + await selectFilters(this); + await this.page.getByText(manager_catalog_filters_button_apply).click(); +}); + +When('User selects the universes in the filters', async function( + this: ICustomWorld, +) { + await setupE2eCatalogApp(this); + await selectFilters(this); +}); + +When('User apply the search', async function(this: ICustomWorld) { + await this.page.getByText(manager_catalog_filters_button_apply).click(); +}); + +When('User clicks on the Clear All button', async function( + this: ICustomWorld, +) { + await this.page.getByText(manager_catalog_search_filter_button).click(); + await this.page.getByText(manager_catalog_filters_reset).click(); +}); + +When('User click on the remove button of a filter', async function( + this: ICustomWorld, +) { + await this.page.locator('osds-chip osds-icon').click(); +}); + +Then('User sees {int} products corresponding to the universe', async function( + this: ICustomWorld, + nbProducts, +) { + const nb = await this.page.locator('osds-tile').count(); + await expect(nb).toBe(nbProducts); + + if (nb === 0) { + const noResultText = await this.page.getByText(no_result); + await expect(noResultText).toBeVisible(); + } +}); + +Then('The corresponding chip is removed', async function( + this: ICustomWorld, +) { + const nbChips = await this.page.locator('osds-chip').count(); + await expect(nbChips).toBe(0); +}); + +Then('User sees all the products of the catalog', async function( + this: ICustomWorld, +) { + const nb = await this.page.locator('osds-tile').count(); + await expect(nb).toBe(40); +}); diff --git a/packages/manager/apps/catalog/e2e/utils/constants.ts b/packages/manager/apps/catalog/e2e/utils/constants.ts new file mode 100644 index 000000000000..9da40833e70a --- /dev/null +++ b/packages/manager/apps/catalog/e2e/utils/constants.ts @@ -0,0 +1,9 @@ +export const appUrl = 'http://localhost:9001/app'; + +export const urls = { + root: '/', +}; + +export type AppRoute = keyof typeof urls; + +export const getUrl = (route: AppRoute) => `${appUrl}${urls[route]}`; diff --git a/packages/manager/apps/catalog/e2e/utils/index.tsx b/packages/manager/apps/catalog/e2e/utils/index.tsx new file mode 100644 index 000000000000..04b6f09886b8 --- /dev/null +++ b/packages/manager/apps/catalog/e2e/utils/index.tsx @@ -0,0 +1,17 @@ +import { expect } from '@playwright/test'; +import { ICustomWorld } from '@playwright-helpers'; +import { ConfigParams as Config, setupNetwork } from './network'; +import { getUrl } from './constants'; +import { title } from '../../src/public/translations/catalog/Messages_fr_FR.json'; + +export async function setupE2eCatalogApp(ctx: ICustomWorld) { + await setupNetwork(ctx); + await ctx.page.goto(getUrl('root'), { + waitUntil: 'load', + }); + await expect(ctx.page.locator('osds-text', { hasText: title })).toBeVisible(); +} + +export * from './selector'; +export * from './network'; +export * from './constants'; diff --git a/packages/manager/apps/catalog/e2e/utils/network.ts b/packages/manager/apps/catalog/e2e/utils/network.ts new file mode 100644 index 000000000000..b2df89784fbd --- /dev/null +++ b/packages/manager/apps/catalog/e2e/utils/network.ts @@ -0,0 +1,28 @@ +import { BrowserContext } from '@playwright/test'; +import { + ICustomWorld, + toPlaywrightMockHandler, + Handler, +} from '../../../../../../playwright-helpers'; +import { + GetAuthenticationMocks, + getAuthenticationMocks, +} from '../../../../../../playwright-helpers/mocks/auth'; +import { GetCatalogMocksParams, getCatalogMocks } from '../../mocks/catalog'; + +export type ConfigParams = GetAuthenticationMocks & GetCatalogMocksParams; + +export const getConfig = (params: ConfigParams): Handler[] => + [getAuthenticationMocks, getCatalogMocks].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/catalog/e2e/utils/selector.ts b/packages/manager/apps/catalog/e2e/utils/selector.ts new file mode 100644 index 000000000000..4c6a29031b0d --- /dev/null +++ b/packages/manager/apps/catalog/e2e/utils/selector.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-await-in-loop */ +import { ICustomWorld } from '@playwright-helpers'; +import { ConfigParams } from './network'; +import { manager_catalog_search_filter_button } from '../../src/public/translations/catalog/search/Messages_fr_FR.json'; + +export const selectFilters = async (ctx: ICustomWorld) => { + const filters = ctx.testContext.data.filters as string[]; + + await ctx.page.getByText(manager_catalog_search_filter_button).click(); + + const checkboxes = await ctx.page.locator('osds-checkbox').all(); + + // eslint-disable-next-line no-restricted-syntax + for (const checkbox of checkboxes) { + const checkboxName = await checkbox.evaluate((el: Element) => + el.getAttribute('name'), + ); + if (filters.some((name) => checkboxName.includes(name))) { + await checkbox.click(); + } + } +}; diff --git a/packages/manager/apps/catalog/mocks/catalog-example.json b/packages/manager/apps/catalog/mocks/catalog-example.json new file mode 100644 index 000000000000..5408a2344146 --- /dev/null +++ b/packages/manager/apps/catalog/mocks/catalog-example.json @@ -0,0 +1,720 @@ +[ + { + "id": 20374, + "name": "Block Storage", + "description": "Créez vos volumes de stockage, utilisables comme des disques supplémentaires et sécurisés via une triple réplication des données", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Block Storage" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "BLOCK_STORAGE", + "order": "https://www.ovhcloud.com/fr/public-cloud/block-storage", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20375, + "name": "Cloud Archive", + "description": "Archivez vos données sur le long terme dans un espace de stockage cloud, accessible par des protocoles standards.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Cloud Archive" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "CLOUD_ARCHIVE", + "order": "https://www.ovhcloud.com/fr/public-cloud/cloud-archive", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20379, + "name": "Data Processing", + "description": "Lancez vos travaux de calcul Apache Spark rapidement et facilement", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Data Analytics", + "Data Processing" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "DATA_PROCESSING", + "featureAvailability": "data-processing", + "order": "https://www.ovhcloud.com/fr/public-cloud/data-processing", + "universe": "Public Cloud", + "category": "Data Analytics" + }, + { + "id": 20385, + "name": "E-mail Pro", + "description": "Le compte e-mail professionnel pour démarrer votre activité au prix le plus juste.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Solutions E-mails", + "E-mail Pro" + ], + "url": "https://ovhcloud.com/fr/emails/email-pro/", + "regionTags": ["EU"], + "productName": "EMAIL_PRO", + "order": "https://www.ovh.com/fr/emails/email-pro/", + "regions": ["EU"], + "featureAvailability": "email-pro", + "universe": "Hébergements web & Domaines", + "category": "Solutions E-mails" + }, + { + "id": 20390, + "name": "Instance Backup", + "description": "Profitez d'un service de sauvegarde de vos instances", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Instance Backup" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "INSTANCES_BACKUP", + "order": "https://www.ovhcloud.com/fr/public-cloud/instance-backup", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20394, + "name": "Microsoft 365", + "description": "Travaillez de manière optimale en consultant et en éditant vos documents où que vous soyez et en toute sécurité.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Solutions collaboratives", + "Microsoft 365" + ], + "url": "https://ovhcloud.com/fr/collaborative-tools/microsoft-365/", + "regionTags": ["EU"], + "productName": "LICENSE_OFFICE", + "order": "https://www.ovh.com/fr/office-365/", + "featureAvailability": "office", + "universe": "Hébergements web & Domaines", + "category": "Solutions collaboratives" + }, + { + "id": 20398, + "name": "Managed Kubernetes Service", + "description": "Orchestrez vos applications conteneurisées avec un cluster Kubernetes certifié CNCF", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Containers and orchestration", + "Managed Kubernetes Service" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "MANAGED_KUBERNETES", + "order": "https://www.ovhcloud.com/fr/public-cloud/kubernetes", + "featureAvailability": "kubernetes", + "universe": "Public Cloud", + "category": "Containers and orchestration" + }, + { + "id": 20399, + "name": "Managed Private Registry", + "description": "Gérez un dépôt pour vos briques logicielles, sous la forme d'images Docker ou de charts Helm", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Containers and orchestration", + "Managed Private Registry" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "MANAGED_PRIVATE_REGISTRY", + "order": "https://www.ovhcloud.com/fr/public-cloud/managed-private-registry", + "universe": "Public Cloud", + "category": "Containers and orchestration" + }, + { + "id": 20407, + "name": "Object Storage", + "description": "Profitez du stockage illimité à la demande, accessible par API, compatible S3", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Object Storage" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "OBJECT_STORAGE", + "order": "https://www.ovhcloud.com/fr/public-cloud/object-storage", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20414, + "name": "VMware on OVHcloud", + "description": "VMware on OVHcloud est une solution unique sur le marché offrant l'évolutivité du cloud sur une infrastructure hardware 100 % dédiée. La virtualisation de votre infrastructure est réalisée grâce à la technologie VMware et entièrement gérée par OVHcloud.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hosted Private Cloud", + "Plateforme", + "VMware on OVHCloud", + "VMware on OVHcloud" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "PRIVATE_CLOUD", + "order": "https://www.ovhcloud.com/fr/enterprise/products/hosted-private-cloud/prices/", + "highlight": true, + "featureAvailability": "dedicated-cloud:order", + "universe": "Hosted Private Cloud", + "category": "Plateforme" + }, + { + "id": 20415, + "name": "Base de données SQL privé", + "description": "Des bases de données privées, conçues pour étendre les capacités des hébergements web, offrir davantage de performances et de liberté de configuration.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Databases", + "Base de données SQL privé" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "PRIVATE_DATABASE", + "order": "https://www.ovh.com/fr/hebergement-web/options-sql.xml", + "featureAvailability": "private-database", + "universe": "Hébergements web & Domaines", + "category": "Databases" + }, + { + "id": 20418, + "name": "Shared Content Delivery Network (CDN)", + "description": "Optimisez le trafic de votre site web avec l'option Shared CDN pour une expérience utilisateur optimale.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Options Hébergement Web", + "Shared Content Delivery Network (CDN)" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "SHARED_CDN", + "order": "https://www.ovh.com/fr/hebergement-web/cdn.xml", + "featureAvailability": "hosting:shared-cdn", + "universe": "Hébergements web & Domaines", + "category": "Options Hébergement Web" + }, + { + "id": 20425, + "name": "Visibilité Pro", + "description": "Référencez votre activité partout où votre clientèle vous cherche. Recevez des alertes et répondez rapidement aux avis de vos clientes et clients. ", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Options Hébergement Web", + "Visibilité Pro" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "VISIBILITY_PRO", + "order": "https://www.ovh.com/fr/hebergement-web/referencement-local.xml", + "universe": "Hébergements web & Domaines", + "category": "Options Hébergement Web" + }, + { + "id": 20426, + "name": "Volume Snapshot", + "description": "Déclenchez un snapshot sur vos volumes Block Storage", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Volume Snapshot" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "VOLUME_SNAPSHOT", + "order": "https://www.ovhcloud.com/fr/public-cloud/volume-snapshot", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20430, + "name": "Web PaaS Powered by Platform.sh", + "description": "La plateforme de développement pensée pour les développeurs et leurs équipes, afin de concevoir, déployer et exécuter des applications web.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hébergements web & Domaines", + "Web PaaS", + "Web PaaS Powered by Platform.sh" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "WEB_PAAS", + "order": "https://www.ovh.com/manager/#/web/paas/webpaas/new", + "featureAvailability": "web-paas", + "universe": "Hébergements web & Domaines", + "category": "Web PaaS" + }, + { + "id": 20431, + "name": "Workflow Management", + "description": "Automatisez vos tâches pour manipuler vos ressources cloud, en vous basant sur votre logique métier, afin de répondre à chaque situation", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Containers and orchestration", + "Workflow Management" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "WORKFLOW_MANAGEMENT", + "order": "https://www.ovhcloud.com/fr/public-cloud/orchestration", + "universe": "Public Cloud", + "category": "Containers and orchestration" + }, + { + "id": 20447, + "name": "Nutanix on OVHcloud", + "description": "La solution Nutanix on OVHcloud associe les licences de la Nutanix Cloud Platform et les infrastructures Hosted Private Cloud d'OVHcloud dédiées et qualifiées Nutanix, pour pré-déployer un environnement hyperconvergé (HCI) Nutanix en quelques heures.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hosted Private Cloud", + "Plateforme", + "Nutanix on OVHcloud" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "NUTANIX", + "order": "https://www.ovhcloud.com/fr/lp/nutanix/", + "excludeSubsidiaries": ["ASIA", "AU", "SG"], + "featureAvailability": "nutanix", + "universe": "Hosted Private Cloud", + "category": "Plateforme" + }, + { + "id": 20449, + "name": "Compute", + "description": "Choisissez des instances parmi une large gamme de modèles, déployez-les et profitez de la flexibilité du cloud pour vous développer selon vos besoins.", + "lang": "fr_FR", + "categories": ["Catalogs", "Public Cloud", "Compute"], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "PCI_INSTANCES", + "order": "https://www.ovhcloud.com/fr/public-cloud/", + "universe": "Public Cloud", + "category": "Compute" + }, + { + "id": 20466, + "name": "Databases", + "description": "Exploitez la puissance de vos données en gardant le contrôle de vos ressources.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Databases" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "DATABASES", + "order": "https://www.ovhcloud.com/fr/public-cloud/databases", + "featureAvailability": "databases", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 20470, + "name": "AI Notebooks", + "description": "Démarrez vos notebooks Jupyter ou VS Code dans le cloud, simplement et rapidement", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "AI & machine learning", + "AI Notebooks" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "NOTEBOOKS", + "order": "https://www.ovhcloud.com/fr/public-cloud/ai-notebooks", + "featureAvailability": "notebooks", + "universe": "Public Cloud", + "category": "AI & machine learning" + }, + { + "id": 20473, + "name": "AI Training", + "description": "Entraînez efficacement et simplement vos modèles d'intelligence artificielle, de machine learning et de deep learning et optimisez vos usages GPU.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "AI & machine learning", + "AI Training" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "AI_TRAINING", + "order": "https://www.ovhcloud.com/fr/public-cloud/ai-training/", + "universe": "Public Cloud", + "category": "AI & machine learning" + }, + { + "id": 20633, + "name": "Horizon", + "description": "Retrouvez l'interface web originale d'OpenStack, afin d'administrer facilement vos ressources cloud", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Management Interfaces", + "Horizon" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "HORIZON", + "order": "https://docs.ovh.com/fr/public-cloud/presentation-dhorizon/", + "universe": "Public Cloud", + "category": "Management Interfaces" + }, + { + "id": 20804, + "name": "Serveurs dédiés", + "description": "Choisissez l’infrastructure la plus adaptée à vos applications métier. Nous vous proposons différentes gammes de serveurs dédiés performants et adaptés aux besoins les plus exigeants de toutes les entreprises.", + "lang": "fr_FR", + "categories": ["Catalogs", "Bare Metal Cloud", "Serveurs dédiés"], + "url": "https://ovhcloud.com/fr/bare-metal", + "regionTags": ["EU", "US", "CA"], + "productName": "DEDICATED_SERVER", + "order": "https://www.ovhcloud.com/fr/bare-metal/prices/", + "highlight": true, + "featureAvailability": "dedicated-server:order", + "universe": "Bare Metal Cloud", + "category": "Serveurs dédiés" + }, + { + "id": 20817, + "name": "Serveurs privés virtuels", + "description": "Un serveur privé virtuel (VPS) est un serveur dédié virtualisé. Contrairement à un hébergement web mutualisé où la gestion technique est prise en charge par OVHcloud, c’est vous qui administrez complètement votre VPS.", + "lang": "fr_FR", + "categories": ["Catalogs", "Bare Metal Cloud", "Serveurs privés virtuels"], + "url": "https://ovhcloud.com/fr/vps", + "regionTags": ["EU", "US", "CA"], + "productName": "VPS", + "order": "https://www.ovh.com/fr/vps/", + "highlight": true, + "featureAvailability": "vps", + "universe": "Bare Metal Cloud", + "category": "Serveurs privés virtuels" + }, + { + "id": 20820, + "name": "Managed Bare Metal", + "description": "Votre cloud dédié évolutif, hébergé et supervisé par OVHcloud, disponible en 90 minutes. Avec la gamme Essentials, choisissez la virtualisation VMware hautement disponible et restez concentré sur votre métier.", + "lang": "fr_FR", + "categories": ["Catalogs", "Bare Metal Cloud", "Managed Bare Metal"], + "url": "https://ovhcloud.com/fr/managed-bare-metal/", + "regionTags": ["EU", "US", "CA"], + "productName": "ESSENTIALS", + "order": "https://www.ovhcloud.com/fr/managed-bare-metal/", + "highlight": true, + "featureAvailability": "managed-bare-metal", + "universe": "Bare Metal Cloud", + "category": "Managed Bare Metal" + }, + { + "id": 20825, + "name": "Enterprise File Storage", + "description": "Enterprise File Storage, powered by NetApp. Connectez toutes vos solutions OVHcloud à un service de stockage de fichiers hautement performant, pour vos applications professionnelles les plus critiques.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Bare Metal Cloud", + "Storage and Backup", + "Enterprise File Storage" + ], + "url": "https://ovhcloud.com/fr/storage-solutions/enterprise-file-storage/", + "regionTags": ["EU", "CA"], + "productName": "NETAPP", + "order": "https://www.ovh.com/manager/#/dedicated/netapp/new", + "highlight": true, + "featureAvailability": "netapp", + "universe": "Bare Metal Cloud", + "category": "Storage and Backup" + }, + { + "id": 20837, + "name": "OVHcloud Load Balancer", + "description": "Scalez votre activité sans contraintes avec nos produits cloud, dans tous nos datacenters. L'OVHcloud Load Balancer répartit la charge entre vos différents services dans nos centres de données.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Network", + "Services réseau", + "OVHcloud Load Balancer" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "IP_LOAD_BALANCER", + "order": "https://www.ovh.com/fr/solutions/load-balancer/", + "featureAvailability": "ip-load-balancer", + "universe": "Network", + "category": "Services réseau" + }, + { + "id": 20838, + "name": "vRack", + "description": "La technologie vRack (baie virtuelle) permet de connecter, d’isoler ou de répartir vos produits OVHcloud compatibles au sein d’un ou plusieurs réseaux privés.", + "lang": "fr_FR", + "categories": ["Catalogs", "Network", "Services réseau", "vRack"], + "url": "https://mockup.url.com", + "regionTags": ["EU", "US", "CA"], + "productName": "VRACK", + "order": "https://www.ovh.com/fr/order/express/#/new/express/resume?products=~(~(planCode~'vrack~quantity~1~productId~'vrack))", + "universe": "Network", + "category": "Services réseau" + }, + { + "id": 20839, + "name": "OVHcloud Connect", + "description": "Notre solution de connexion hybride, OVHcloud Connect, vous permet de relier votre réseau d'entreprise à votre réseau privé OVHcloud vRack, de manière sécurisée et performante.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Network", + "Services réseau", + "OVHcloud Connect" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "OVH_CLOUD_CONNECT", + "order": "https://www.ovhcloud.com/fr/network-security/ovhcloud-connect/", + "featureAvailability": "cloud-connect", + "universe": "Network", + "category": "Services réseau" + }, + { + "id": 20847, + "name": "Veeam Cloud Connect", + "description": "Externalisez facilement et automatiquement vos sauvegardes dans le cloud sécurisé d’OVHcloud. Depuis Veeam Availability Suite, assurez la réplication de vos données dans nos datacenters.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Bare Metal Cloud", + "Storage and Backup", + "Veeam Cloud Connect" + ], + "url": "https://ovhcloud.com/fr/storage-solutions/veeam-cloud-connect", + "regionTags": ["EU", "CA"], + "productName": "VEEAM_CLOUD_CONNECT", + "order": "https://www.ovh.com/fr/storage-solutions/veeam-cloud-connect/", + "featureAvailability": "veeam-cloud-connect", + "universe": "Bare Metal Cloud", + "category": "Storage and Backup" + }, + { + "id": 20854, + "name": "Veeam Enterprise", + "description": "Accédez à la puissance de Veeam Backup & Replication en toute simplicité. OVHcloud vous permet de déployer votre solution et de configurer vos sauvegardes en toute liberté.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hosted Private Cloud", + "Storage and Backup", + "Veeam Enterprise" + ], + "url": "https://ovhcloud.com/fr/storage-solutions/veeam-enterprise", + "regionTags": ["EU", "CA"], + "productName": "VEEAM_ENTERPRISE", + "order": "https://www.ovh.com/fr/storage-solutions/veeam-enterprise.xml", + "featureAvailability": "veeam-enterprise:order", + "universe": "Hosted Private Cloud", + "category": "Storage and Backup" + }, + { + "id": 20872, + "name": "Logs Data Platform", + "description": "Indexez et analysez vos logs en temps réel. Soyez alertés en cas de dysfonctionnement. Partagez vos données avec vos collaborateurs.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Data Analytics", + "Logs Data Platform" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "ANALYTICS_DATA_PLATFORM", + "order": "https://www.ovhcloud.com/fr/public-cloud/big-data-hadoop/", + "universe": "Public Cloud", + "category": "Data Analytics" + }, + { + "id": 20895, + "name": "Réseau privé", + "description": "Déployez des réseaux privés pour implémenter des switchs virtuels capables de raccorder les instances de votre projet instantanément, à chaud et sans interruption.", + "lang": "fr_FR", + "categories": ["Catalogs", "Network", "Services réseau", "Réseau privé"], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "PRIVATE_NETWORK", + "order": "https://www.ovhcloud.com/fr/public-cloud/private-network", + "universe": "Network", + "category": "Services réseau" + }, + { + "id": 21197, + "name": "Cold Archive", + "description": "Bénéficiez de notre classe de stockage la plus économique pour l’archivage de vos données sur le long terme, dans une infrastructure s’appuyant sur des bandes magnétiques.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Cold Archive" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU"], + "productName": "COLD_ARCHIVE", + "order": "https://www.ovhcloud.com/fr/public-cloud/cold-archive/", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 21227, + "name": "Volume Backup", + "description": "Effectuez un backup de vos volumes Block Storage. Les données sauvegardées sont stockées sur notre service Object Storage.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Storage and Backup", + "Volume Backup" + ], + "url": "https://www.ovhcloud.com/fr/public-cloud/volume-backup/", + "regionTags": ["EU", "CA", "US"], + "productName": "VOLUME_BACKUP", + "order": "https://www.ovhcloud.com/fr/public-cloud/volume-backup/", + "universe": "Public Cloud", + "category": "Storage and Backup" + }, + { + "id": 21429, + "name": "AI Deploy", + "description": "Déployez des modèles et des applications de machine learning en production simplement, créez vos points d'accès API sans effort et réalisez des prédictions efficaces.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "AI & machine learning", + "AI Deploy" + ], + "url": "https://www.ovhcloud.com/fr/public-cloud/ai-deploy/", + "regionTags": ["EU", "CA"], + "productName": "AI_DEPLOY", + "order": "https://www.ovhcloud.com/fr/public-cloud/ai-deploy/", + "universe": "Public Cloud", + "category": "AI & machine learning" + }, + { + "id": 21725, + "name": "SAP HANA on Private Cloud", + "description": "Cette nouvelle plateforme combine des serveurs HCI certifiés SAP HANA avec notre infrastructure VMware on OVHcloud. Elle permet l’hébergement sécurisé et le déploiement facilité de vos environnements SAP les plus critiques dans un cloud souverain.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Hosted Private Cloud", + "Plateforme", + "SAP HANA on Private Cloud", + "SAP HANA on Private Cloud" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "SAP_HANA", + "order": "https://www.ovhcloud.com/fr/hosted-private-cloud/sap-hana/", + "highlight": true, + "featureAvailability": "dedicated-cloud:sapHanaOrder", + "universe": "Hosted Private Cloud", + "category": "Plateforme" + }, + { + "id": 21763, + "name": "Kubernetes Load Balancer", + "description": "Distribution automatique du trafic sur la solution Managed Kubernetes Service", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Containers and orchestration", + "Kubernetes Load Balancer" + ], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "KUBERNETES_LOAD_BALANCER", + "order": "https://www.ovhcloud.com/fr/public-cloud/kubernetes", + "featureAvailability": "kubernetes", + "universe": "Public Cloud", + "category": "Containers and orchestration" + }, + { + "id": 21764, + "name": "Load Balancer", + "description": "Répartition automatique du trafic sur les ressources Public Cloud", + "lang": "fr_FR", + "categories": ["Catalogs", "Network", "Services réseau", "Load Balancer"], + "url": "https://mockup.url.com", + "regionTags": ["EU", "CA"], + "productName": "OCTAVIA_LOAD_BALANCER", + "order": "https://www.ovhcloud.com/fr/public-cloud/private-network", + "featureAvailability": "octavia-load-balancer", + "universe": "Network", + "category": "Services réseau" + }, + { + "id": 100001, + "name": "Managed Rancher Service", + "description": "Déployez et gérez vos applications conteneurisées de manière transparente dans un environnement Kubernetes multicluster, avec une approche multicloud ou cloud hybride.", + "lang": "fr_FR", + "categories": [ + "Catalogs", + "Public Cloud", + "Containers and orchestration", + "Containers and orchestration" + ], + "url": "https://www.ovhcloud.com/fr/public-cloud/managed-rancher-service/", + "regionTags": ["EU", "CA"], + "productName": "MANAGED_RANCHER_SERVICE", + "order": "https://www.ovhcloud.com/fr/public-cloud/rancher", + "featureAvailability": "pci-rancher", + "universe": "Public Cloud", + "category": "Containers and orchestration" + } +] diff --git a/packages/manager/apps/catalog/mocks/catalog.ts b/packages/manager/apps/catalog/mocks/catalog.ts new file mode 100644 index 000000000000..a6da0b6eb518 --- /dev/null +++ b/packages/manager/apps/catalog/mocks/catalog.ts @@ -0,0 +1,24 @@ +import { Handler } from '../../../../../playwright-helpers'; +import productList from './catalog-example.json'; + +export type GetCatalogMocksParams = { isKo?: boolean }; + +export const getCatalogMocks = ({ isKo }: GetCatalogMocksParams): Handler[] => [ + { + url: '/catalog', + response: isKo + ? { + status: 500, + code: 'ERR_CATALOG_ERROR', + response: { + status: 500, + data: { + message: 'Catalog error', + }, + }, + } + : productList, + status: isKo ? 500 : 200, + api: 'aapi', + }, +]; diff --git a/packages/manager/apps/catalog/package.json b/packages/manager/apps/catalog/package.json index 8bacbd96f7da..4bb41e73ea6d 100644 --- a/packages/manager/apps/catalog/package.json +++ b/packages/manager/apps/catalog/package.json @@ -18,17 +18,16 @@ "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-catalog-app' --include-dependencies -- npm run dev:watch --if-present", "pretest": "tsc -p tsconfig.e2e.json", "test": "jest", - "test:e2e": "tsc && npx playwright test --headed", - "test:e2e:cii": "tsc && npx playwright test", - "test:e2e:script": "tsc && node ../../../../scripts/run-playwright.js" + "test:e2e": "tsc && node ../../../../scripts/run-playwright-bdd.js", + "test:e2e:ci": "tsc && node ../../../../scripts/run-playwright-bdd.js --ci" }, "dependencies": { - "@ovh-ux/manager-core-api": "^0.7.1", - "@ovh-ux/manager-core-utils": "^0.2.0", - "@ovh-ux/manager-react-core-application": "^0.8.2", - "@ovh-ux/manager-react-shell-client": "^0.4.2", - "@ovh-ux/shell": "^3.5.1", - "@ovhcloud/manager-components": "^1.10.2", + "@cucumber/cucumber": "^10.3.1", + "@ovh-ux/manager-core-api": "*", + "@ovh-ux/manager-core-utils": "*", + "@ovh-ux/manager-react-core-application": "*", + "@ovh-ux/manager-react-shell-client": "*", + "@ovhcloud/manager-components": "*", "@ovhcloud/ods-common-core": "17.2.1", "@ovhcloud/ods-common-theming": "17.2.1", "@ovhcloud/ods-components": "17.2.1", @@ -49,7 +48,7 @@ "devDependencies": { "@ovh-ux/manager-tailwind-config": "^0.2.0", "@ovh-ux/manager-vite-config": "^0.6.1", - "@playwright/test": "^1.34.3", + "@playwright/test": "^1.41.2", "@types/jest": "^29.5.5", "ts-jest": "^29.1.1", "typescript": "^4.3.2", diff --git a/packages/manager/apps/catalog/src/App.tsx b/packages/manager/apps/catalog/src/App.tsx index 3a3ae6aed06a..81fb56dfb34f 100644 --- a/packages/manager/apps/catalog/src/App.tsx +++ b/packages/manager/apps/catalog/src/App.tsx @@ -1,28 +1,35 @@ -import React, { useEffect } from 'react'; -import { QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { RouterProvider, createHashRouter } from 'react-router-dom'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { odsSetup } from '@ovhcloud/ods-common-core'; -import { useShell } from '@ovh-ux/manager-react-shell-client'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import appRoutes from './routes'; import '@ovhcloud/ods-theme-blue-jeans'; -import queryClient from './query.client'; odsSetup(); -const router = createHashRouter(appRoutes); -const Router = () => ; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: 300_000, + }, + }, +}); function App() { - const shell = useShell(); - useEffect(() => { + const { shell } = React.useContext(ShellContext); + const router = createHashRouter(appRoutes); + + React.useEffect(() => { shell.ux.hidePreloader(); }, []); return ( - - + + ); } diff --git a/packages/manager/apps/catalog/src/api/GET/aapi/service.ts b/packages/manager/apps/catalog/src/api/catalog/index.ts similarity index 75% rename from packages/manager/apps/catalog/src/api/GET/aapi/service.ts rename to packages/manager/apps/catalog/src/api/catalog/index.ts index ed9478831d3c..a1a254b05c11 100644 --- a/packages/manager/apps/catalog/src/api/GET/aapi/service.ts +++ b/packages/manager/apps/catalog/src/api/catalog/index.ts @@ -17,20 +17,6 @@ export type Product = { category: string; }; -export type CatalogData = { - data: Product[]; -}; - -export type ResponseData = { - status: number; - data: T; - code: string; - response?: { - status: number; - data: { message: string }; - }; -}; - /** * 2API-catalog endpoints : Get catalog */ diff --git a/packages/manager/apps/catalog/src/api/index.ts b/packages/manager/apps/catalog/src/api/index.ts index 9d40e74ad3fd..d435521dc909 100644 --- a/packages/manager/apps/catalog/src/api/index.ts +++ b/packages/manager/apps/catalog/src/api/index.ts @@ -1 +1 @@ -export * from './GET/aapi/service'; +export * from './catalog'; diff --git a/packages/manager/apps/catalog/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/apps/catalog/src/components/Breadcrumb/Breadcrumb.tsx index 04da1bf8fa33..c5e0b8ff1ba7 100644 --- a/packages/manager/apps/catalog/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/manager/apps/catalog/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,14 +1,14 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { OsdsBreadcrumb } from '@ovhcloud/ods-components/react'; -import { useShell } from '@ovh-ux/manager-react-shell-client'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { useTranslation } from 'react-i18next'; function Breadcrumb() { - const [href, setHref] = useState(''); - const shell = useShell(); + const { shell } = React.useContext(ShellContext); const { t } = useTranslation('catalog'); + const [href, setHref] = React.useState(''); - useEffect(() => { + React.useEffect(() => { const fetchUrl = async () => { try { const response = await shell.navigation.getURL('hub', '#/', {}); diff --git a/packages/manager/apps/catalog/src/components/Error/Errors.tsx b/packages/manager/apps/catalog/src/components/Error/Errors.tsx index c443d8592e9a..3363dfd189fe 100644 --- a/packages/manager/apps/catalog/src/components/Error/Errors.tsx +++ b/packages/manager/apps/catalog/src/components/Error/Errors.tsx @@ -1,15 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { ErrorBanner } from '@ovhcloud/manager-components'; -import { sendErrorTracking, ErrorObject } from '@/utils/trackingError'; +import { useSendErrorTracking, ErrorObject } from '@/utils/trackingError'; const Errors: React.FC = ({ error }) => { const navigate = useNavigate(); const location = useLocation(); - - useEffect(() => { - sendErrorTracking({ error }); - }, []); + useSendErrorTracking({ error }); return ( ([]); // full list of products const [results, setResults] = useState([]); // the filtered list of products - const service = useQuery, AxiosError>({ + const { error, isLoading, isSuccess, data } = useQuery< + ApiResponse, + ApiError + >({ queryKey: getManagerCatalogListQueryKey, - queryFn: () => getManagerCatalogList(), + queryFn: getManagerCatalogList, staleTime: Infinity, }); - const { error } = service; - useEffect(() => { - if (service?.data?.status === 200) { - const response: Product[] = service.data?.data; + if (data?.status === 200) { + const response: Product[] = data?.data; setProducts(response); setResults(response); } - }, [service.isSuccess, service.data]); + }, [isSuccess, data]); useEffect(() => { setResults(filterProducts(products, categories, universes, searchText)); @@ -45,7 +46,7 @@ export const useCatalog = ({ return { products, results, - isLoading: service.isLoading, + isLoading, error, }; }; diff --git a/packages/manager/apps/catalog/src/i18n.ts b/packages/manager/apps/catalog/src/i18n.ts deleted file mode 100644 index 45fa4e76e609..000000000000 --- a/packages/manager/apps/catalog/src/i18n.ts +++ /dev/null @@ -1,33 +0,0 @@ -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: 'catalog', - ns: ['catalog/filters', 'catalog/search', 'catalog/error'], // 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/catalog/src/index.tsx b/packages/manager/apps/catalog/src/index.tsx index ebf018eea192..c06ebd7ffa60 100644 --- a/packages/manager/apps/catalog/src/index.tsx +++ b/packages/manager/apps/catalog/src/index.tsx @@ -1,51 +1,40 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { - ShellProvider, + ShellContext, initShellContext, + initI18n, } from '@ovh-ux/manager-react-shell-client'; import App from './App'; -import initI18n from './i18n'; - import './global.css'; - import '@/vite-hmr'; -const init = async ( - appName: string, - { reloadOnLocaleChange } = { reloadOnLocaleChange: false }, -) => { +const init = async (appName: string) => { const context = await initShellContext(appName); - const region = context.environment.getRegion(); + try { await import(`./config-${region}.js`); } catch (error) { // nothing to do } - const locales = await context.shell.i18n.getAvailableLocales(); - - const i18n = await initI18n( - context.environment.getUserLocale(), - locales.map(({ key }: any) => key), - ); - - context.shell.i18n.onLocaleChange(({ locale }: { locale: string }) => { - if (reloadOnLocaleChange) { - window.top?.location.reload(); - } else { - i18n.changeLanguage(locale); - } + initI18n({ + context, + ns: ['catalog/filters', 'catalog/search', 'catalog/error'], + defaultNS: 'catalog', + reloadOnLocaleChange: true, }); - ReactDOM.createRoot(document.getElementById('root')!).render( + const root = ReactDOM.createRoot(document.getElementById('root')); + + root.render( - + - + , ); }; -init('catalog', { reloadOnLocaleChange: true }); +init('catalog'); diff --git a/packages/manager/apps/catalog/src/pages/_app.tsx b/packages/manager/apps/catalog/src/pages/_app.tsx deleted file mode 100644 index b22128f8405a..000000000000 --- a/packages/manager/apps/catalog/src/pages/_app.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import i18next from 'i18next'; -import { useTranslation } from 'react-i18next'; - -export function breadcrumb() { - return i18next.t('catalog:title'); -} - -export default function App({ - children, -}: Readonly<{ children: React.ReactNode }>) { - useTranslation('catalog'); - return <>{children}; -} diff --git a/packages/manager/apps/catalog/src/pages/index.tsx b/packages/manager/apps/catalog/src/pages/index.tsx index ec2f9eef55ea..e35fb0e64bd5 100644 --- a/packages/manager/apps/catalog/src/pages/index.tsx +++ b/packages/manager/apps/catalog/src/pages/index.tsx @@ -46,11 +46,6 @@ export default function Catalog() { } }, [searchText, categories, universes, products, isRouterInitialized]); - const getResultsNumber = () => { - if (isLoading) return ''; - return `(${results.length})`; - }; - if (!isLoading && error) return ; return ( @@ -63,7 +58,7 @@ export default function Catalog() { color={ODS_THEME_COLOR_INTENT.text} className="mb-3" > - {`${t('title')} ${getResultsNumber()}`} + {`${t('title')} ${isLoading ? '' : `(${results.length})`}`} { shell.ux.hidePreloader(); + shell.routing.stopListenForHashChange(); }, []); useEffect(() => { - routing.stopListenForHashChange(); - }, []); - useEffect(() => { - routing.onHashChange(); + shell.routing.onHashChange(); }, [location]); return ( diff --git a/packages/manager/apps/catalog/src/query.client.ts b/packages/manager/apps/catalog/src/query.client.ts deleted file mode 100644 index 94100b5328ad..000000000000 --- a/packages/manager/apps/catalog/src/query.client.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { QueryClient } from '@tanstack/react-query'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 300_000, - }, - }, -}); - -export default queryClient; diff --git a/packages/manager/apps/catalog/src/routes.tsx b/packages/manager/apps/catalog/src/routes.tsx index 98e6c05bd67f..5d6549ac65a9 100644 --- a/packages/manager/apps/catalog/src/routes.tsx +++ b/packages/manager/apps/catalog/src/routes.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { RouteObject } from 'react-router-dom'; import NotFound from './pages/404'; const lazyRouteConfig = (importFn: CallableFunction) => { @@ -14,7 +15,7 @@ const lazyRouteConfig = (importFn: CallableFunction) => { }; }; -export default [ +export const routes: RouteObject[] = [ { path: '/', ...lazyRouteConfig(() => import('@/pages/layout')), @@ -30,3 +31,5 @@ export default [ element: , }, ]; + +export default routes; diff --git a/packages/manager/apps/catalog/src/shell.ts b/packages/manager/apps/catalog/src/shell.ts deleted file mode 100644 index 9499d03add4e..000000000000 --- a/packages/manager/apps/catalog/src/shell.ts +++ /dev/null @@ -1,18 +0,0 @@ -let shellClient: any; - -export const setShellClient = (client: any) => { - shellClient = client; - - // set callbacks on locale change - shellClient.i18n.onLocaleChange(() => { - window.top.location.reload(); - }); - - return shellClient; -}; - -export const getShellClient = () => { - return shellClient; -}; - -export default { setShellClient, getShellClient }; diff --git a/packages/manager/apps/catalog/src/utils/trackingError.ts b/packages/manager/apps/catalog/src/utils/trackingError.ts index 0a30c1c3d98e..8eb7d2e5d4b1 100644 --- a/packages/manager/apps/catalog/src/utils/trackingError.ts +++ b/packages/manager/apps/catalog/src/utils/trackingError.ts @@ -1,6 +1,6 @@ -import { useShell } from '@ovh-ux/manager-react-core-application'; +import React from 'react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { useLocation } from 'react-router-dom'; - import { ErrorMessage, TRACKING_LABELS } from '@ovhcloud/manager-components'; export interface ErrorObject { @@ -16,20 +16,22 @@ export function getTypology(error: ErrorMessage) { return TRACKING_LABELS.PAGE_LOAD; } -export function sendErrorTracking({ error }: ErrorObject) { - const shell = useShell(); +export function useSendErrorTracking({ error }: ErrorObject) { + const { shell } = React.useContext(ShellContext); const location = useLocation(); const { tracking, environment } = shell; const env = environment.getEnvironment(); - env.then((response: any) => { - const { applicationName } = response; - const name = `errors::${getTypology(error)}::${applicationName}`; - tracking.trackPage({ - name, - level2: '81', - type: 'navigation', - page_category: location.pathname, + React.useEffect(() => { + env.then((response: any) => { + const { applicationName } = response; + const name = `errors::${getTypology(error)}::${applicationName}`; + tracking.trackPage({ + name, + level2: '81', + type: 'navigation', + page_category: location.pathname, + }); }); - }); + }, []); } diff --git a/packages/manager/apps/catalog/tsconfig.e2e.json b/packages/manager/apps/catalog/tsconfig.e2e.json deleted file mode 100644 index c383ee77bf55..000000000000 --- a/packages/manager/apps/catalog/tsconfig.e2e.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./e2e-dist" - }, - "include": ["e2e/*.e2e.ts"] -} diff --git a/packages/manager/apps/catalog/tsconfig.json b/packages/manager/apps/catalog/tsconfig.json index 8d2f3082477d..d005e8963c9e 100644 --- a/packages/manager/apps/catalog/tsconfig.json +++ b/packages/manager/apps/catalog/tsconfig.json @@ -20,9 +20,10 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], + "@playwright-helpers": ["../../../../playwright-helpers/index"], "@playwright-helpers/*": ["../../../../playwright-helpers/*"] } }, - "include": ["src"], + "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "dist", "types", "src/__tests__"] } diff --git a/packages/manager/apps/catalog/tsconfig.test.json b/packages/manager/apps/catalog/tsconfig.test.json new file mode 100644 index 000000000000..7048c297c8f6 --- /dev/null +++ b/packages/manager/apps/catalog/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/playwright-helpers/config.ts b/playwright-helpers/config.ts deleted file mode 100644 index 9a4e645bd2a4..000000000000 --- a/playwright-helpers/config.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const config = { - appUrl: 'http://localhost:9000/app/', - accountLoginOrigin: 'https://www.ovh.com', - ovhLogout: 'https://www.ovh.com/auth/?action=disconnect', - ovhLoginOrigin: 'https://login.corp.ovh.com', - accountLogin: '', - email: '', - password: '', -}; - -export default config; diff --git a/playwright-helpers/login.ts b/playwright-helpers/login.ts deleted file mode 100644 index 58e1b2c05488..000000000000 --- a/playwright-helpers/login.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Page, test } from '@playwright/test'; -import config from './config'; - -export async function login(page: Page): Promise { - await page.goto(config.appUrl); - await page.fill('input#account', config.accountLogin); - await page.locator('button#login-submit').click(); - await page.fill('input#userNameInput', config.email); - await page.fill('input#passwordInput', config.password); - await page.locator('#submitButton').click(); -} - -export async function logout(page: Page): Promise { - await page.goto(config.ovhLogout); -} - -type UseFunction = (page: Page) => Promise; - -export async function setupPage({ page }: { page: Page }, use: UseFunction) { - await login(page); - await page.waitForTimeout(4000); // for the tests when we need to do goto - - await use(page); - - await logout(page); -} - -test.use({ page: setupPage }); diff --git a/scripts/run-playwright.js b/scripts/run-playwright.js deleted file mode 100644 index 666ba77a98f5..000000000000 --- a/scripts/run-playwright.js +++ /dev/null @@ -1,46 +0,0 @@ -const execa = require('execa'); -// eslint-disable-next-line import/no-extraneous-dependencies -const { createServer } = require('vite'); - -console.log( - `\n\nRun e2e tests of ${process - .cwd() - .split('/') - .slice(-1)} micro-app\n`, -); - -const runTests = async () => { - let exitCode = 0; - const getBaseConfig = await import( - '../packages/manager/core/vite-config/src/index.js' - ).then((module) => module.getBaseConfig); - - const server = await createServer({ - ...getBaseConfig(), - server: { - port: 9001, - }, - }); - - await server.listen(); - - try { - const result = await execa('npx', ['playwright', 'test'], { - stdio: 'inherit', - detached: true, - encoding: 'utf-8', - }); - - exitCode = result.exitCode; - } catch (err) { - console.log('error:', err); - exitCode = 2; - } finally { - await server.close(); - } - return exitCode; -}; - -runTests() - .then(process.exit) - .catch(() => process.exit(1));