diff --git a/.eslintignore b/.eslintignore index 54d32b39db2b..0ba531a05d9b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,7 +17,6 @@ vue/src packages/components/ovh-at-internet/src/ovh-at-internet.ts packages/manager/apps/pci-databases-analytics/src/components/ui packages/manager/apps/container - packages/manager/core/api packages/manager/core/application packages/manager/core/ovh-product-icons @@ -41,9 +40,18 @@ packages/manager-tools/manager-static-analysis-kit packages/manager-tools/manager-tailwind-config packages/manager-tools/manager-tests-setup packages/manager-tools/manager-vite-config +packages/manager-tools/manager-muk-cli packages/manager/apps/pci-instances packages/manager/apps/pci-workflow packages/manager/apps/web-domains packages/manager/apps/web-hosting packages/manager/apps/web-office packages/manager/apps/zimbra +!.storybook +manager-static-analysis-kit +manager-generator +lint-runner.js +lint-cli.js +packages/manager-tools/manager-generator/bin/manager-generator.js +packages/manager-tools/manager-legacy-tools/test-utils/src/utils/ui-test-helpers-ods18.ts +packages/manager-ui-kit/src/vite-env.d.ts diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index df73b6dcc8d8..f25a8ced51bc 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -24,7 +24,7 @@ jobs: yarn exec turbo -- run build --filter="./packages/components/ovh-at-internet" yarn exec turbo -- run build --filter="./packages/manager/modules/config" yarn exec turbo -- run build --filter="./packages/manager/modules/common-translations" - yarn exec turbo -- run build --filter="./packages/manager-react-components" + yarn exec turbo -- run build --filter="./packages/manager-ui-kit" yarn workspace @ovh-ux/manager-documentation run docs:build - name: Deploy uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/run-bdd-tests.yml b/.github/workflows/run-bdd-tests.yml index 0d951a955138..26d2166cd964 100644 --- a/.github/workflows/run-bdd-tests.yml +++ b/.github/workflows/run-bdd-tests.yml @@ -23,7 +23,7 @@ jobs: - name: Build covered packages and dependencies run: | yarn exec turbo -- run build --filter="./packages/manager/core/*" --concurrency=5 - yarn exec turbo -- run build --filter="./packages/manager-react-components" + yarn exec turbo -- run build --filter="./packages/manager-ui-kit" yarn exec turbo -- run build --filter="./packages/manager/modules/order" yarn exec turbo -- run build --filter="./packages/manager/modules/common-api" yarn exec turbo -- run build --filter="./packages/manager/modules/common-translations" diff --git a/.gitignore b/.gitignore index ae4e19ef1c33..2a8d2bb33d9a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ code-duplication-reports/** tests-coverage-reports/** static-dynamic-quality-reports/**/** **/.type-coverage/result.json +packages/manager-tools/manager-muk-cli/target/**/**/** diff --git a/docs/docs/guide/manager-react-components.md b/docs/docs/guide/manager-react-components.md deleted file mode 100644 index e802b08f1164..000000000000 --- a/docs/docs/guide/manager-react-components.md +++ /dev/null @@ -1,29 +0,0 @@ -# Manager React components - -We use a library of super components in our react applications. - -## The package is accessible on the monorepo - -- [@ovh-ux/manager-react-components](https://github.com/ovh/manager/blob/develop/packages/manager-react-components/README.md) - -## The storybook : - -The storybook is accessible on -here. - -## How to start the application? - -```sh -$ yarn workspace @ovh-ux/manager-react-components run start -``` - -Go to `` - -## Example for Header on the storybook : - -![Screenshot of the manager-react-components storybook](/assets/img/storybook-manager-components.png) - -## Importation of a component on your react code application : - -The component is not builded so you can import directly the component named `Card` from the workspace like this : -`import Card from '@ovh-ux/manager-react-components'` diff --git a/docs/docs/guide/manager-ui-kit.md b/docs/docs/guide/manager-ui-kit.md new file mode 100644 index 000000000000..08504c82474d --- /dev/null +++ b/docs/docs/guide/manager-ui-kit.md @@ -0,0 +1,29 @@ +# Manager UI Kit + +We use a library of super components in our react applications. + +## The package is accessible on the monorepo + +- [@ovh-ux/muk](https://github.com/ovh/manager/blob/develop/packages/manager-ui-kit/README.md) + +## The storybook : + +The storybook is accessible on +here. + +## How to start the application? + +```sh +$ yarn workspace @ovh-ux/muk run start +``` + +Go to `` + +## Example for Header on the storybook : + +![Screenshot of the manager-ui-kit storybook](/assets/img/storybook-manager-components.png) + +## Importation of a component on your react code application : + +The component is not builded so you can import directly the component named `Card` from the workspace like this : +`import Card from '@ovh-ux/muk'` diff --git a/lerna.json b/lerna.json index 8e1181619fe5..e8cceb8c4e19 100644 --- a/lerna.json +++ b/lerna.json @@ -7,7 +7,7 @@ "packages/manager/core/*", "packages/manager/modules/*", "packages/manager/tools/*", - "packages/manager-react-components" + "packages/manager-ui-kit" ], "npmClient": "yarn", "useWorkspaces": true, diff --git a/package.json b/package.json index 68aaf49411ce..17c964cb5656 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "packages": [ "docs", "packages/components/*", - "packages/manager-react-components", "packages/manager-tools/*", "packages/manager-tools/manager-legacy-tools/*", + "packages/manager-ui-kit", "packages/manager-wiki", "packages/manager/apps/*", "packages/manager/core/api", @@ -35,7 +35,7 @@ "format:html": "yarn run lint:html && prettier --parser=html --write \"packages/**/*.html\"", "format:js": "yarn run lint:js --fix", "format:md": "yarn run lint:md --fix", - "format:tsx": "yarn run lint:tsx --fix && prettier --write \"packages/{manager/apps,manager-react-components}/**/*.{tsx,ts}\"", + "format:tsx": "yarn run lint:tsx --fix && prettier --write \"packages/{manager/apps,manager-ui-kit}/**/*.{tsx,ts}\"", "generate:uapp": "yarn manager-generator", "lint": "run-p lint:*", "lint:css": "stylelint \"packages/**/*.{css,less,scss}\"", diff --git a/packages/manager-react-components/.eslintrc.json b/packages/manager-react-components/.eslintrc.json deleted file mode 100644 index 405c232a45a0..000000000000 --- a/packages/manager-react-components/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../.eslintrc.js", - "rules": { - "@typescript-eslint/no-loss-of-precision": "off" - } -} diff --git a/packages/manager-react-components/package.json b/packages/manager-react-components/package.json deleted file mode 100644 index e4091ae790e2..000000000000 --- a/packages/manager-react-components/package.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "name": "@ovh-ux/manager-react-components", - "version": "2.42.2", - "license": "BSD-3-Clause", - "author": "OVH SAS", - "types": "dist/types/src/lib.d.ts", - "main": "dist/src/lib.js", - "browser": "dist/manager-react-components-lib.es.ts", - "homepage": "https://github.com/ovh/manager/blob/master/packages/manager-react-components/README.md", - "bugs": { - "url": "https://github.com/ovh/manager/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/ovh/manager.git", - "directory": "packages/manager-react-components" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc && vite build", - "dev": "tsc && vite build", - "prepare": "tsc && vite build", - "prettier": "prettier --write \"src/**/*.{ts,tsx,js,mdx}\"", - "test": "TZ=UTC vitest run", - "test:cov": "TZ=UTC vitest run --coverage", - "test:watch": "TZ=UTC vitest" - }, - "lint-staged": { - "*.{ts,tsx,js,jsx,json,css,md}": [ - "prettier -w" - ] - }, - "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.56.1", - "tailwindcss": "^3.4.4", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@babel/core": "7.22.10", - "@mdx-js/react": "^3.0.1", - "@ovh-ux/manager-common-translations": "^0.19.2", - "@ovh-ux/manager-core-api": "^0.18.6", - "@ovh-ux/manager-core-utils": "^0.4.4", - "@ovh-ux/manager-react-shell-client": "^0.9.3", - "@ovh-ux/manager-tailwind-config": "^0.5.4", - "@ovh-ux/manager-vite-config": "^0.13.3", - "@ovhcloud/ods-components": "^18.6.2", - "@ovhcloud/ods-themes": "^18.6.2", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "14.0.0", - "@types/jest": "^29.5.5", - "@types/lodash.isdate": "^4.0.9", - "@types/lodash.isequal": "^4.5.0", - "@types/node": "20.4.9", - "@types/react": "18.2.45", - "@types/react-dom": "18.2.7", - "@typescript-eslint/eslint-plugin": "^8.26.1", - "@typescript-eslint/parser": "^8.0.0", - "@vitest/coverage-v8": "^3.0.8", - "autoprefixer": "10.4.14", - "axios-mock-adapter": "2.1.0", - "babel-loader": "9.1.3", - "babel-preset-react-app": "^10.0.1", - "date-fns": "~4.1.0", - "element-internals-polyfill": "^3.0.2", - "eslint": "^9.22.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-n": "^17.16.2", - "eslint-plugin-promise": "^7.2.1", - "eslint-plugin-react": "^7.37.4", - "file-loader": "^6.2.0", - "husky": "8.0.3", - "i18next": "^23.8.2", - "jsdom": "26.0.0", - "json": "11.0.0", - "lint-staged": "13.2.3", - "msw": "2.3.1", - "postcss": "8.4.31", - "prettier": "3.0.1", - "prop-types": "15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^14.0.5", - "react-router-dom": "^6.3.0", - "ts-jest": "^29.1.1", - "typescript": "^4.3.2", - "undici": "5.29.0", - "vite": "^6.0.7", - "vite-plugin-dts": "^4.5.3", - "vite-plugin-static-copy": "^2.3.0", - "vitest": "^3.0.8", - "zustand": "^4.5.5" - }, - "peerDependencies": { - "@ovh-ux/manager-common-translations": "*", - "@ovh-ux/manager-core-api": "^0.10.0", - "@ovh-ux/manager-core-utils": "*", - "@ovh-ux/manager-react-shell-client": "^0.9.1", - "@ovhcloud/ods-components": "^18.3.0", - "@ovhcloud/ods-themes": "^18.3.0", - "date-fns": "~4.1.0", - "i18next": "^23.8.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.3.0", - "zustand": "^4.5.5" - } -} diff --git a/packages/manager-react-components/src/components/ManagerButton/ManagerButton.spec.tsx b/packages/manager-react-components/src/components/ManagerButton/ManagerButton.spec.tsx deleted file mode 100644 index abb93c1a4b68..000000000000 --- a/packages/manager-react-components/src/components/ManagerButton/ManagerButton.spec.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { fireEvent, screen } from '@testing-library/react'; -import { ManagerButton, ManagerButtonProps } from './ManagerButton'; -import { render } from '../../utils/test.provider'; -import fr_FR from './translations/Messages_fr_FR.json'; -import { useAuthorizationIam } from '../../hooks/iam'; -import { IamAuthorizationResponse } from '../../hooks/iam/iam.interface'; - -vitest.mock('../../hooks/iam'); - -const renderComponent = (props: ManagerButtonProps) => { - return render(); -}; - -const mockedHook = - useAuthorizationIam as unknown as jest.Mock; - -describe('ManagerButton tests', () => { - afterEach(() => { - vitest.resetAllMocks(); - }); - - describe('should display manager button', () => { - it('with true value for useAuthorizationIam', () => { - mockedHook.mockReturnValue({ - isAuthorized: true, - isLoading: true, - isFetched: true, - }); - renderComponent({ - id: 'test-manager-button', - urn: 'urn:v9:eu:resource:manager-react-components:vrz-a878-dsflkds-fdsfsd', - iamActions: [ - 'manager-react-components:apiovh:manager-react-components/attach-action', - ], - label: 'foo-manager-button', - }); - expect(screen.getByTestId('manager-button')).not.toBeNull(); - }); - - it('with false value for useAuthorizationIam', () => { - mockedHook.mockReturnValue({ - isAuthorized: false, - isLoading: true, - isFetched: true, - }); - renderComponent({ - id: 'test-manager-button', - urn: 'urn:v9:eu:resource:manager-react-components:vrz-a878-dsflkds-fdsfsd', - iamActions: [ - 'manager-react-components:apiovh:manager-react-components/attach', - ], - label: 'fo manager button', - }); - expect(screen.getByTestId('manager-button-tooltip')).not.toBeNull(); - expect(screen.getByTestId('manager-button-tooltip')).toHaveAttribute( - 'is-disabled', - 'true', - ); - }); - }); - - describe('should display tooltip', () => { - it('with false value for useAuthorizationIam', () => { - mockedHook.mockReturnValue({ - isAuthorized: false, - isLoading: true, - isFetched: true, - }); - renderComponent({ - id: 'manager-button', - urn: 'urn:v9:eu:resource:manager-react-components:vrz-a878-dsflkds-fdsfsd', - iamActions: [ - 'manager-react-components:apiovh:manager-react-components/attach-action', - ], - label: 'foo-manager-button', - }); - expect(screen.getByTestId('manager-button-tooltip')).not.toBeNull(); - expect(screen.getByTestId('manager-button-tooltip')).toHaveAttribute( - 'is-disabled', - 'true', - ); - const button = screen.getByTestId('manager-button-tooltip'); - fireEvent.mouseOver(button); - expect( - screen.getAllByText(fr_FR.common_iam_actions_message), - ).not.toBeNull(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/ManagerButton/ManagerButton.tsx b/packages/manager-react-components/src/components/ManagerButton/ManagerButton.tsx deleted file mode 100644 index ac2d135f5035..000000000000 --- a/packages/manager-react-components/src/components/ManagerButton/ManagerButton.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { PropsWithChildren, RefAttributes, HTMLAttributes } from 'react'; -import { OdsButton, OdsTooltip } from '@ovhcloud/ods-components/react'; -import { JSX } from '@ovhcloud/ods-components'; -import { useTranslation } from 'react-i18next'; -import { StyleReactProps } from '@ovhcloud/ods-components/react/dist/types/react-component-lib/interfaces'; -import './translations'; - -import { useAuthorizationIam } from '../../hooks/iam'; - -export type ManagerButtonProps = PropsWithChildren<{ - id: string; - iamActions?: string[]; - urn?: string; - displayTooltip?: boolean; - isIamTrigger?: boolean; - label: string; -}>; - -export const ManagerButton = ({ - id, - children, - label, - iamActions, - urn, - displayTooltip = true, - isIamTrigger = true, - ...restProps -}: ManagerButtonProps & - Partial< - JSX.OdsButton & - Omit, 'style' | 'id'> & - StyleReactProps & - RefAttributes - >) => { - const { t } = useTranslation('iam'); - const { isAuthorized } = useAuthorizationIam(iamActions, urn, isIamTrigger); - - if (isAuthorized || !(iamActions && urn)) { - return ( - - ); - } - return displayTooltip ? ( - <> -
- -
- -
{t('common_iam_actions_message')}
-
- - ) : ( - - ); -}; diff --git a/packages/manager-react-components/src/components/ManagerLink/ManagerLink.component.tsx b/packages/manager-react-components/src/components/ManagerLink/ManagerLink.component.tsx deleted file mode 100644 index edb068fab9e4..000000000000 --- a/packages/manager-react-components/src/components/ManagerLink/ManagerLink.component.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useId, useMemo } from 'react'; -import { NAMESPACES } from '@ovh-ux/manager-common-translations'; -import { OdsLink, OdsTooltip } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; -import { useAuthorizationIam } from '../../hooks'; - -export type ManagerLinkProps = React.ComponentProps & { - iamActions?: string[]; - urn?: string; - isDisplayTooltip?: boolean; - isIamCheckDisabled?: boolean; -}; - -export const ManagerLink = ({ - children, - iamActions, - urn, - isDisplayTooltip, - isIamCheckDisabled, - isDisabled, - ...restProps -}: ManagerLinkProps) => { - const tooltipTriggerRawId = useId(); - const tooltipTriggerId = useMemo( - () => tooltipTriggerRawId.replace(/:/g, ''), - [tooltipTriggerRawId], - ); - - const { t } = useTranslation(NAMESPACES.IAM); - const { isAuthorized } = useAuthorizationIam( - iamActions, - urn, - !isIamCheckDisabled, - ); - - if (!isDisabled && (isAuthorized || iamActions === undefined)) { - return {children}; - } - - return !isDisplayTooltip || isDisabled ? ( - - {children} - - ) : ( -
- - - {children} - - - -
{t('iam_actions_message')}
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/ManagerLink/ManagerLink.spec.tsx b/packages/manager-react-components/src/components/ManagerLink/ManagerLink.spec.tsx deleted file mode 100644 index 5ea7698d4fa8..000000000000 --- a/packages/manager-react-components/src/components/ManagerLink/ManagerLink.spec.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import AxiosMockAdapter from 'axios-mock-adapter'; -import apiClient from '@ovh-ux/manager-core-api'; -import { waitFor } from '@testing-library/react'; -import { render } from '../../utils/test.provider'; -import { ManagerLink } from './ManagerLink.component'; - -const PROPS_LINK = { - label: 'Link', - href: 'https://www.example.com', -}; - -const mockAdapter = new AxiosMockAdapter(apiClient.v2); - -describe('Manager Links component', () => { - it('renders a link correctly', () => { - const { container } = render(); - const linkElement = container.querySelector('[label="Link"]'); - expect(linkElement).toBeInTheDocument(); - expect(linkElement).not.toHaveAttribute('is-disabled'); - }); - - it('not renders a link when we have not the autorization', async () => { - mockAdapter.onPost('/iam/resource/test/authorization/check').reply(200, []); - - const { container } = render( - , - ); - const linkElement = container.querySelector('[label="Link"]'); - expect(linkElement).toBeInTheDocument(); - - await waitFor(() => { - expect(linkElement).toHaveAttribute('is-disabled', 'true'); - }); - }); - - it('renders a link when we have the autorization', async () => { - mockAdapter - .onPost('/iam/resource/test/authorization/check') - .reply(200, { authorizedActions: ['subscribe'] }); - - const { container } = render( - , - ); - const linkElement = container.querySelector('[label="Link"]'); - expect(linkElement).toBeInTheDocument(); - - await waitFor(() => { - expect(linkElement).not.toHaveAttribute('is-disabled'); - }); - }); - - it('renders a disabled link when we have the autorization but forced disabled', async () => { - mockAdapter - .onPost('/iam/resource/test/authorization/check') - .reply(200, { authorizedActions: ['subscribe'] }); - - const { container } = render( - , - ); - const linkElement = container.querySelector('[label="Link"]'); - expect(linkElement).toBeInTheDocument(); - - await waitFor(() => { - expect(linkElement).toHaveAttribute('is-disabled', 'true'); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/ManagerText/ManagerText.spec.tsx b/packages/manager-react-components/src/components/ManagerText/ManagerText.spec.tsx deleted file mode 100644 index e54ca8ba6ca8..000000000000 --- a/packages/manager-react-components/src/components/ManagerText/ManagerText.spec.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { screen } from '@testing-library/react'; -import { ManagerText, ManagerTextProps } from './ManagerText'; -import { render } from '../../utils/test.provider'; -import fr_FR from './translations/Messages_fr_FR.json'; -import { useAuthorizationIam } from '../../hooks/iam'; -import { IamAuthorizationResponse } from '../../hooks/iam/iam.interface'; - -vitest.mock('../../hooks/iam'); - -const renderComponent = (props: ManagerTextProps) => { - return render(); -}; -const mockedHook = - useAuthorizationIam as unknown as jest.Mock; - -describe('ManagerText tests', () => { - afterEach(() => { - vitest.resetAllMocks(); - }); - - describe('should display manager text', () => { - it('with true value for useAuthorizationIam', () => { - mockedHook.mockReturnValue({ - isAuthorized: true, - isLoading: true, - isFetched: true, - }); - renderComponent({ - urn: 'urn:v9:eu:resource:manager-react-components:vrz-a878-dsflkds-fdsfsd', - iamActions: [ - 'manager-react-components:apiovh:manager-react-components/get-display', - ], - children:
foo-manager-text
, - }); - expect(screen.getAllByText('foo-manager-text')).not.toBeNull(); - }); - }); - describe('should display error manager text', () => { - it('with false value for useAuthorizationIam', () => { - mockedHook.mockReturnValue({ - isAuthorized: false, - isLoading: true, - isFetched: true, - }); - renderComponent({ - urn: 'urn:v9:eu:resource:manager-react-components:vrz-a878-dsflkds-fdsfsd', - iamActions: [ - 'manager-react-components:apiovh:manager-react-components/get-display', - ], - children:
foo-manager-text
, - }); - expect(screen.findByText(fr_FR.iam_hidden_text)).not.toBeNull(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/ManagerText/ManagerText.tsx b/packages/manager-react-components/src/components/ManagerText/ManagerText.tsx deleted file mode 100644 index 1a6e5b9e869e..000000000000 --- a/packages/manager-react-components/src/components/ManagerText/ManagerText.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { PropsWithChildren, RefAttributes, HTMLAttributes } from 'react'; -import { JSX } from '@ovhcloud/ods-components'; -import { OdsText, OdsTooltip } from '@ovhcloud/ods-components/react'; -import { StyleReactProps } from '@ovhcloud/ods-components/react/dist/types/react-component-lib/interfaces'; -import { useTranslation } from 'react-i18next'; -import './translations'; - -import { useAuthorizationIam } from '../../hooks/iam'; - -export type ManagerTextProps = PropsWithChildren<{ - iamActions?: string[]; - urn?: string; -}>; - -export const ManagerText = ({ - children, - iamActions, - urn, - ...restProps -}: ManagerTextProps & - Partial< - JSX.OdsText & - Omit, 'style'> & - StyleReactProps & - RefAttributes - >) => { - const { t } = useTranslation('iam'); - const { isAuthorized } = useAuthorizationIam(iamActions, urn); - - if (!isAuthorized) { - return ( - <> -
- - {t('iam_hidden_text').toUpperCase()} - -
- -
{t('common_iam_get_message')}
-
- - ); - } - return {children}; -}; diff --git a/packages/manager-react-components/src/components/Modal/Modal.component.tsx b/packages/manager-react-components/src/components/Modal/Modal.component.tsx deleted file mode 100644 index 7062a9c5de40..000000000000 --- a/packages/manager-react-components/src/components/Modal/Modal.component.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { Ref } from 'react'; -import { - OdsButton, - OdsModal, - OdsText, - OdsSpinner, -} from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_VARIANT, - ODS_MODAL_COLOR, - ODS_BUTTON_COLOR, - ODS_TEXT_PRESET, - ODS_SPINNER_SIZE, -} from '@ovhcloud/ods-components'; -import { NAMESPACES } from '@ovh-ux/manager-common-translations'; -import { useTranslation } from 'react-i18next'; - -export interface ModalProps { - /** Title of modal */ - heading?: string; - /** Properties for the step number displayed on the top right of the modal */ - step?: { - /** Current step displayed on the modal (must define heading and total) */ - current?: number; - /** Total number of steps in the modal (must defined heading and current) */ - total?: number; - }; - /** Type of modal. It can be any of `ODS_MODAL_COLOR` */ - type?: ODS_MODAL_COLOR; - /** Is loading state for display a spinner */ - isLoading?: boolean; - /** Label of primary button */ - primaryLabel?: string; - /** Is loading state for primary button */ - isPrimaryButtonLoading?: boolean; - /** Is disabled state for primary button */ - isPrimaryButtonDisabled?: boolean; - /** Action of primary button */ - onPrimaryButtonClick?: () => void; - /** Test id of primary button */ - primaryButtonTestId?: string; - /** Label of secondary button */ - secondaryLabel?: string; - /** Is loading state for secondary button */ - isSecondaryButtonDisabled?: boolean; - /** Is loading state for secondary button */ - isSecondaryButtonLoading?: boolean; - /** Is disabled state for secondary button */ - onSecondaryButtonClick?: () => void; - /** Test id of secondary button */ - secondaryButtonTestId?: string; - /** Is dismissible action */ - onDismiss?: () => void; - /** Is modal open state */ - isOpen?: boolean; - /** Children of modal */ - children?: React.ReactNode; -} - -const getPrimaryButtonColor = (type: ODS_MODAL_COLOR) => - type === ODS_MODAL_COLOR.critical - ? ODS_BUTTON_COLOR.critical - : ODS_BUTTON_COLOR.primary; - -export const ModalComponent: React.ForwardRefRenderFunction< - HTMLOdsModalElement, - ModalProps -> = ( - { - heading, - step, - type = ODS_MODAL_COLOR.information, - isLoading, - primaryLabel, - isPrimaryButtonLoading, - isPrimaryButtonDisabled, - onPrimaryButtonClick, - primaryButtonTestId, - secondaryLabel, - isSecondaryButtonDisabled, - isSecondaryButtonLoading, - onSecondaryButtonClick, - secondaryButtonTestId, - onDismiss, - isOpen = true, - children, - }, - ref: Ref, -) => { - const buttonColor = getPrimaryButtonColor(type); - const { t } = useTranslation(NAMESPACES.FORM); - - return ( - - {heading && ( -
- - {heading} - - {Number.isInteger(step?.current) && Number.isInteger(step?.total) && ( - - {t('stepPlaceholder', { - current: step.current, - total: step.total, - })} - - )} -
- )} - {isLoading ? ( -
- -
- ) : ( - <> -
{children}
- {secondaryLabel && ( - - )} - {primaryLabel && ( - - )} - - )} -
- ); -}; - -export const Modal: React.ForwardRefExoticComponent< - React.PropsWithoutRef & React.RefAttributes -> = React.forwardRef(ModalComponent); -Modal.displayName = 'Modal'; diff --git a/packages/manager-react-components/src/components/Modal/Modal.mock.tsx b/packages/manager-react-components/src/components/Modal/Modal.mock.tsx deleted file mode 100644 index 4b4d0755ed61..000000000000 --- a/packages/manager-react-components/src/components/Modal/Modal.mock.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export const basic = { - heading: 'Example of modal', - children:
Example of content
, -}; - -export const actions = { - primaryLabel: 'Confirm', - isPrimaryButtonLoading: false, - isPrimaryButtonDisabled: false, - onPrimaryButtonClick: () => 'onPrimaryButtonClick', - secondaryLabel: 'Cancel', - isSecondaryButtonDisabled: false, - isSecondaryButtonLoading: false, - onSecondaryButtonClick: () => 'onSecondaryButtonClick', - onDismiss: () => 'onDismiss', -}; - -export const type = { - type: 'warning', -}; - -export const loading = { - isLoading: true, -}; - -export const other = { - isOpen: true, -}; diff --git a/packages/manager-react-components/src/components/Modal/Modal.spec.tsx b/packages/manager-react-components/src/components/Modal/Modal.spec.tsx deleted file mode 100644 index 38c8d8992154..000000000000 --- a/packages/manager-react-components/src/components/Modal/Modal.spec.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { vi } from 'vitest'; -import { ODS_MODAL_COLOR } from '@ovhcloud/ods-components'; -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { Modal } from './Modal.component'; -import { - basic as basicMock, - actions as actionsMock, - loading as loadingMock, -} from './Modal.mock'; - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -it('should display the basic modal', () => { - const { getByTestId } = render(); - expect(getByTestId('modal')).not.toBeNull(); - expect(screen.queryByText('Example of modal')).not.toBeNull(); - expect(screen.queryByText('Example of content')).not.toBeNull(); -}); - -it('should display the modal with action', async () => { - const onPrimaryButtonClick = vi.fn(actionsMock.onPrimaryButtonClick); - const onSecondaryButtonClick = vi.fn(actionsMock.onSecondaryButtonClick); - const { getByTestId } = render( - , - ); - const primaryButton = getByTestId('primary-button'); - const secondaryButton = getByTestId('secondary-button'); - - expect(primaryButton).toHaveAttribute('label', 'Confirm'); - expect(primaryButton).toHaveAttribute('color', 'primary'); - expect(primaryButton).toHaveAttribute('variant', 'default'); - expect(primaryButton).toHaveAttribute('is-loading', 'false'); - expect(primaryButton).toHaveAttribute('is-disabled', 'false'); - - expect(secondaryButton).toHaveAttribute('label', 'Cancel'); - expect(secondaryButton).toHaveAttribute('color', 'primary'); - expect(secondaryButton).toHaveAttribute('variant', 'ghost'); - expect(secondaryButton).toHaveAttribute('is-loading', 'false'); - expect(secondaryButton).toHaveAttribute('is-disabled', 'false'); - - await act(() => fireEvent.click(primaryButton)); - expect(onPrimaryButtonClick).toHaveBeenCalled(); - - await act(() => fireEvent.click(secondaryButton)); - expect(onSecondaryButtonClick).toHaveBeenCalled(); -}); - -it('should display the modal with critical type', () => { - const { getByTestId } = render( - , - ); - expect(getByTestId('modal')).not.toBeNull(); - expect(screen.queryAllByTestId(/-button/)).toHaveLength(2); - const primaryButton = getByTestId('primary-button'); - const secondaryButton = getByTestId('secondary-button'); - - expect(primaryButton).toHaveAttribute('label', 'Confirm'); - expect(primaryButton).toHaveAttribute('color', 'critical'); - expect(primaryButton).toHaveAttribute('variant', 'default'); - expect(primaryButton).toHaveAttribute('is-loading', 'false'); - expect(primaryButton).toHaveAttribute('is-disabled', 'false'); - - expect(secondaryButton).toHaveAttribute('label', 'Cancel'); - expect(secondaryButton).toHaveAttribute('color', 'critical'); - expect(secondaryButton).toHaveAttribute('variant', 'ghost'); - expect(secondaryButton).toHaveAttribute('is-loading', 'false'); - expect(secondaryButton).toHaveAttribute('is-disabled', 'false'); -}); - -it('should display the modal with loading state', () => { - const { getByTestId } = render( - , - ); - expect(getByTestId('spinner')).not.toBeNull(); - expect(screen.queryAllByTestId(/-button/)).toHaveLength(0); -}); - -it('should display the modal with disabled buttons', async () => { - const onPrimaryButtonClick = vi.fn(actionsMock.onPrimaryButtonClick); - const onSecondaryButtonClick = vi.fn(actionsMock.onSecondaryButtonClick); - const { getByTestId } = render( - , - ); - const primaryButton = getByTestId('primary-button'); - const secondaryButton = getByTestId('secondary-button'); - expect(primaryButton).toHaveAttribute('is-disabled', 'true'); - expect(secondaryButton).toHaveAttribute('is-disabled', 'true'); - - await act(() => fireEvent.click(primaryButton)); - expect(onPrimaryButtonClick).not.toHaveBeenCalled(); - - await act(() => fireEvent.click(secondaryButton)); - expect(onSecondaryButtonClick).not.toHaveBeenCalled(); -}); - -it('should display the basic modal with steps', () => { - render(); - - expect(screen.getByTestId('modal')).not.toBeNull(); - expect(screen.queryByText(basicMock.heading)).not.toBeNull(); - expect(screen.queryByText('stepPlaceholder')).not.toBeNull(); -}); - -it('should not display the step count', () => { - render(); - - expect(screen.getByTestId('modal')).not.toBeNull(); - expect(screen.queryByText(basicMock.heading)).not.toBeNull(); - expect(screen.queryByText('stepPlaceholder')).toBeNull(); -}); diff --git a/packages/manager-react-components/src/components/Modal/index.ts b/packages/manager-react-components/src/components/Modal/index.ts deleted file mode 100644 index f80428f8a030..000000000000 --- a/packages/manager-react-components/src/components/Modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Modal.component'; diff --git a/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.spec.tsx b/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.spec.tsx deleted file mode 100644 index ecff168d8a0f..000000000000 --- a/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.spec.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { ResourceStatus } from '../../hooks'; -import { render } from '../../utils/test.provider'; -import { ServiceStateBadge } from './ServiceStateBadge.component'; - -vitest.mock('../../hooks/iam'); - -const renderComponent = ( - props: React.ComponentProps, -) => { - return render(); -}; - -describe('should display manager state with the good color', () => { - it.each([ - { - state: 'active', - label: 'service_state_active', - color: 'success', - } as const, - { - state: 'deleted', - label: 'service_state_deleted', - color: 'critical', - } as const, - { - state: 'unknown' as ResourceStatus, - label: 'unknown', - color: 'information', - } as const, - ])( - `should display manager $state badge for $color`, - ({ state, label, color }) => { - const container = renderComponent({ state }); - const badge = container.getByTestId('badge'); - expect(badge).toBeDefined(); - expect(badge.getAttribute('label')).toBe(label); - expect(badge.getAttribute('color')).toBe(color); - }, - ); -}); diff --git a/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.tsx b/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.tsx deleted file mode 100644 index 1a9bfe3caade..000000000000 --- a/packages/manager-react-components/src/components/ServiceStateBadge/ServiceStateBadge.component.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { NAMESPACES } from '@ovh-ux/manager-common-translations'; -import { useTranslation } from 'react-i18next'; -import { OdsBadge } from '@ovhcloud/ods-components/react'; -import { ResourceStatus } from '../../hooks/services/services.type'; - -export type ServiceStateBadgeProps = Omit< - React.ComponentProps, - 'color' | 'label' -> & { - state: ResourceStatus; -}; - -export const STATES = { - active: { label: 'service_state_active', color: 'success' }, - deleted: { label: 'service_state_deleted', color: 'critical' }, - suspended: { label: 'service_state_suspended', color: 'warning' }, - toActivate: { label: 'service_state_toActivate', color: 'information' }, - toDelete: { label: 'service_state_toDelete', color: 'information' }, - toSuspend: { label: 'service_state_toSuspend', color: 'information' }, -} as const; - -export const ServiceStateBadge = ({ - state, - ...rest -}: ServiceStateBadgeProps) => { - const { t } = useTranslation(NAMESPACES.SERVICE); - - const { label, color } = STATES[state] ?? { - label: state, - color: 'information', - }; - - return ; -}; diff --git a/packages/manager-react-components/src/components/action-banner/action-banner.component.tsx b/packages/manager-react-components/src/components/action-banner/action-banner.component.tsx deleted file mode 100644 index 935b7d97c643..000000000000 --- a/packages/manager-react-components/src/components/action-banner/action-banner.component.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { - OdsButton, - OdsLink, - OdsMessage, - OdsText, -} from '@ovhcloud/ods-components/react'; -import { - ODS_MESSAGE_COLOR, - ODS_MESSAGE_VARIANT, -} from '@ovhcloud/ods-components'; - -export type ActionBannerProps = { - message: string; - cta: string; - variant?: ODS_MESSAGE_VARIANT; - color?: ODS_MESSAGE_COLOR; - onClick?: () => void; - href?: string; - className?: string; - isDismissible?: boolean; -}; - -export function ActionBanner({ - message, - cta, - color, - onClick, - href, - variant, - isDismissible = false, -}: Readonly) { - return ( - -
- - - - {onClick && ( - - )} - {href && ( - - )} -
-
- ); -} diff --git a/packages/manager-react-components/src/components/action-banner/action-banner.scss b/packages/manager-react-components/src/components/action-banner/action-banner.scss deleted file mode 100644 index 86849bf529a5..000000000000 --- a/packages/manager-react-components/src/components/action-banner/action-banner.scss +++ /dev/null @@ -1,23 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -.action-banner.action-banner—-success .action-banner__text::part(text) { - color: var(--ods-color-success-700); -} - -.action-banner.action-banner—-warning .action-banner__text::part(text) { - color: var(--ods-color-warning-700); -} - -.action-banner.action-banner—-information .action-banner__text::part(text) { - color: var(--ods-color-information-700); -} - -.action-banner.action-banner—-danger .action-banner__text::part(text) { - color: var(--ods-color-critical-700); -} - -.action-banner.action-banner—-critical .action-banner__text::part(text) { - color: var(--ods-color-critical-700); -} diff --git a/packages/manager-react-components/src/components/action-banner/action-banner.spec.tsx b/packages/manager-react-components/src/components/action-banner/action-banner.spec.tsx deleted file mode 100644 index ea330b659776..000000000000 --- a/packages/manager-react-components/src/components/action-banner/action-banner.spec.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { fireEvent, screen, render, act } from '@testing-library/react'; -import { ActionBanner, ActionBannerProps } from './action-banner.component'; - -const renderComponent = (props: ActionBannerProps) => - render(); - -describe('ActionBanner tests', () => { - it('should display message', () => { - renderComponent({ - message: 'hello world', - cta: 'custom action', - onClick: () => {}, - }); - - expect(screen.getAllByText('hello world')).not.toBeNull(); - }); - - it('should have a working call to action button', () => { - const mockOnClick = vitest.fn(); - - renderComponent({ - message: 'hello world', - cta: 'custom action', - onClick: mockOnClick, - }); - expect(screen.getByTestId('actionBanner-button')).not.toBeNull(); - const cta = screen.queryByTestId('actionBanner-button'); - expect(mockOnClick).not.toHaveBeenCalled(); - act(() => fireEvent.click(cta)); - expect(mockOnClick).toHaveBeenCalled(); - }); - - it('should have a link action', () => { - const href = 'www.ovhcloud.com'; - renderComponent({ - message: 'hello world', - cta: 'custom action', - href, - }); - const link = screen.queryByTestId('action-banner-link'); - expect(link).toBeDefined(); - expect(link.getAttribute('href')).toBe(href); - expect(link.getAttribute('target')).toBe('_blank'); - }); -}); diff --git a/packages/manager-react-components/src/components/action-banner/index.ts b/packages/manager-react-components/src/components/action-banner/index.ts deleted file mode 100644 index a5e7dc4efaf5..000000000000 --- a/packages/manager-react-components/src/components/action-banner/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ActionBanner } from './action-banner.component'; diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_de_DE.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_de_DE.json deleted file mode 100644 index 36961fd3befe..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_de_DE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Aktivieren" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_en_GB.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_en_GB.json deleted file mode 100644 index 1c09686c44a4..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_en_GB.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Enable" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_es_ES.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_es_ES.json deleted file mode 100644 index dd68ece45513..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_es_ES.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Activar" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_it_IT.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_it_IT.json deleted file mode 100644 index a0789496f3d0..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_it_IT.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Attivare" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pl_PL.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pl_PL.json deleted file mode 100644 index dd0353b9443d..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pl_PL.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Włącz" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pt_PT.json b/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pt_PT.json deleted file mode 100644 index c409e165cb09..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/discovery/Messages_pt_PT.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "pci_projects_project_activate_project_banner_cta": "Ativar" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_de_DE.json b/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_de_DE.json deleted file mode 100644 index a4f548328b85..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_de_DE.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "pci_free_local_zones_banner_text": "Lassen Sie sich unser Einführungsangebot nicht entgehen: Alle Ihre Local Zones Dienste sind bis zum 31. August kostenlos!", - "pci_free_local_zones_banner_text_confirm": "Mit unserem Einführungsangebot für Local Zones sind alle Ihre Local Zones Dienste bis zum 31. August kostenlos!", - "pci_free_local_zones_banner_link": "Mehr erfahren" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_en_GB.json b/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_en_GB.json deleted file mode 100644 index f1f1c0a64cd0..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_en_GB.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "pci_free_local_zones_banner_text": "Don’t miss our special launch offer! Get all our Local Zones services free until 31 August", - "pci_free_local_zones_banner_text_confirm": "As part of our Local Zones launch offer, you can enjoy free access to Local Zones until 31 August!", - "pci_free_local_zones_banner_link": "Find out more" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_es_ES.json b/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_es_ES.json deleted file mode 100644 index 9f3dc2ddab02..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_es_ES.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "pci_free_local_zones_banner_text": "No se pierda nuestra oferta de lanzamiento: ¡disfrute gratis de todos sus servicios Local Zones hasta el 31 de agosto!", - "pci_free_local_zones_banner_text_confirm": "De acuerdo con la oferta de lanzamiento de nuestras Local Zones, ¡podrá disfrutar gratis de todos sus servicios Local Zones hasta el 31 de agosto!", - "pci_free_local_zones_banner_link": "Más información" -} diff --git a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_pl_PL.json b/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_pl_PL.json deleted file mode 100644 index e2a1e346aa56..000000000000 --- a/packages/manager-react-components/src/components/action-banner/pci/translations/free-localzones/Messages_pl_PL.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "pci_free_local_zones_banner_text": "Nie przegap oferty startowej: wszystkie usługi Local Zones są bezpłatne do 31 sierpnia!", - "pci_free_local_zones_banner_text_confirm": "Oferta startowa Local Zones: do 31 sierpnia korzystaj bezpłatnie z wszystkich usług Local Zones!", - "pci_free_local_zones_banner_link": "Dowiedz się więcej" -} diff --git a/packages/manager-react-components/src/components/badge/badge.component.tsx b/packages/manager-react-components/src/components/badge/badge.component.tsx deleted file mode 100644 index a884d76d9492..000000000000 --- a/packages/manager-react-components/src/components/badge/badge.component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ComponentProps } from 'react'; -import { OdsBadge, OdsSkeleton } from '@ovhcloud/ods-components/react'; - -export interface BadgeProps extends ComponentProps { - isLoading?: boolean; - 'data-testid'?: string; -} - -export const Badge = ({ isLoading, ...props }: BadgeProps) => { - return isLoading ? ( - - ) : ( - - ); -}; diff --git a/packages/manager-react-components/src/components/badge/badge.spec.tsx b/packages/manager-react-components/src/components/badge/badge.spec.tsx deleted file mode 100644 index be98f4b7dfa3..000000000000 --- a/packages/manager-react-components/src/components/badge/badge.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { waitFor, screen } from '@testing-library/react'; -import { - ODS_BADGE_COLOR, - ODS_BADGE_SIZE, - ODS_ICON_NAME, -} from '@ovhcloud/ods-components'; -import { render } from '../../utils/test.provider'; -import { Badge } from './badge.component'; - -describe('Badge component', () => { - it('should render badge with correct props', async () => { - render( - , - ); - await waitFor(() => { - const component = screen.getByTestId('test'); - expect(component.hasAttribute(ODS_BADGE_COLOR.information)); - expect(component.hasAttribute('active')); - }); - }); - it('should render OdsSkeleton when isLoading is true', async () => { - render(); - await waitFor(() => { - const skeleton = screen.getByTestId('skeleton'); - expect(skeleton).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/badge/index.ts b/packages/manager-react-components/src/components/badge/index.ts deleted file mode 100644 index 613ec25be2e0..000000000000 --- a/packages/manager-react-components/src/components/badge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './badge.component'; diff --git a/packages/manager-react-components/src/components/breadcrumb/breadcrumb.component.tsx b/packages/manager-react-components/src/components/breadcrumb/breadcrumb.component.tsx deleted file mode 100644 index 3773df5ac92e..000000000000 --- a/packages/manager-react-components/src/components/breadcrumb/breadcrumb.component.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { - OdsBreadcrumb, - OdsBreadcrumbItem, -} from '@ovhcloud/ods-components/react'; -import { useBreadcrumb } from '../../hooks'; - -export interface BreadcrumbProps { - /** root label step */ - rootLabel: string; - /** application name define in the shell */ - appName: string; - /** hides app name from breadcrumb */ - hideRootLabel?: boolean; -} - -export const Breadcrumb: React.FC = ({ - rootLabel, - appName, - hideRootLabel = false, -}) => { - const breadcrumbItems = useBreadcrumb({ - rootLabel, - appName, - hideRootLabel, - }); - return ( - - {breadcrumbItems - ?.filter((item) => !item.hideLabel) - ?.map((item) => ( - - ))} - - ); -}; diff --git a/packages/manager-react-components/src/components/breadcrumb/breadcrumb.spec.tsx b/packages/manager-react-components/src/components/breadcrumb/breadcrumb.spec.tsx deleted file mode 100644 index b60ca1e939c9..000000000000 --- a/packages/manager-react-components/src/components/breadcrumb/breadcrumb.spec.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { vitest } from 'vitest'; -import { waitFor } from '@testing-library/react'; -import { render } from '../../utils/test.provider'; -import { Breadcrumb } from './breadcrumb.component'; - -const setupSpecTest = async ({ hideRootLabel }) => - waitFor(() => - render( - , - ), - ); - -describe('breadcrumb component', () => { - beforeEach(() => { - vitest.mock('../../hooks/breadcrumb/useBreadcrumb', () => ({ - useBreadcrumb: vitest.fn(({ hideRootLabel }) => [ - { label: 'vRack services', href: '/', hideLabel: hideRootLabel }, - { label: 'vRack service', href: '/:id', hideLabel: false }, - { - label: 'vRack service listing', - href: '/:id/listing', - hideLabel: false, - }, - ]), - })); - }); - - it('should render value 3 breadcrumb items', async () => { - const { container } = await setupSpecTest({ hideRootLabel: false }); - const items = container.querySelectorAll('ods-breadcrumb-item'); - expect(items.length).toBe(3); - expect(items[0]).toBeVisible(); - expect(items[1]).toBeVisible(); - expect(items[2]).toBeVisible(); - }); - - it('should hide root label', async () => { - const { container } = await setupSpecTest({ hideRootLabel: true }); - const items = container.querySelectorAll('ods-breadcrumb-item'); - expect(items.length).toBe(2); - }); -}); diff --git a/packages/manager-react-components/src/components/clipboard/clipboard.component.tsx b/packages/manager-react-components/src/components/clipboard/clipboard.component.tsx deleted file mode 100644 index 32674aa4f7af..000000000000 --- a/packages/manager-react-components/src/components/clipboard/clipboard.component.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { JSX } from '@ovhcloud/ods-components'; -import { OdsClipboard } from '@ovhcloud/ods-components/react'; -import React, { HTMLAttributes, RefAttributes } from 'react'; -import { useTranslation } from 'react-i18next'; -import { StyleReactProps } from '@ovhcloud/ods-components/react/dist/types/react-component-lib/interfaces'; -import './translations'; - -export const Clipboard: React.FC< - Partial< - JSX.OdsClipboard & - HTMLAttributes & - StyleReactProps & - RefAttributes - > -> = (props) => { - const { t } = useTranslation('clipboard'); - - return ( - - ); -}; diff --git a/packages/manager-react-components/src/components/clipboard/clipboard.spec.tsx b/packages/manager-react-components/src/components/clipboard/clipboard.spec.tsx deleted file mode 100644 index 1d8c5aeb51fa..000000000000 --- a/packages/manager-react-components/src/components/clipboard/clipboard.spec.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { waitFor } from '@testing-library/react'; -import { render } from '../../utils/test.provider'; -import { Clipboard } from './clipboard.component'; - -const setupSpecTest = async () => - waitFor(() => render()); - -describe('Clipboard', () => { - it('should render value', async () => { - const { getByTestId } = await setupSpecTest(); - const clipboard = getByTestId('clipboard'); - expect(clipboard).toHaveValue('Value to copy to clipboard'); - }); -}); diff --git a/packages/manager-react-components/src/components/container/index.ts b/packages/manager-react-components/src/components/container/index.ts deleted file mode 100644 index 9d1a7386f8ab..000000000000 --- a/packages/manager-react-components/src/components/container/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './step/Step.component'; -export * from './tabs/Tabs.component'; diff --git a/packages/manager-react-components/src/components/container/step/Step.component.tsx b/packages/manager-react-components/src/components/container/step/Step.component.tsx deleted file mode 100644 index f7d141110e14..000000000000 --- a/packages/manager-react-components/src/components/container/step/Step.component.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { Suspense } from 'react'; -import { OdsButton, OdsIcon, OdsSpinner } from '@ovhcloud/ods-components/react'; -import { v4 as uuidV4 } from 'uuid'; -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, - ODS_SPINNER_SIZE, -} from '@ovhcloud/ods-components'; -import { clsx } from 'clsx'; - -export type TStepProps = { - id?: string; - title?: string | JSX.Element; - subtitle?: string | JSX.Element; - isOpen: boolean; - isChecked: boolean; - isLocked: boolean; - order: number; - next?: { - action: (id: string) => void; - label: string | JSX.Element; - isDisabled?: boolean; - isLoading?: boolean; - }; - edit?: { - action: (id: string) => void; - label: string | JSX.Element; - isDisabled?: boolean; - }; - skip?: { - action: (id: string) => void; - label: string | JSX.Element; - isDisabled?: boolean; - hint?: string; - }; - children?: JSX.Element | JSX.Element[]; -}; - -export const StepComponent = ({ - id = uuidV4(), - title = '', - subtitle = '', - isOpen, - isChecked, - isLocked, - order, - children, - next, - edit, - skip, -}: TStepProps): JSX.Element => { - return ( -
-
- {isChecked ? ( - - ) : ( -
- - {order} - -
- )} -
-
-
-
- {title} - {skip?.hint &&
{skip.hint}
} -
- {edit?.action && isLocked && ( -
- { - if (!edit.isDisabled) { - edit.action(id); - } - }} - /> -
- )} -
- {isOpen && ( - <> - {subtitle &&
{subtitle}
} -
- }> - {children} - -
- {!isLocked && ( -
- {next?.action && !isLocked && ( -
- {typeof next.label === 'string' ? ( - { - next.action(id); - }} - className="w-fit" - isDisabled={next.isDisabled || undefined} - isLoading={next.isLoading || false} - /> - ) : ( - next.label - )} -
- )} - {skip?.action && ( -
- { - skip.action(id); - }} - className="w-fit" - isDisabled={skip.isDisabled || undefined} - /> -
- )} -
- )} - - )} -
-
- ); -}; - -export default StepComponent; diff --git a/packages/manager-react-components/src/components/container/tabs/Tabs.component.tsx b/packages/manager-react-components/src/components/container/tabs/Tabs.component.tsx deleted file mode 100644 index 53703b4ea886..000000000000 --- a/packages/manager-react-components/src/components/container/tabs/Tabs.component.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useMedia } from 'react-use'; -import { clsx } from 'clsx'; -import { v4 as uuidV4 } from 'uuid'; -import { OdsIcon, OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_ICON_NAME } from '@ovhcloud/ods-components'; -import { hashCode } from '../../../utils'; - -type TProps = { - id?: string; - items?: Item[]; - itemKey?: (item: Item) => string; - titleElement?: (item: Item, isSelected?: boolean) => JSX.Element | string; - contentElement?: (item: Item) => JSX.Element; - mobileBreakPoint?: number; - className?: string; - onChange?: (item: Item) => void; -}; - -type TState = { - items: Item[]; - selectedItem?: Item; -}; - -export function TabsComponent({ - id = uuidV4(), - items = [], - itemKey, - titleElement = (item) =>
{`title ${item}`}
, - contentElement = (item) =>
{`content ${item}`}
, - mobileBreakPoint, - className, - onChange, -}: TProps): JSX.Element { - const [state, setState] = useState>({ - items, - selectedItem: items?.[0], - }); - - const setSelectedItem = (item: Item): void => { - setState((prev) => ({ - ...prev, - selectedItem: item, - })); - }; - - const uniqKey = (item: Item): string => - itemKey ? itemKey(item) : `${hashCode(item)}`; - - useEffect(() => { - if ( - Array.isArray(items) && - items.length && - (items.length !== state.items.length || - items.some((item, index) => !Object.is(item, state.items[index]))) - ) { - setState(() => ({ - items, - selectedItem: items[0], - })); - } - }, [items]); - - useEffect(() => { - if (typeof onChange === 'function') { - onChange(state.selectedItem); - } - }, [state.selectedItem]); - - const isDesktop = useMedia(`(min-width: ${mobileBreakPoint || 760}px)`); - - return ( - <> - {isDesktop ? ( -
-
    - {state.items.map((item) => ( -
  • - -
  • - ))} -
  • -
-
- {contentElement(state.selectedItem)} -
-
- ) : ( -
- {state.items.map((item) => ( -
- - {Object.is(state.selectedItem, item) && ( -
{contentElement(item)}
- )} -
- ))} -
- )} - - ); -} - -export default TabsComponent; diff --git a/packages/manager-react-components/src/components/content/ManagerTile/help-icon-with-tooltip.tsx b/packages/manager-react-components/src/components/content/ManagerTile/help-icon-with-tooltip.tsx deleted file mode 100644 index c015059779ba..000000000000 --- a/packages/manager-react-components/src/components/content/ManagerTile/help-icon-with-tooltip.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useId } from 'react'; -import { ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; -import { OdsText, OdsIcon, OdsTooltip } from '@ovhcloud/ods-components/react'; - -type HelpIconWithTooltipProps = { - label: string; -}; - -export const HelpIconWithTooltip = ({ label }: HelpIconWithTooltipProps) => { - const tooltipId = useId(); - - return ( - <> - - - - {label} - - - - ); -}; diff --git a/packages/manager-react-components/src/components/content/ManagerTile/manager-tile-item.component.tsx b/packages/manager-react-components/src/components/content/ManagerTile/manager-tile-item.component.tsx deleted file mode 100644 index 998a9fa571e0..000000000000 --- a/packages/manager-react-components/src/components/content/ManagerTile/manager-tile-item.component.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { PropsWithChildren } from 'react'; -import { ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import { HelpIconWithTooltip } from './help-icon-with-tooltip'; - -export const ManagerTileItem = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -type ManagerTileItemLabelProps = PropsWithChildren<{ tooltip?: string }>; - -const ManagerTileItemLabel = ({ - children, - tooltip, -}: ManagerTileItemLabelProps) => { - return ( -
- {children} - {tooltip && } -
- ); -}; - -const ManagerTileItemDescription = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -ManagerTileItem.Label = ManagerTileItemLabel; -ManagerTileItem.Description = ManagerTileItemDescription; diff --git a/packages/manager-react-components/src/components/content/ManagerTile/manager-tile.component.tsx b/packages/manager-react-components/src/components/content/ManagerTile/manager-tile.component.tsx deleted file mode 100644 index 8e15ae28fd9b..000000000000 --- a/packages/manager-react-components/src/components/content/ManagerTile/manager-tile.component.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { OdsDivider, OdsCard, OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_CARD_COLOR, ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; -import { ManagerTileItem } from './manager-tile-item.component'; - -export type ManagerTileProps = React.ComponentProps; - -export const ManagerTile = ({ - className, - children, - ...props -}: ManagerTileProps) => { - return ( - -
{children}
-
- ); -}; - -type ManagerTileTitleProps = React.PropsWithChildren; -const ManagerTileTitle = ({ children }: ManagerTileTitleProps) => { - return {children}; -}; - -const ManagerTileDivider = () => ; - -ManagerTile.Title = ManagerTileTitle; -ManagerTile.Item = ManagerTileItem; -ManagerTile.Divider = ManagerTileDivider; diff --git a/packages/manager-react-components/src/components/content/ManagerTile/tile.spec.tsx b/packages/manager-react-components/src/components/content/ManagerTile/tile.spec.tsx deleted file mode 100644 index da4528827596..000000000000 --- a/packages/manager-react-components/src/components/content/ManagerTile/tile.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { waitFor, screen } from '@testing-library/react'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import { render } from '../../../utils/test.provider'; -import { ManagerTile } from './manager-tile.component'; - -const testItem = ( - - label - - value - - -); - -describe('Manager Tile component', () => { - it('renders correctly', async () => { - render( - - Title - - {testItem} - , - ); - await waitFor(() => { - expect(screen.getByText('Title')).toBeInTheDocument(); - expect(screen.getByText('value')).toBeInTheDocument(); - expect(screen.getByText('label')).toBeInTheDocument(); - }); - }); - - it('renders correctly without items', async () => { - render( - - Title 2 - , - ); - await waitFor(() => { - expect(screen.getByText('Title 2')).toBeInTheDocument(); - }); - }); - - it('renders correctly without title', async () => { - render({testItem}); - await waitFor(() => { - expect(screen.getByText('value')).toBeInTheDocument(); - expect(screen.getByText('label')).toBeInTheDocument(); - }); - }); - - it('renders correctly without label', async () => { - render( - - - value - - , - ); - await waitFor(() => { - expect(screen.getByText('value')).toBeInTheDocument(); - expect(screen.queryByText('label')).not.toBeInTheDocument(); - }); - }); - - it('renders a tooltip on labels correctly', async () => { - render( - - - - label - - - , - ); - - expect(screen.getByText('This is a tooltip')).toBeInTheDocument(); - }); -}); diff --git a/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.component.tsx b/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.component.tsx deleted file mode 100644 index 7fe359339e2f..000000000000 --- a/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.component.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { OdsDivider, OdsCard } from '@ovhcloud/ods-components/react'; -import { ODS_CARD_COLOR } from '@ovhcloud/ods-components'; -import { TileBlock, TileBlockOptions } from './tile-block.component'; - -export type DashboardTileBlockItem = { - id: string; - value: React.ReactNode; -} & TileBlockOptions; - -export type DashboardTileProps = { - title?: string; - items: DashboardTileBlockItem[]; - 'data-testid'?: string; -}; - -/** - * DashboardTile - * @deprecated component, use ManagerTile instead. - */ -export const DashboardTile: React.FC = ({ - title, - items, - ...props -}) => ( - -
- {title && ( - <> -

- {title} -

- - - )} - {items.map((item, index) => ( - - - {item.value} - - {index < items.length - 1 && } - - ))} -
-
-); diff --git a/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.spec.tsx b/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.spec.tsx deleted file mode 100644 index 86656e0ffa54..000000000000 --- a/packages/manager-react-components/src/components/content/dashboard-tile/dashboard-tile.spec.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { waitFor, screen } from '@testing-library/react'; -import { render } from '../../../utils/test.provider'; -import { DashboardTile } from './dashboard-tile.component'; - -const testItem = { - id: 'id', - label: 'label', - value: 'value', -}; - -describe('Dashboard Tile component', () => { - it('renders correctly', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('Title')).toBeInTheDocument(); - expect(screen.getByText(testItem.value)).toBeInTheDocument(); - expect(screen.getByText(testItem.label)).toBeInTheDocument(); - expect(screen.queryByText(testItem.id)).not.toBeInTheDocument(); - }); - }); - - it('renders correctly without items', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('Title 2')).toBeInTheDocument(); - }); - }); - - it('renders correctly without title', async () => { - render(); - await waitFor(() => { - expect(screen.getByText(testItem.value)).toBeInTheDocument(); - expect(screen.getByText(testItem.label)).toBeInTheDocument(); - expect(screen.queryByText(testItem.id)).not.toBeInTheDocument(); - }); - }); - - it('renders correctly without label', async () => { - render(); - await waitFor(() => { - expect(screen.getByText(testItem.value)).toBeInTheDocument(); - expect(screen.queryByText(testItem.label)).not.toBeInTheDocument(); - expect(screen.queryByText(testItem.id)).not.toBeInTheDocument(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/content/dashboard-tile/tile-block.component.tsx b/packages/manager-react-components/src/components/content/dashboard-tile/tile-block.component.tsx deleted file mode 100644 index bcb9c7f93e81..000000000000 --- a/packages/manager-react-components/src/components/content/dashboard-tile/tile-block.component.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * TileBlock will be removed in MRC V3 - * @deprecated use the new Tile Component instead - */ -import { PropsWithChildren } from 'react'; -import { ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import { HelpIconWithTooltip } from '../ManagerTile/help-icon-with-tooltip'; - -/** - * TileBlock will be removed in MRC V3 - * @deprecated use the new Tile Component instead - */ -export type TileBlockOptions = { - label?: string; - labelTooltip?: string; -}; - -/** - * TileBlock will be removed in MRC V3 - * @deprecated use the new Tile Component instead - */ -export type TileBlockProps = PropsWithChildren; - -/** - * TileBlock will be removed in MRC V3 - * @deprecated use the new Tile Component instead - */ -export const TileBlock = ({ - label, - labelTooltip, - children, -}: TileBlockProps) => ( -
-
- {label && ( -
- {label} - {labelTooltip && } -
- )} -
-
{children}
-
-); diff --git a/packages/manager-react-components/src/components/content/headers/headers.component.tsx b/packages/manager-react-components/src/components/content/headers/headers.component.tsx deleted file mode 100644 index 2d15afff2c43..000000000000 --- a/packages/manager-react-components/src/components/content/headers/headers.component.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { ReactNode } from 'react'; -import { OdsBadge, OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_BADGE_COLOR, ODS_BADGE_SIZE } from '@ovhcloud/ods-components'; -import { Subtitle } from '../../typography'; - -export interface HeadersProps { - title?: string; - badge?: { - color: ODS_BADGE_COLOR; - size: ODS_BADGE_SIZE; - label: string; - }; - subtitle?: string; - description?: string; - headerButton?: React.ReactElement; - changelogButton?: React.ReactElement; -} - -export const Headers: React.FC = ({ - title, - badge, - subtitle, - description, - headerButton, - changelogButton, -}) => { - return ( -
-
-
- {title && {title}} - {badge && ( - - )} -
- {subtitle && {subtitle}} - {description && ( - - {description} - - )} -
- {(headerButton || changelogButton) && ( -
- {changelogButton} - {headerButton} -
- )} -
- ); -}; - -export default Headers; diff --git a/packages/manager-react-components/src/components/content/headers/headers.spec.tsx b/packages/manager-react-components/src/components/content/headers/headers.spec.tsx deleted file mode 100644 index 659d04004192..000000000000 --- a/packages/manager-react-components/src/components/content/headers/headers.spec.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { vitest, vi } from 'vitest'; -import { waitFor, screen } from '@testing-library/react'; -import { render } from '../../../utils/test.provider'; -import { Headers } from './headers.component'; -import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; -import { useAuthorizationIam } from '../../../hooks/iam'; -import { GuideButton, ActionMenu } from '../../navigation'; - -export const header = () => ( - -); -export const subHeader = () => ( - -); -export const headerWithHeaderButtons = () => ( - - } - /> -); -export const headerWithActions = () => ( - - } - /> -); - -vitest.mock('../../../hooks/iam'); -vitest.mock('@ovh-ux/manager-react-shell-client', () => ({ - useOvhTracking: () => ({ - trackClick: vi.fn(), - }), -})); -vitest.mock('storybook-addon-react-router-v6', () => ({ - withRouter: () => vi.fn(), -})); - -const mockedHook = - useAuthorizationIam as unknown as jest.Mock; - -describe('Headers component', () => { - beforeEach(() => { - mockedHook.mockReturnValue({ - isAuthorized: true, - isLoading: true, - isFetched: true, - }); - }); - - it('renders header correctly', async () => { - render(header()); - await waitFor(() => { - expect(screen.getByText('Example for header')).toBeInTheDocument(); - expect(screen.getByText('description for header')).toBeInTheDocument(); - }); - }); - - it('renders subHeader correctly', async () => { - render(subHeader()); - await waitFor(() => { - expect(screen.getByText('Example for subHeader')).toBeInTheDocument(); - expect(screen.getByText('description for subheader')).toBeInTheDocument(); - }); - }); - - it('renders header with header buttons correctly', async () => { - render(headerWithHeaderButtons()); - await waitFor(() => { - expect( - screen.getByText('Example for header with header buttons'), - ).toBeInTheDocument(); - expect(screen.getByText('description for subheader')).toBeInTheDocument(); - }); - }); - - it('renders header with actions correctly', async () => { - render(headerWithActions()); - await waitFor(() => { - expect( - screen.getByText('Example for header with actions'), - ).toBeInTheDocument(); - expect(screen.getByText('description for header')).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/content/index.ts b/packages/manager-react-components/src/components/content/index.ts deleted file mode 100644 index d7cd5ea485a2..000000000000 --- a/packages/manager-react-components/src/components/content/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './headers/headers.component'; -export * from './price/price.component'; -export * from './dashboard-tile/dashboard-tile.component'; -export * from './dashboard-tile/tile-block.component'; -export * from './ManagerTile/manager-tile.component'; diff --git a/packages/manager-react-components/src/components/content/price/price.component.tsx b/packages/manager-react-components/src/components/content/price/price.component.tsx deleted file mode 100644 index f75962c90d34..000000000000 --- a/packages/manager-react-components/src/components/content/price/price.component.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { OdsText } from '@ovhcloud/ods-components/react'; - -import { IntervalUnitType } from '../../../enumTypes'; -import { - getPrice, - convertIntervalPrice, - getPriceTextFormatted, - PriceProps, -} from './price.utils'; -import './translations/translations'; - -const TextPriceContent: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => {children}; - -export function Price({ - value, - intervalUnit, - tax, - ovhSubsidiary, - locale, - isConvertIntervalUnit, -}: Readonly) { - const { t } = useTranslation('price'); - const isAsiaFormat = ['ASIA', 'AU', 'IN', 'SG'].includes(ovhSubsidiary); - const isGermanFormat = ['DE', 'FI', 'SN'].includes(ovhSubsidiary); - const isFrenchFormat = [ - 'CZ', - 'ES', - 'FR', - 'GB', - 'IE', - 'IT', - 'LT', - 'MA', - 'NL', - 'PL', - 'PT', - 'TN', - ].includes(ovhSubsidiary); - const isUSFormat = ['CA', 'QC', 'US', 'WE', 'WS'].includes(ovhSubsidiary); - - const convertedValue = isConvertIntervalUnit - ? convertIntervalPrice(value, intervalUnit) - : value; - const convertedTax = isConvertIntervalUnit - ? convertIntervalPrice(tax || 0, intervalUnit) - : tax || 0; - - const priceWithoutTax = getPriceTextFormatted( - ovhSubsidiary, - locale, - getPrice(convertedValue), - ); - const priceWithTax = getPriceTextFormatted( - ovhSubsidiary, - locale, - getPrice(convertedValue, convertedTax), - ); - const intervalUnitText = - intervalUnit && intervalUnit !== IntervalUnitType.none - ? t(`price_per_${intervalUnit}`) - : ''; - const components = [ - { - condition: value === 0, - component: {t('price_free')}, - }, - { - condition: isFrenchFormat && tax > 0, - component: ( - <> - - {priceWithoutTax} - - - {t('price_ht_label')} - - - {intervalUnitText} - - - - ({priceWithTax} - - - {t('price_ttc_label')}) - - - - ), - }, - { - condition: isFrenchFormat && !tax, - component: ( - <> - - {priceWithoutTax} - - - {t('price_ht_label')} - - - {intervalUnitText} - - - ), - }, - { - condition: isGermanFormat && tax > 0, - component: ( - <> - - {priceWithTax} - - - {intervalUnitText} - - - ), - }, - { - condition: isAsiaFormat && (!tax || tax === 0), - component: ( - <> - - {priceWithoutTax} - - - {t('price_gst_excl_label')} - - - {intervalUnitText} - - - ), - }, - { - condition: isAsiaFormat, - component: ( - <> - - {priceWithoutTax} - - - {t('price_gst_excl_label')} - - - {intervalUnitText} - - - - ({priceWithTax} - - - {t('price_gst_incl_label')}) - - - - ), - }, - { - condition: isUSFormat, - component: ( - <> - - {priceWithoutTax} - - - {intervalUnitText} - - - ), - }, - ]; - - const matchingComponent = components.find(({ condition }) => condition); - if (!matchingComponent) { - return <>; - } - - return {matchingComponent.component}; -} - -export default Price; diff --git a/packages/manager-react-components/src/components/content/price/price.spec.tsx b/packages/manager-react-components/src/components/content/price/price.spec.tsx deleted file mode 100644 index 9eca7b5482c6..000000000000 --- a/packages/manager-react-components/src/components/content/price/price.spec.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import Price from './price.component'; -import { render } from '../../../utils/test.provider'; -import { IntervalUnitType, OvhSubsidiary } from '../../../enumTypes'; - -describe('Price component', () => { - const renderPriceComponent = (props: React.ComponentProps) => { - render(); - }; - - const baseProps = { - value: 3948000000, - ovhSubsidiary: OvhSubsidiary.FR, - intervalUnit: IntervalUnitType.month, - }; - const priceDefault = '39,48 €'; - const priceHtMonth = '39,48 €HT/mois'; - const priceTTC = '47,38 €'; - const taxNumber = 789600000; - const localeFr = 'fr-FR'; - it('renders "Inclus" when value is 0', () => { - const props = { - ...baseProps, - value: 0, - intervalUnit: IntervalUnitType.none, - locale: localeFr, - }; - renderPriceComponent(props); - const priceElement = screen.getByText('Inclus'); - expect(priceElement.parentElement).toHaveTextContent('Inclus'); - }); - - it('renders a price with locale of the form xx_XX correctly', () => { - const props = { ...baseProps, locale: 'fr_FR' }; - renderPriceComponent(props); - const priceElement = screen.getByText(priceDefault); - expect(priceElement.parentElement).toHaveTextContent(priceHtMonth); - }); - - it('renders a price for HT correctly', () => { - const props = { ...baseProps, locale: localeFr }; - renderPriceComponent(props); - const priceElement = screen.getByText(priceDefault); - expect(priceElement.parentElement).toHaveTextContent(priceHtMonth); - }); - - it('if I have a bad local I want it in French', () => { - const props = { ...baseProps, locale: 'toto' }; - renderPriceComponent(props); - const priceElement = screen.getByText(priceDefault); - expect(priceElement.parentElement).toHaveTextContent(priceHtMonth); - }); - - it('renders a price FR for TTC correctly', () => { - const props = { ...baseProps, locale: localeFr, tax: taxNumber }; - renderPriceComponent(props); - const priceElement = screen.getByText(priceDefault); - expect(priceElement.parentElement).toHaveTextContent( - `${priceDefault}HT/mois(${priceTTC}TTC)`, - ); - }); - - it('renders a price US correctly', () => { - const props = { - ...baseProps, - locale: 'en-US', - ovhSubsidiary: OvhSubsidiary.US, - }; - renderPriceComponent(props); - const priceElementTTC = screen.getByText('$39.48'); - expect(priceElementTTC.parentElement).toHaveTextContent('$39.48'); - }); - - it('renders a price ASIA correctly with convert unit month', () => { - const props = { - ...baseProps, - locale: 'en-US', - ovhSubsidiary: OvhSubsidiary.ASIA, - isConvertIntervalUnit: true, - tax: taxNumber, - }; - renderPriceComponent(props); - const priceElementTTC = screen.getByText('$3.29'); - expect(priceElementTTC.parentElement).toHaveTextContent( - '$3.29ex. GST/mois($3.95incl. GST)', - ); - }); - it('renders a price ASIA correctly with convert unit month excl gst', () => { - const props = { - ...baseProps, - locale: 'en-US', - ovhSubsidiary: OvhSubsidiary.ASIA, - isConvertIntervalUnit: true, - }; - renderPriceComponent(props); - const priceElementTTC = screen.getByText('$3.29'); - expect(priceElementTTC.parentElement).toHaveTextContent( - '$3.29ex. GST/mois', - ); - }); - - it('renders a price Deutch correctly', () => { - const props = { - ...baseProps, - locale: 'de-DE', - ovhSubsidiary: OvhSubsidiary.DE, - tax: taxNumber, - }; - renderPriceComponent(props); - const priceElementTTC = screen.getByText(priceTTC); - expect(priceElementTTC.parentElement).toHaveTextContent(priceTTC); - }); -}); diff --git a/packages/manager-react-components/src/components/content/price/price.utils.ts b/packages/manager-react-components/src/components/content/price/price.utils.ts deleted file mode 100644 index e70f02832e84..000000000000 --- a/packages/manager-react-components/src/components/content/price/price.utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - OvhSubsidiary, - IntervalUnitType, - OVH_CURRENCY_BY_SUBSIDIARY, -} from '../../../enumTypes'; - -export interface PriceProps { - /** The price value to display */ - value: number; - /** The tax value to display */ - tax?: number; - /** The interval unit for the price (day, month, year) */ - intervalUnit?: IntervalUnitType; - /** The OVH subsidiary to determine price format */ - ovhSubsidiary: OvhSubsidiary; - /** Whether to convert the price based on interval unit */ - isConvertIntervalUnit?: boolean; - /** The locale for price formatting */ - locale: string; -} - -export const getPrice = (value: number, tax?: number): number => { - const valueWithTax = tax ? value + tax : value; - return valueWithTax / 100000000; -}; - -export const convertIntervalPrice = ( - price: number, - intervalUnit: IntervalUnitType, -): number => { - const conversionRates = { - [IntervalUnitType.day]: price / 365, - [IntervalUnitType.month]: price / 12, - [IntervalUnitType.year]: price, - [IntervalUnitType.none]: price, - }; - - return conversionRates[intervalUnit] || price; -}; - -export const getPriceTextFormatted = ( - ovhSubsidiary: OvhSubsidiary, - locale: string, - priceValue: number, -): string => { - try { - return new Intl.NumberFormat(locale.replace('_', '-'), { - style: 'currency', - currency: OVH_CURRENCY_BY_SUBSIDIARY[ovhSubsidiary], - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(priceValue); - } catch (e) { - return new Intl.NumberFormat('fr-FR', { - style: 'currency', - currency: OVH_CURRENCY_BY_SUBSIDIARY[ovhSubsidiary], - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(priceValue); - } -}; diff --git a/packages/manager-react-components/src/components/content/price/translations/translations.ts b/packages/manager-react-components/src/components/content/price/translations/translations.ts deleted file mode 100644 index 453f4846072a..000000000000 --- a/packages/manager-react-components/src/components/content/price/translations/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'price'); diff --git a/packages/manager-react-components/src/components/datagrid/clipboard-cell.component.tsx b/packages/manager-react-components/src/components/datagrid/clipboard-cell.component.tsx deleted file mode 100644 index a896f73335b8..000000000000 --- a/packages/manager-react-components/src/components/datagrid/clipboard-cell.component.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { OdsClipboard, OdsText } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; - -type DataGridClipboardCellProps = { - text: string; -}; - -/** Simple datagrid cell clipboard text formatter applying ODS style */ -export function DataGridClipboardCell({ - text, -}: Readonly) { - const { t } = useTranslation('datagrid'); - return ( - - - {t('common_clipboard_success_label')} - - - {t('common_clipboard_error_label')} - - - ); -} - -export default DataGridClipboardCell; diff --git a/packages/manager-react-components/src/components/datagrid/datagrid-topbar.component.tsx b/packages/manager-react-components/src/components/datagrid/datagrid-topbar.component.tsx deleted file mode 100644 index b48eb92f5c00..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid-topbar.component.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - ODS_ICON_NAME, - ODS_BUTTON_VARIANT, - ODS_BUTTON_SIZE, - ODS_INPUT_TYPE, -} from '@ovhcloud/ods-components'; -import { - OdsPopover, - OdsButton, - OdsInput, -} from '@ovhcloud/ods-components/react'; -import { FilterComparator } from '@ovh-ux/manager-core-api'; -import { FilterWithLabel } from '../filters/interface'; -import { FilterAdd, FilterList } from '../filters'; -import { ColumnFilter } from '../filters/filter-add.component'; -import './translations'; -import { - ColumnsVisibility, - VisibilityManagement, -} from './visibility/visibility-management.component'; - -type ColumnFilterProps = { - key: string; - value: string | string[]; - comparator: FilterComparator; - label: string; -}; - -export interface SearchProps { - searchInput: string; - setSearchInput: React.Dispatch>; - onSearch: (search: string) => void; - placeholder?: string; -} - -export interface FilterProps { - filters: FilterWithLabel[]; - add: (filters: ColumnFilterProps) => void; - remove: (filter: FilterWithLabel) => void; -} - -export interface DatagridTopbarProps { - columnsVisibility?: ColumnsVisibility[]; - toggleAllColumnsVisible?: (a: boolean) => void; - getIsAllColumnsVisible?: () => boolean; - getIsSomeColumnsVisible?: () => boolean; - filtersColumns?: ColumnFilter[]; - isSearchable?: boolean; - filters?: FilterProps; - search?: SearchProps; - topbar?: React.ReactNode; - resourceType?: string; -} - -export const DatagridTopbar = ({ - columnsVisibility, - toggleAllColumnsVisible, - getIsAllColumnsVisible, - getIsSomeColumnsVisible, - filters, - filtersColumns, - isSearchable, - search, - topbar, - resourceType, -}: DatagridTopbarProps) => { - const { t } = useTranslation('filters'); - const filterPopoverRef = useRef(null); - const hasVisibilityFeature = columnsVisibility?.some( - (col) => col.enableHiding, - ); - - return ( - <> - {(isSearchable || - filtersColumns?.length > 0 || - topbar || - hasVisibilityFeature) && ( -
-
- {topbar && <>{topbar}} -
-
-
- {isSearchable && ( -
{ - e.preventDefault(); - search?.onSearch(search?.searchInput); - }} - > - { - search?.onSearch(''); - search?.setSearchInput(''); - }} - type={ODS_INPUT_TYPE.search} - id="datagrid-searchbar" - name="datagrid-searchbar" - placeholder={search?.placeholder} - defaultValue={search?.searchInput} - data-testid="datagrid-searchbar" - onOdsChange={(event) => - search?.setSearchInput( - event?.detail?.value?.toString() || '', - ) - } - value={search?.searchInput} - /> - - )} - {filtersColumns?.length > 0 && ( -
- - - { - filters.add({ - ...addedFilter, - label: column.label, - }); - filterPopoverRef.current?.hide(); - }} - /> - -
- )} - {hasVisibilityFeature && ( -
0 ? 'ml-[10px]' : ''}> - -
- )} -
-
-
- )} - {filters?.filters.length > 0 && ( -
- -
- )} - - ); -}; diff --git a/packages/manager-react-components/src/components/datagrid/datagrid-topbar.spec.tsx b/packages/manager-react-components/src/components/datagrid/datagrid-topbar.spec.tsx deleted file mode 100644 index 083f667cc669..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid-topbar.spec.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { vitest } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { FilterCategories } from '@ovh-ux/manager-core-api'; -import { FilterProps } from './datagrid.component'; -import { DatagridTopbar } from './datagrid-topbar.component'; - -vitest.mock('react-i18next', async () => { - const originalModule = await vitest.importActual('react-i18next'); - - return { - ...originalModule, - useTranslation: () => { - return { - t: (str: string) => str, - i18n: { - changeLanguage: () => new Promise(() => {}), - }, - }; - }, - }; -}); - -vitest.mock('@ovhcloud/ods-components/react', async () => { - const originalModule = await vitest.importActual( - '@ovhcloud/ods-components/react', - ); - - return { - ...originalModule, - OdsCheckbox: (column) => ( - - ), - }; -}); - -const filtersColumns = [ - { - id: 'ip', - label: 'ip', - comparators: FilterCategories.String, - }, - { - id: 'os', - label: 'os', - comparators: FilterCategories.String, - }, - { - id: 'name', - label: 'name', - comparators: FilterCategories.String, - }, -]; - -const filters = { - filters: [ - { - key: 'customName', - comparator: 'includes', - value: 'coucou', - label: 'customName', - }, - ], - add: null, - remove: null, -} as FilterProps; - -const search = { - searchInput: '', - setSearchInput: () => {}, - onSearch: () => {}, -}; - -describe('datagrid topbar', () => { - it('should display the search bar component', async () => { - render(); - const searchElement = screen.queryByTestId('datagrid-searchbar'); - expect(searchElement).toBeInTheDocument(); - }); - - it('should not display the search bar component', async () => { - render(); - const searchElement = screen.queryByTestId('datagrid-searchbar'); - expect(searchElement).not.toBeInTheDocument(); - }); - - it('should display the search bar and filters add component', async () => { - render( - , - ); - const searchElement = screen.queryByTestId('datagrid-searchbar'); - expect(searchElement).toBeInTheDocument(); - const filterElement = screen.queryByTestId('datagrid-topbar-filters'); - expect(filterElement).toBeInTheDocument(); - }); - - it('should display filters add and list component', async () => { - render( - , - ); - const filterElement = screen.queryByTestId('datagrid-topbar-filters'); - expect(filterElement).toBeInTheDocument(); - const filterListElement = screen.queryByTestId('datagrid-filter-list'); - expect(filterListElement).toBeInTheDocument(); - }); - - it('should display only topbar element', async () => { - const topbar =
CTA button
; - render(); - const filterListElement = screen.queryByTestId('custom-topbar-element'); - expect(filterListElement).toBeInTheDocument(); - }); - - it('should display visibility component', () => { - render( - true, - onChange: () => null, - }, - { - id: 'price', - label: 'Price', - enableHiding: true, - isDisabled: false, - isVisible: () => true, - onChange: () => null, - }, - ]} - toggleAllColumnsVisible={() => null} - getIsAllColumnsVisible={() => true} - getIsSomeColumnsVisible={() => true} - />, - ); - - const visibilityElement = screen.queryByTestId( - 'datagrid-topbar-visibility-button', - ); - - expect(visibilityElement).toHaveAttribute('label', 'common_topbar_columns'); - }); -}); diff --git a/packages/manager-react-components/src/components/datagrid/datagrid.component.tsx b/packages/manager-react-components/src/components/datagrid/datagrid.component.tsx deleted file mode 100644 index b2593c94a9b2..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid.component.tsx +++ /dev/null @@ -1,656 +0,0 @@ -import React, { - Fragment, - useCallback, - useEffect, - useMemo, - useRef, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import { - ColumnDef, - ColumnSort as TanstackColumnSort, - PaginationState as TanstackPaginationState, - flexRender, - getCoreRowModel, - useReactTable, - getSortedRowModel, - Row, - VisibilityState, - RowSelectionState, -} from '@tanstack/react-table'; -import { - ODS_ICON_NAME, - ODS_BUTTON_VARIANT, - ODS_BUTTON_SIZE, -} from '@ovhcloud/ods-components'; -import { - OdsButton, - OdsIcon, - OdsPagination, - OdsSkeleton, - OdsTable, -} from '@ovhcloud/ods-components/react'; -import { - FilterCategories, - FilterComparator, - FilterTypeCategories as DatagridColumnTypes, -} from '@ovh-ux/manager-core-api'; -import { clsx } from 'clsx'; -import { Option, ColumnFilter } from '../filters/filter-add.component'; -import { FilterWithLabel } from '../filters/interface'; -import { DataGridTextCell } from './text-cell.component'; -import { defaultNumberOfLoadingRows } from './datagrid.constants'; -import { DatagridTopbar } from './datagrid-topbar.component'; -import './translations'; -import { IndeterminateCheckbox } from './indeterminate-checkbox'; - -export type ColumnSort = TanstackColumnSort; -export type PaginationState = TanstackPaginationState; -export { FilterTypeCategories as DatagridColumnTypes } from '@ovh-ux/manager-core-api'; -// Note: current prettier version does not supports export type -// we could replace those types with : -// export type { ColumnSort } from '@tanstack/react-table'; -// export type { PaginationState } from '@tanstack/react-table'; - -export interface DatagridColumn { - /** unique column identifier */ - id: string; - /** formatter function to render a column cell */ - cell: (props: T) => JSX.Element; - /** label displayed for the column in the table */ - label: string; - /** is the column sortable ? (defaults is true) */ - isSortable?: boolean; - /** set column comparator for the filter */ - comparator?: FilterComparator[]; - /** Filters displayed for the column */ - type?: DatagridColumnTypes; - /** Trigger the column filter */ - isFilterable?: boolean; - /** Trigger the column search */ - isSearchable?: boolean; - /** Set default column size */ - size?: number; - /** filterOptions can be passed to have selector instead of input to choose value */ - filterOptions?: Option[]; - /** Allows the column to be hidden or shown dynamically */ - enableHiding?: boolean; -} - -type ColumnFilterProps = { - key: string; - value: string | string[]; - comparator: FilterComparator; - label: string; -}; - -export interface FilterProps { - filters: FilterWithLabel[]; - add: (filters: ColumnFilterProps) => void; - remove: (filter: FilterWithLabel) => void; -} - -export interface SearchProps { - searchInput: string; - setSearchInput: React.Dispatch>; - onSearch: (search: string) => void; -} - -export interface RowSelectionProps { - rowSelection: RowSelectionState; - setRowSelection: React.Dispatch>; - /** This callback is called every time 1 or multiple rows are selected */ - onRowSelectionChange?: (selectedRows: T[]) => void; - /** when used, for each row if expression is false, the row is disabled */ - enableRowSelection?: (row: Row) => boolean; -} - -export interface DatagridProps { - /** list of datagrid columns */ - columns: DatagridColumn[]; - /** list of items (rows) to display in the table */ - items: T[]; - /** total number of items (in case of pagination) */ - totalItems: number; - /** state of pagination (optional if no pagination is required) */ - pagination?: PaginationState; - /** state of column sorting (optional if column sorting is not required) */ - sorting?: ColumnSort; - /** callback to handle pagination change events (optional if no pagination is required) */ - onPaginationChange?: (pagination: PaginationState) => void; - /** callback to handle column sorting change events (optional if column sorting is not required) */ - onSortChange?: any; - /** option to add custom CSS class */ - className?: string; - /** option to adjust content on the left */ - contentAlignLeft?: boolean; - /** boolean to display load more button, and load all button if onFetchAllPages is defined */ - hasNextPage?: boolean; - /** callback on load more button click */ - onFetchNextPage?: any; - /** callback on load all button click, the button will be displayed only if this function is defined and hasNextPage is true */ - onFetchAllPages?: React.MouseEventHandler; - /** Enables manual sorting for the table */ - manualSorting?: boolean; - /** Enables manual pagination */ - manualPagination?: boolean; - /** If provided, this function will be called with an updaterFn when state.sorting changes. */ - /** setSorting?: OnChangeFn; */ - /** label displayed if there is no item in the datagrid */ - noResultLabel?: string; - /** whether or not the table is in loading state */ - isLoading?: boolean; - /** number of loading rows to show when table is in loading state, defaults to pagination.pageSize or 5 */ - numberOfLoadingRows?: number; - /** List of filters and handlers to add, remove */ - filters?: FilterProps; - /** Trigger the column search. In case of backend search, make sure to add this on columns on which API supports the search option. */ - search?: SearchProps; - /** ids of the columns visible in the datagrid (optional by default all columns are visible) */ - columnVisibility?: string[]; - /** callback to handle column visibility change events (optional) */ - setColumnVisibility?: React.Dispatch>; - /** Add react element at left in the datagrid topbar */ - topbar?: React.ReactNode; - /** Function to render sub component as row child */ - renderSubComponent?: ( - row: Row, - headerRefs?: React.MutableRefObject>, - ) => JSX.Element; - /** function to define if row can be expanded or not */ - getRowCanExpand?: (row: Row) => boolean; - /** Hide datagrid header if true */ - hideHeader?: boolean; - /** Resets the expanded rows when data is updated */ - resetExpandedRowsOnItemsChange?: boolean; - /** When true, will fix the columns size by column definition size */ - tableLayoutFixed?: boolean; - /** To use if tag column is present and filter is enabled. This allows to fetch all tags from iam only for this resource type */ - resourceType?: string; - /** Enable and configure row selection */ - rowSelection?: RowSelectionProps; - /** Use to overwrite row id */ - getRowId?: (originalRow: T, index: number) => string; -} - -export const Datagrid = ({ - columns, - columnVisibility, - setColumnVisibility, - items, - filters, - search, - topbar, - totalItems, - pagination, - sorting, - className, - onPaginationChange, - onSortChange, - contentAlignLeft = true, - hasNextPage, - onFetchNextPage, - onFetchAllPages, - manualSorting = true, - manualPagination = true, - noResultLabel, - isLoading = false, - numberOfLoadingRows, - renderSubComponent, - getRowCanExpand, - resetExpandedRowsOnItemsChange, - hideHeader, - tableLayoutFixed, - resourceType, - rowSelection, - getRowId, -}: DatagridProps) => { - const { t } = useTranslation('datagrid'); - const pageCount = pagination - ? Math.ceil(totalItems / pagination.pageSize) - : 1; - - const columnVisibilityState = useMemo(() => { - if (columnVisibility) { - return columns.reduce((acc, col) => { - acc[col.id] = columnVisibility.includes(col.id); - return acc; - }, {} as VisibilityState); - } - return undefined; - }, [columnVisibility?.join(','), JSON.stringify(columns)]); - - const onColumnVisibilityChange = useCallback((getToggledColumn) => { - // Toggling one column - if (typeof getToggledColumn === 'function') { - const colName = Object.keys(getToggledColumn())?.[0]; - setColumnVisibility?.((prev) => - prev.includes(colName) - ? prev.filter((c) => c !== colName) - : [...prev, colName], - ); - } - // Toggling all columns - else if (typeof getToggledColumn === 'object') { - const newVisibility = Object.entries(getToggledColumn) - .filter(([_, isVisible]) => isVisible) - .map(([colName, _]) => colName); - setColumnVisibility?.(newVisibility); - } - }, []); - - const headerRefs = useRef({}); - - const table = useReactTable({ - columns: [ - ...(rowSelection - ? [ - { - id: 'select', - cell: ({ row }: { row: Row }) => ( - row.toggleSelected()} - isChecked={row.getIsSelected()} - isDisabled={!row.getCanSelect()} - /> - ), - header: () => ( - { - table.toggleAllRowsSelected(); - }} - isChecked={table.getIsAllRowsSelected()} - isIndeterminate={table.getIsSomeRowsSelected()} - /> - ), - }, - ] - : []), - ...(getRowCanExpand && renderSubComponent - ? [ - { - id: 'expander', - enableHiding: false, - cell: ({ row }: { row: Row }) => { - return row.getCanExpand() ? ( - - ) : null; - }, - }, - ] - : []), - ...columns.map( - (col): ColumnDef => ({ - id: col.id, - accessorKey: col.id, - cell: (props) => col.cell(props.row.original), - header: col.label, - enableSorting: col.isSortable !== false, - size: col.size, - enableHiding: col.enableHiding !== false, - }), - ), - ], - data: items, - manualPagination, - manualSorting, - enableSortingRemoval: false, - sortDescFirst: false, - getCoreRowModel: getCoreRowModel(), - getRowCanExpand, - pageCount, - ...(!manualSorting && { - onSortingChange: onSortChange, - state: { - sorting, - ...(rowSelection?.rowSelection && { - rowSelection: rowSelection.rowSelection, - }), - ...(setColumnVisibility && { - columnVisibility: columnVisibilityState, - }), - }, - getSortedRowModel: getSortedRowModel(), - }), - ...(manualSorting && { - state: { - ...(sorting && { - sorting: [sorting], - }), - ...(rowSelection?.rowSelection && { - rowSelection: rowSelection.rowSelection, - }), - ...(setColumnVisibility && { - columnVisibility: columnVisibilityState, - }), - }, - onStateChange: (updater) => { - if (typeof updater === 'function') { - const state = updater({ ...table.getState(), ...sorting }); - onSortChange?.(state.sorting[0]); - } else if (onSortChange) { - onSortChange(updater.sorting[0]); - } - }, - }), - initialState: { - ...(!setColumnVisibility && - columnVisibility && { - columnVisibility: columnVisibilityState, - }), - }, - ...(setColumnVisibility && { onColumnVisibilityChange }), - enableRowSelection: (row) => { - if (rowSelection?.enableRowSelection) - return rowSelection.enableRowSelection(row); - - return !!rowSelection; - }, - onRowSelectionChange: rowSelection?.setRowSelection, - getRowId, - }); - - useEffect(() => { - if (resetExpandedRowsOnItemsChange) { - table.resetExpanded(); - } - }, [items, resetExpandedRowsOnItemsChange]); - - const filtersColumns = useMemo( - () => - columns - ?.filter( - (item) => - ('comparator' in item || 'type' in item) && - 'isFilterable' in item && - item.isFilterable, - ) - .map((column) => ({ - id: column.id, - label: column.label, - ...(column?.type && { - comparators: FilterCategories[column.type], - type: column.type, - }), - ...(column?.comparator && { comparators: column.comparator }), - ...(column?.filterOptions && { options: column.filterOptions }), - })), - [columns], - ); - - const columnsVisibility = useMemo( - () => - table.getAllLeafColumns().map((column) => { - const col = columns.find((item) => column.id === item.id); - return { - id: column.id, - label: col?.label, - isVisible: () => column.getIsVisible(), - isDisabled: !column.getCanHide(), - enableHiding: col?.enableHiding, - onChange: () => column.toggleVisibility(!column.getIsVisible()), - }; - }), - [columns], - ); - - const searchColumns = useMemo( - () => columns?.find((item) => item?.isSearchable), - [columns], - ); - - // Handle onRowSelectionChange callback - useEffect(() => { - const selectedRows = - table.getSelectedRowModel()?.rows?.map(({ original }) => original) || []; - rowSelection?.onRowSelectionChange?.(selectedRows); - }, [JSON.stringify(rowSelection?.rowSelection)]); - - /** - * Update internal rowSelection state when items list change: - * if items selected but is remove from list, remove it from row selection state - */ - useEffect(() => { - if (table.getSelectedRowModel()?.rows) { - const newSelectionState = table - .getSelectedRowModel() - .rows.reduce((selection, { id }) => { - return { - ...selection, - [id]: true, - }; - }, {}); - table.setRowSelection(newSelectionState); - } - }, [JSON.stringify(items)]); - - return ( -
- -
- - - {!hideHeader && ( - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - )} - - {table.getRowModel().rows.map((row) => ( - - - {row.getVisibleCells().map((cell) => ( - - ))} - - {row.getIsExpanded() && !!renderSubComponent && ( - - {/* 2nd row is a custom 1 cell row */} - - - )} - - ))} - {table.getRowModel().rows.length === 0 && !isLoading && ( - - - - )} - {isLoading && - Array.from({ - length: - numberOfLoadingRows || - pagination?.pageSize || - defaultNumberOfLoadingRows, - }).map((_, idx) => ( - - {table.getAllColumns().map((col) => - col.getIsVisible() ? ( - - ) : null, - )} - - ))} - -
{ - headerRefs.current[header.id] = el; - }} - className={`${ - contentAlignLeft ? 'text-left pl-4' : 'text-center' - } h-11 whitespace-nowrap ${ - onSortChange && header.column.getCanSort() - ? 'cursor-pointer' - : '' - }`} - {...{ - ...(onSortChange && { - onClick: header.column.getToggleSortingHandler(), - }), - }} - > - {header.isPlaceholder ? null : ( -
- - <> - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - - - - -
- )} -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
- {renderSubComponent(row, headerRefs)} -
- - {noResultLabel ?? t('common_pagination_no_results')} - -
- -
-
-
- {!onFetchNextPage && items?.length > 0 && pagination ? ( - { - if (detail.current !== detail.oldCurrent) { - onPaginationChange({ - ...pagination, - pageIndex: detail.current - 1, - pageSize: detail.itemPerPage, - }); - } - }} - onOdsItemPerPageChange={({ detail }) => { - if (detail.current !== pagination.pageSize) - onPaginationChange({ - ...pagination, - pageSize: detail.current, - pageIndex: 0, - }); - }} - > - - {t('common_pagination_of')} - - - {t('common_pagination_results')} - - - ) : ( - <> - )} - {hasNextPage ? ( -
- - {onFetchAllPages && ( - - )} -
- ) : null} -
- ); -}; diff --git a/packages/manager-react-components/src/components/datagrid/datagrid.constants.ts b/packages/manager-react-components/src/components/datagrid/datagrid.constants.ts deleted file mode 100644 index 89587becb640..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid.constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PaginationState } from '@tanstack/react-table'; - -/* List of allowed page sizes */ -export const PAGE_SIZES = [10, 25, 50, 100, 300]; - -export const DEFAULT_PAGINATION: PaginationState = { - pageIndex: 0, - pageSize: PAGE_SIZES[0], -}; - -export const defaultNumberOfLoadingRows = 5; - -export const INTERNAL_COLUMNS = ['expander', 'actions']; diff --git a/packages/manager-react-components/src/components/datagrid/datagrid.mock.tsx b/packages/manager-react-components/src/components/datagrid/datagrid.mock.tsx deleted file mode 100644 index 420ca197d8b5..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid.mock.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { FilterCategories } from '@ovh-ux/manager-core-api'; -import { ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; -import { DataGridTextCell } from './text-cell.component'; -import { ActionMenu } from '../navigation'; -import { DatagridColumn, DatagridColumnTypes } from './datagrid.component'; -import { IamObject } from '../../hooks'; - -export interface Item { - label: string; - price: number; - status: string; - anotherStatus: string; - iam: IamObject; -} - -export const columns = [ - { - id: 'label', - cell: (item: Item) => { - return {item.label}; - }, - label: 'Label', - }, - { - id: 'price', - cell: (item: Item) => { - return {item.price} €; - }, - label: 'Price', - }, -]; - -export const columnsFilters: DatagridColumn[] = [ - { - id: 'label', - cell: (item: Item) => { - return {item.label}; - }, - label: 'Label', - isFilterable: true, - comparator: FilterCategories.String, - }, - { - id: 'price', - cell: (item: Item) => { - return {item.price} €; - }, - label: 'Price', - isFilterable: true, - comparator: FilterCategories.String, - }, -]; - -export const columnsFiltersWithTags: DatagridColumn[] = [ - { - id: 'label', - cell: (item: Item) => { - return {item.label}; - }, - label: 'Label', - isFilterable: true, - comparator: FilterCategories.String, - }, - { - id: 'price', - cell: (item: Item) => { - return {item.price} €; - }, - label: 'Price', - isFilterable: true, - comparator: FilterCategories.String, - }, - { - id: 'Tags', - cell: (item: Item) => { - return ( - {JSON.stringify(item.iam?.tags)} - ); - }, - label: 'Tags', - isFilterable: true, - type: DatagridColumnTypes.Tags, - }, -]; - -export const columnsVisibility = [ - { - id: 'label', - cell: (item: Item) => { - return {item.label}; - }, - label: 'Label', - enableHiding: false, - }, - { - id: 'price', - cell: (item: Item) => { - return {item.price} €; - }, - label: 'Price', - enableHiding: true, - }, - { - id: 'actions', - cell: (item: Item) => { - return ( -
- console.log(`Action on ${item.label}`), - label: `Action on ${item.label}`, - }, - ]} - variant={ODS_BUTTON_VARIANT.ghost} - isCompact - /> -
- ); - }, - label: '', - enableHiding: false, - }, -]; - -export const columnsSearchAndFilters = [ - { - id: 'label', - cell: (item: Item) => { - return {item.label}; - }, - label: 'Label', - isFilterable: true, - isSearchable: true, - enableHiding: true, - comparator: FilterCategories.String, - }, - { - id: 'price', - cell: (item: Item) => { - return {item.price} €; - }, - label: 'Price', - isFilterable: true, - isSearchable: true, - enableHiding: true, - comparator: FilterCategories.String, - }, - { - id: 'status', - cell: (item: Item) => { - return {item.status}; - }, - label: 'Status', - isFilterable: true, - comparator: FilterCategories.Options, - filterOptions: [ - { label: 'Status #0', value: 'Status #0' }, - { label: 'Status #1', value: 'Status #1' }, - ], - }, - { - id: 'anotherStatus', - cell: (item: Item) => { - return {item.anotherStatus}; - }, - label: 'anotherStatus', - isFilterable: true, - comparator: FilterCategories.Options, - filterOptions: [ - { - label: 'anotherStatus #0000000000000000000000', - value: 'anotherStatus #0000000000000000000000', - }, - { label: 'anotherStatus #1', value: 'anotherStatus #1' }, - ], - }, -]; diff --git a/packages/manager-react-components/src/components/datagrid/datagrid.spec.tsx b/packages/manager-react-components/src/components/datagrid/datagrid.spec.tsx deleted file mode 100644 index 7e7684b92599..000000000000 --- a/packages/manager-react-components/src/components/datagrid/datagrid.spec.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import { vitest } from 'vitest'; -import React, { useState } from 'react'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; -import { FilterCategories } from '@ovh-ux/manager-core-api'; -import { Row } from '@tanstack/react-table'; -import { - ColumnSort, - Datagrid, - PaginationState, - FilterProps, -} from './datagrid.component'; -import DataGridTextCell from './text-cell.component'; -import { defaultNumberOfLoadingRows } from './datagrid.constants'; - -vitest.mock('react-i18next', async () => { - const originalModule = await vitest.importActual('react-i18next'); - - return { - ...originalModule, - useTranslation: () => { - return { - t: (str: string) => str, - i18n: { - changeLanguage: () => new Promise(() => {}), - }, - }; - }, - }; -}); - -const sampleColumns = [ - { - id: 'name', - cell: (name: string) => { - return {name}; - }, - label: 'Name', - comparator: FilterCategories.String, - isFilterable: true, - }, - { - id: 'another-column', - label: 'test', - cell: () => , - comparator: FilterCategories.String, - isFilterable: true, - }, -]; - -const cols = [ - { - id: 'name', - cell: (name: string) => { - return {name}; - }, - label: 'Name', - }, - { - id: 'another-column', - label: 'test', - cell: () => , - }, -]; - -const DatagridTest = ({ - columns = cols, - items, - pageIndex, - pageSize, - className, - noResultLabel, - filters, - getRowCanExpand, - renderSubComponent, - tableLayoutFixed, - hasRowSelection, - enableRowSelection, - onRowSelectionChange, - getRowId, -}: { - columns: any; - items: string[]; - pageIndex: number; - pageSize?: number; - className?: string; - noResultLabel?: string; - filters?: FilterProps; - getRowCanExpand?: (props: Row) => boolean; - renderSubComponent?: (row: Row) => JSX.Element; - tableLayoutFixed?: boolean; - hasRowSelection?: boolean; - enableRowSelection?: (row: Row) => boolean; - onRowSelectionChange?: (selectedRows: Row[]) => void; - getRowId?: (originalRow: any, index: number) => string; -}) => { - const [pagination, setPagination] = useState({ - pageIndex, - pageSize: pageSize || 2, - }); - - const [rowSelection, setRowSelection] = useState({}); - const start = pagination.pageIndex * pagination.pageSize; - const end = start + pagination.pageSize; - return ( - {}} - className={className || ''} - noResultLabel={noResultLabel} - filters={filters} - getRowCanExpand={getRowCanExpand} - renderSubComponent={renderSubComponent} - tableLayoutFixed={tableLayoutFixed} - rowSelection={ - hasRowSelection - ? { - rowSelection, - setRowSelection, - enableRowSelection, - onRowSelectionChange, - } - : null - } - getRowId={getRowId} - /> - ); -}; - -describe('Paginated datagrid component', () => { - it('should display the correct number of columns', async () => { - const { container } = render( - <>'a', - label: 'a', - }, - { - id: 'b', - cell: () => <>'b', - label: 'b', - }, - { - id: 'c', - cell: () => <>'c', - label: 'c', - }, - ]} - items={[]} - pageIndex={0} - />, - ); - expect(container.querySelectorAll('thead th').length).toBe(3); - }); - - it('should call sort function when clicking columns header', async () => { - const handleSortChange = vitest.fn(); - const SortTest = () => { - const [sorting, setSorting] = useState({ id: 'a', desc: true }); - return ( - <>'a', - label: 'a', - }, - { - id: 'b', - cell: () => <>'b', - label: 'b', - }, - ]} - items={[]} - totalItems={0} - pagination={{ pageIndex: 0, pageSize: 1 }} - sorting={sorting} - manualSorting={true} - onPaginationChange={() => {}} - onSortChange={(c: ColumnSort) => { - setSorting(c); - handleSortChange(c); - }} - /> - ); - }; - render(); - const headerA = screen.queryByTestId('header-a'); - expect(headerA).not.toBeNull(); - expect(handleSortChange).not.toHaveBeenCalled(); - fireEvent.click(headerA); - expect(handleSortChange).toHaveBeenCalledWith({ id: 'a', desc: false }); - fireEvent.click(headerA); - expect(handleSortChange).toHaveBeenCalledWith({ id: 'a', desc: true }); - fireEvent.click(headerA); - expect(handleSortChange).toHaveBeenCalledWith({ id: 'a', desc: false }); - const headerB = screen.queryByTestId('header-b'); - fireEvent.click(headerB); - expect(handleSortChange).toHaveBeenCalledWith({ id: 'b', desc: false }); - }); - - it('should display first page of items', async () => { - const { container } = render( - , - ); - expect(screen.queryByText('foo')).not.toBeNull(); - expect(screen.queryByText('bar')).not.toBeNull(); - expect(screen.queryByText('hello')).toBeNull(); - expect(container.querySelectorAll('thead tr').length).toBe(1); - expect(container.querySelectorAll('tbody tr').length).toBe(2); - }); - - it('should display second page of items', async () => { - const { container } = render( - , - ); - expect(screen.queryByText('foo')).toBeNull(); - expect(screen.queryByText('bar')).toBeNull(); - expect(screen.queryByText('hello')).not.toBeNull(); - expect(container.querySelectorAll('thead tr').length).toBe(1); - expect(container.querySelectorAll('tbody tr').length).toBe(1); - }); - - it('should display a message if there are no items', async () => { - const { container } = render( - , - ); - expect(screen.queryByText('common_pagination_no_results')).not.toBeNull(); - expect(container.querySelectorAll('thead tr').length).toBe(1); - expect(container.querySelectorAll('tbody tr').length).toBe(1); - }); - - it('should display a custom message if there are no items and we pass a custom message', async () => { - render( - , - ); - expect(screen.queryByText('Test no result')).not.toBeNull(); - }); -}); - -it('should disable overflow of table', async () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('.overflow-hidden').length).toBe(1); -}); - -it('should have the default number of loading row when isLoading is true and numberOfLoadingRows is not specified', async () => { - const { queryAllByTestId } = render( - , - ); - expect(queryAllByTestId('loading-row').length).toBe( - defaultNumberOfLoadingRows, - ); -}); - -it('should display the specified number of loading rows when isLoading is true', async () => { - const numberOfLoadingRows = 2; - const { queryAllByTestId } = render( - , - ); - expect(queryAllByTestId('loading-row').length).toBe(numberOfLoadingRows); -}); - -it('should display take the pageSize and not the default one as numberOfLoadingRows when specified', async () => { - const pageSize = 10; - const { queryAllByTestId } = render( - , - ); - expect(queryAllByTestId('loading-row').length).toBe(pageSize); -}); - -it('should set isLoading to load more button when isLoading is true', async () => { - const { getByTestId } = render( - , - ); - expect(getByTestId('load-more-btn')).toHaveAttribute('is-loading', 'true'); -}); - -it('should display load all button and set isLoading to true', async () => { - const { getByTestId } = render( - {}} - />, - ); - expect(getByTestId('load-all-btn')).toHaveAttribute('is-loading', 'true'); -}); - -it('should disable overflow of table', async () => { - const { container } = render( - , - ); - expect(container.querySelectorAll('.overflow-hidden').length).toBe(1); -}); - -it('should display filter add and filter list', async () => { - const filters = { - filters: [ - { - key: 'customName', - comparator: 'includes', - value: 'coucou', - label: 'customName', - }, - ], - add: null, - remove: null, - } as FilterProps; - const { container } = render( - , - ); - expect( - container.querySelectorAll('#datagrid-filter-popover-trigger').length, - ).toBe(1); - expect(container.querySelectorAll('#datagrid-filter-list').length).toBe(1); -}); - -it('should display new column to expand a row sub component', async () => { - const { container } = render( - true} - renderSubComponent={(row) => ( - {`sub-${row.original}`} - )} - />, - ); - - expect( - container.querySelectorAll('ods-button[icon=chevron-right]').length, - ).toBe(2); - - const button = container.querySelectorAll( - 'ods-button[icon=chevron-right]', - )[0]; - await act(() => fireEvent.click(button)); - expect( - container.querySelectorAll('ods-button[icon=chevron-down]').length, - ).toBe(1); - - expect(container.querySelector('#sub-foo')).toBeDefined(); -}); - -it('should use fixed column width', async () => { - const sizedColumns = sampleColumns.map((column) => ({ ...column, size: 50 })); - const { getByText } = render( - , - ); - - const td = getByText('foo').closest('td'); - expect(td.style.width).toBe('50px'); -}); - -it('should display new column to select rows', async () => { - const selectAllMock = vitest.fn(); - const { container } = render( - row.original === 'foo'} - onRowSelectionChange={selectAllMock} - getRowId={(row) => row} - />, - ); - - const allCheckboxes = container.querySelectorAll('ods-checkbox'); - - expect(allCheckboxes.length).toBe(4); // 3 row and 1 header - - expect(allCheckboxes[2]).toHaveAttribute('is-disabled', 'true'); - - expect(allCheckboxes[1]).toHaveAttribute('is-disabled', 'false'); - - const toogleAll = allCheckboxes[0]; - - fireEvent.click(toogleAll); - - waitFor(() => { - expect(selectAllMock).toHaveBeenCalledWith(['foo']); - }); -}); diff --git a/packages/manager-react-components/src/components/datagrid/indeterminate-checkbox.tsx b/packages/manager-react-components/src/components/datagrid/indeterminate-checkbox.tsx deleted file mode 100644 index 42313f00a47b..000000000000 --- a/packages/manager-react-components/src/components/datagrid/indeterminate-checkbox.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { OdsCheckbox } from '@ovhcloud/ods-components/react'; - -export type IndeterminateCheckboxProps = { - id: string; - name: string; - label: string; - onChange: () => void; - isChecked?: boolean; - isIndeterminate?: boolean; - isDisabled?: boolean; -}; - -export const IndeterminateCheckbox = ({ - id, - name, - label, - onChange, - isChecked, - isIndeterminate, - isDisabled, -}: IndeterminateCheckboxProps) => { - return ( - - ); -}; diff --git a/packages/manager-react-components/src/components/datagrid/text-cell.component.tsx b/packages/manager-react-components/src/components/datagrid/text-cell.component.tsx deleted file mode 100644 index d0ae58a6b6d9..000000000000 --- a/packages/manager-react-components/src/components/datagrid/text-cell.component.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_TEXT_PRESET } from '@ovhcloud/ods-components'; -import { useTranslation } from 'react-i18next'; - -type DataGridTextCellProps = { - className?: string; - 'data-testid'?: string; -}; - -/** Simple datagrid cell text formatter applying ODS style */ -export function DataGridTextCell({ - className, - children, - 'data-testid': dataTestId, -}: React.PropsWithChildren) { - const { t } = useTranslation('datagrid'); - return ( - - {(children as string) ?? t('common_empty_text_cell' as string)} - - ); -} - -export default DataGridTextCell; diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_en_GB.json b/packages/manager-react-components/src/components/datagrid/translations/Messages_en_GB.json deleted file mode 100644 index 152cac36c5ce..000000000000 --- a/packages/manager-react-components/src/components/datagrid/translations/Messages_en_GB.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "common_pagination_of": "of", - "common_pagination_results": "results", - "common_pagination_no_results": "No result", - "common_clipboard_success_label": "Copied!", - "common_clipboard_error_label": "Copy error.", - "common_pagination_load_more": "Load more", - "common_empty_text_cell": "None", - "common_topbar_columns": "Columns", - "common_pagination_load_all": "Load all", - "common_topbar_columns_select_all": "Select all" -} diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/datagrid/translations/Messages_fr_FR.json deleted file mode 100644 index ba1574b20935..000000000000 --- a/packages/manager-react-components/src/components/datagrid/translations/Messages_fr_FR.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "common_pagination_of": "sur", - "common_pagination_results": "résultats", - "common_pagination_no_results": "Aucun résultat", - "common_clipboard_success_label": "Copié !", - "common_clipboard_error_label": "Erreur de copie.", - "common_pagination_load_more": "Charger plus", - "common_pagination_load_all": "Charger tout", - "common_empty_text_cell": "Aucun", - "common_topbar_columns": "Colonnes", - "common_topbar_columns_select_all": "Sélectionner tout" -} diff --git a/packages/manager-react-components/src/components/datagrid/useDatagrid.ts b/packages/manager-react-components/src/components/datagrid/useDatagrid.ts deleted file mode 100644 index a3dda1b09a39..000000000000 --- a/packages/manager-react-components/src/components/datagrid/useDatagrid.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useState } from 'react'; -import { ColumnSort, PaginationState } from '@tanstack/react-table'; -import { DEFAULT_PAGINATION } from './datagrid.constants'; - -export const useDataGrid = (defaultSorting: ColumnSort = undefined) => { - const [pagination, setPagination] = - useState(DEFAULT_PAGINATION); - const [sorting, setSorting] = useState(defaultSorting); - return { pagination, setPagination, sorting, setSorting }; -}; diff --git a/packages/manager-react-components/src/components/datagrid/useDatagridSearchParams.ts b/packages/manager-react-components/src/components/datagrid/useDatagridSearchParams.ts deleted file mode 100644 index 5bf6bf8cf539..000000000000 --- a/packages/manager-react-components/src/components/datagrid/useDatagridSearchParams.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ColumnSort, PaginationState } from '@tanstack/react-table'; -import { useSearchParams } from 'react-router-dom'; -import { DEFAULT_PAGINATION, PAGE_SIZES } from './datagrid.constants'; - -/** - * This hooks allows to store and synchronize the datagrid pagination & sorting - * state within URL search parameters. Thus the user is able to refresh his page - * without loosing his current pagination and column sorting state. - */ - -/* Convert URL search params to plain object */ -const getSearchParamsObject = (search: URLSearchParams) => - Object.fromEntries([...search.entries()]); - -/* Parse pagination from URL search params */ -const parsePagination = (params: URLSearchParams): PaginationState => { - const pagination = { ...DEFAULT_PAGINATION }; - if (params.has('page')) { - let pageIndex = parseInt(params.get('page'), 10) - 1; - if (Number.isNaN(pageIndex) || pageIndex < 0) pageIndex = 0; - pagination.pageIndex = pageIndex; - } - if (params.has('pageSize')) { - let pageSize = parseInt(params.get('pageSize'), 10); - if (!PAGE_SIZES.includes(pageSize)) [pageSize] = PAGE_SIZES; - pagination.pageSize = pageSize; - } - return pagination; -}; - -/* Parse column sorting from URL search params */ -const parseSorting = ( - params: URLSearchParams, - defaultSorting?: ColumnSort, -): ColumnSort => { - const sorting: ColumnSort = { - id: null, - desc: false, - }; - if (params.has('sort')) { - sorting.id = params.get('sort'); - if (params.has('sortOrder')) { - sorting.desc = params.get('sortOrder') === 'desc'; - } - } else if (defaultSorting) { - return defaultSorting; - } - return sorting; -}; - -/** Use URL search params to store datagrid pagination & column sorting */ -export const useDatagridSearchParams = (defaultSorting?: ColumnSort) => { - const [searchParams, setSearchParams] = useSearchParams(); - - return { - pagination: parsePagination(searchParams), - sorting: parseSorting(searchParams, defaultSorting), - setPagination: ({ pageIndex, pageSize }: PaginationState) => { - if (pageIndex > 0) searchParams.set('page', `${pageIndex + 1}`); - else searchParams.delete('page'); - if (PAGE_SIZES.includes(pageSize) && pageSize !== PAGE_SIZES[0]) - searchParams.set('pageSize', `${pageSize}`); - else searchParams.delete('pageSize'); - setSearchParams({ - ...getSearchParamsObject(searchParams), - }); - }, - setSorting: ({ id, desc }: ColumnSort) => { - if (id) { - searchParams.set('sort', id); - if (desc) { - searchParams.set('sortOrder', 'desc'); - } else { - searchParams.delete('sortOrder'); - } - } else { - searchParams.delete('sort'); - searchParams.delete('sortOrder'); - } - setSearchParams({ - ...getSearchParamsObject(searchParams), - }); - }, - }; -}; diff --git a/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.component.tsx b/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.component.tsx deleted file mode 100644 index 657cea7057d6..000000000000 --- a/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.component.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { - OdsButton, - OdsCheckbox, - OdsFormField, - OdsPopover, - OdsText, -} from '@ovhcloud/ods-components/react'; -import { useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { INTERNAL_COLUMNS } from '../datagrid.constants'; - -export type ColumnsVisibility = { - id: string; - isDisabled: boolean; - label: string; - enableHiding: boolean; - isVisible: () => boolean; - onChange: () => void; -}; - -export type ColumnsVisibilityProps = { - columnsVisibility: ColumnsVisibility[]; - toggleAllColumnsVisible: (a: boolean) => void; - getIsAllColumnsVisible: () => boolean; - getIsSomeColumnsVisible: () => boolean; -}; - -export function VisibilityManagement({ - columnsVisibility, - toggleAllColumnsVisible, - getIsAllColumnsVisible, - getIsSomeColumnsVisible, -}: Readonly) { - const { t } = useTranslation('datagrid'); - const visibilityPopoverRef = useRef(null); - const eligibleColumns = columnsVisibility.filter( - (column) => !INTERNAL_COLUMNS.includes(column.id) && column.label !== '', - ); - - const visibleColumnsCount = eligibleColumns.filter((column) => - column.isVisible(), - ).length; - - const isAllColumnsVisible = getIsAllColumnsVisible(); - const isSomeColumnsVisible = getIsSomeColumnsVisible(); - - return ( - <> - - -
-
- toggleAllColumnsVisible(!isAllColumnsVisible)} - ariaLabel={t('common_topbar_columns_select_all')} - isIndeterminate={!isAllColumnsVisible && isSomeColumnsVisible} - /> - -
- {eligibleColumns.map((column) => ( - -
- - -
-
- ))} -
-
- - ); -} diff --git a/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.spec.tsx b/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.spec.tsx deleted file mode 100644 index 85b11e014411..000000000000 --- a/packages/manager-react-components/src/components/datagrid/visibility/visibility-management.spec.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { vitest } from 'vitest'; -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { VisibilityManagement } from './visibility-management.component'; - -vitest.mock('@ovhcloud/ods-components/react', async () => { - const originalModule = await vitest.importActual( - '@ovhcloud/ods-components/react', - ); - return { - ...originalModule, - OdsCheckbox: ({ name, inputId, isDisabled, isChecked, onOdsChange }) => { - return ( - - ); - }, - }; -}); - -describe('visibility management button part', () => { - it('should display visibility with one disabled and only this disabled is shown', () => { - render( - true, - onChange: () => null, - }, - { - id: 'price', - label: 'Price', - enableHiding: true, - isDisabled: false, - isVisible: () => false, - onChange: () => null, - }, - ]} - toggleAllColumnsVisible={() => null} - getIsAllColumnsVisible={() => false} - getIsSomeColumnsVisible={() => true} - />, - ); - - const visibilityElement = screen.queryByTestId( - 'datagrid-topbar-visibility-button', - ); - - expect(visibilityElement).toHaveAttribute( - 'label', - 'common_topbar_columns (1)', - ); - }); - - it('should display visibility with all available and one shown', () => { - render( - true, - onChange: () => null, - }, - { - id: 'price', - label: 'Price', - enableHiding: true, - isDisabled: false, - isVisible: () => false, - onChange: () => null, - }, - ]} - toggleAllColumnsVisible={() => null} - getIsAllColumnsVisible={() => false} - getIsSomeColumnsVisible={() => true} - />, - ); - - const visibilityElement = screen.queryByTestId( - 'datagrid-topbar-visibility-button', - ); - - expect(visibilityElement).toHaveAttribute( - 'label', - 'common_topbar_columns (1)', - ); - }); -}); - -describe('visibility management dropdown part', () => { - it('should display visibility with all available and all shown', () => { - render( - true, - onChange: () => null, - }, - { - id: 'price', - label: 'Price', - enableHiding: true, - isDisabled: false, - isVisible: () => true, - onChange: () => null, - }, - ]} - toggleAllColumnsVisible={() => null} - getIsAllColumnsVisible={() => true} - getIsSomeColumnsVisible={() => true} - />, - ); - - const checkboxElements = screen.queryAllByRole('checkbox'); - expect(checkboxElements[1]).toHaveProperty('checked', true); - expect(checkboxElements[1]).toHaveProperty('disabled', false); - - expect(checkboxElements[2]).toHaveProperty('checked', true); - expect(checkboxElements[2]).toHaveProperty('disabled', false); - }); - - it('should display visibility column disabled', async () => { - const onChange = vitest.fn(() => null); - render( - true, - onChange, - }, - { - id: 'price', - label: 'Price', - enableHiding: true, - isDisabled: false, - isVisible: () => false, - onChange, - }, - ]} - toggleAllColumnsVisible={() => null} - getIsAllColumnsVisible={() => false} - getIsSomeColumnsVisible={() => true} - />, - ); - - const checkboxElements = screen.queryAllByRole('checkbox'); - expect(checkboxElements[1]).toHaveProperty('checked', true); - expect(checkboxElements[1]).toHaveProperty('disabled', true); - - expect(checkboxElements[2]).toHaveProperty('checked', false); - expect(checkboxElements[2]).toHaveProperty('disabled', false); - - await act(() => fireEvent.click(checkboxElements[1])); - expect(onChange).toHaveBeenCalled(); - }); -}); diff --git a/packages/manager-react-components/src/components/drawer/Drawer.component.tsx b/packages/manager-react-components/src/components/drawer/Drawer.component.tsx deleted file mode 100644 index 31621d6e8f14..000000000000 --- a/packages/manager-react-components/src/components/drawer/Drawer.component.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import DrawerBackdrop from './DrawerBackdrop.component'; -import { DrawerBase, DrawerBaseProps } from './DrawerBase.component'; -import './translations'; - -export type DrawerProps = Omit; - -export const Drawer = (props: DrawerProps) => { - return ( -
- - {props.isOpen && } -
- ); -}; diff --git a/packages/manager-react-components/src/components/drawer/Drawer.spec.tsx b/packages/manager-react-components/src/components/drawer/Drawer.spec.tsx deleted file mode 100644 index 482bbd137a3f..000000000000 --- a/packages/manager-react-components/src/components/drawer/Drawer.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import userEvent from '@testing-library/user-event'; -import { act, render, screen } from '@testing-library/react'; -import { mockedProps } from './DrawerBase.mock'; -import { Drawer } from './Drawer.component'; - -it('should display a backdrop overlay', async () => { - const user = userEvent.setup(); - render(); - - expect(screen.getByTestId('drawer')).not.toBeNull(); - expect(screen.getByTestId('drawer-backdrop')).toBeVisible(); -}); - -it('should close the drawer on backdrop click', async () => { - const user = userEvent.setup(); - render(); - - expect(screen.getByTestId('drawer')).not.toBeNull(); - - const backdrop = screen.getByTestId('drawer-backdrop'); - await act(() => user.click(backdrop)); - - expect(mockedProps.onDismiss).toHaveBeenCalled(); -}); diff --git a/packages/manager-react-components/src/components/drawer/DrawerBase.component.tsx b/packages/manager-react-components/src/components/drawer/DrawerBase.component.tsx deleted file mode 100644 index c5b16a834b95..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerBase.component.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { - OdsButton, - OdsDrawer, - OdsSpinner, - OdsText, -} from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_COLOR, - ODS_TEXT_PRESET, - ODS_SPINNER_SIZE, - ODS_BUTTON_VARIANT, -} from '@ovhcloud/ods-components'; -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; -import './translations'; - -export type DrawerBaseProps = PropsWithChildren & { - /** Drawer heading */ - heading: string; - /** Open/close the drawer (default to true) */ - isOpen?: boolean; - /** Callback function to be called on close of the Drawer */ - onDismiss: () => void; - /** Show a loader instead of the drawer content */ - isLoading?: boolean; - /** Primary Button Label */ - primaryButtonLabel?: string; - /** Primary Button Loading State */ - isPrimaryButtonLoading?: boolean; - /** Primary Button Disabled State */ - isPrimaryButtonDisabled?: boolean; - /** Primary Button Callback */ - onPrimaryButtonClick?: () => void; - /** Secondary Button Label */ - secondaryButtonLabel?: string; - /** Secondary Button Disabled State */ - isSecondaryButtonDisabled?: boolean; - /** Secondary Button Loading State */ - isSecondaryButtonLoading?: boolean; - /** Secondary Button Callback */ - onSecondaryButtonClick?: () => void; - className?: string; -}; - -export const DrawerBase = ({ - children, - heading, - isOpen = true, - isLoading, - onDismiss, - isPrimaryButtonLoading, - isPrimaryButtonDisabled, - onPrimaryButtonClick, - primaryButtonLabel, - isSecondaryButtonLoading, - isSecondaryButtonDisabled, - onSecondaryButtonClick, - secondaryButtonLabel, - className, -}: DrawerBaseProps) => { - const { t } = useTranslation('drawer'); - - return ( - -
-
-
- {!isLoading && ( - {heading} - )} - -
-
- - {isLoading && ( -
- -
- )} - - {!isLoading && ( - <> -
- {children} -
- - {(primaryButtonLabel || secondaryButtonLabel) && ( -
- {secondaryButtonLabel && ( - - )} - {primaryButtonLabel && ( - - )} -
- )} - - )} -
-
- ); -}; diff --git a/packages/manager-react-components/src/components/drawer/DrawerBase.mock.tsx b/packages/manager-react-components/src/components/drawer/DrawerBase.mock.tsx deleted file mode 100644 index 3c251f77958d..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerBase.mock.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { vi } from 'vitest'; -import { DrawerBaseProps } from './DrawerBase.component'; - -vi.mock('@ovhcloud/ods-components/react', async () => { - const original = await vi.importActual('@ovhcloud/ods-components/react'); - return { - ...original, - OdsDrawer: vi.fn(({ children, className, ...props }) => ( -
- {children} -
- )), - }; -}); - -export const mockedProps: DrawerBaseProps = { - heading: 'Drawer heading', - isOpen: true, - children:
Drawer content
, - primaryButtonLabel: 'Confirm', - isPrimaryButtonLoading: false, - isPrimaryButtonDisabled: false, - onPrimaryButtonClick: vi.fn(), - secondaryButtonLabel: 'Cancel', - isSecondaryButtonDisabled: false, - isSecondaryButtonLoading: false, - onSecondaryButtonClick: vi.fn(), - onDismiss: vi.fn(), -}; diff --git a/packages/manager-react-components/src/components/drawer/DrawerBase.spec.tsx b/packages/manager-react-components/src/components/drawer/DrawerBase.spec.tsx deleted file mode 100644 index b43fcb51e656..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerBase.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { getOdsButtonByLabel } from '@ovh-ux/manager-core-test-utils'; -import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { mockedProps } from './DrawerBase.mock'; -import { DrawerBase } from './DrawerBase.component'; - -it('should display the drawer', async () => { - const { container } = render(); - expect(screen.getByTestId('drawer')).not.toBeNull(); - expect(screen.queryByText('Drawer heading')).not.toBeNull(); - expect(screen.queryByText('Drawer content')).not.toBeNull(); - - const dismissButton = screen.getByTestId('drawer-dismiss-button'); - const primaryButton = await getOdsButtonByLabel({ - container, - label: mockedProps.primaryButtonLabel, - }); - const secondaryButton = await getOdsButtonByLabel({ - container, - label: mockedProps.secondaryButtonLabel, - }); - - expect(dismissButton).toHaveAttribute('aria-label', 'close'); - - expect(primaryButton).toHaveAttribute('label', 'Confirm'); - expect(primaryButton).toHaveAttribute('color', 'primary'); - expect(primaryButton).toHaveAttribute('variant', 'default'); - expect(primaryButton).toHaveAttribute('is-loading', 'false'); - expect(primaryButton).toHaveAttribute('is-disabled', 'false'); - - expect(secondaryButton).toHaveAttribute('label', 'Cancel'); - expect(secondaryButton).toHaveAttribute('color', 'primary'); - expect(secondaryButton).toHaveAttribute('variant', 'ghost'); - expect(secondaryButton).toHaveAttribute('is-loading', 'false'); - expect(secondaryButton).toHaveAttribute('is-disabled', 'false'); - - expect(screen.queryByTestId('drawer-handle')).toBeNull(); -}); - -it('should show a loader when isLoading is true', () => { - render(); - expect(screen.getByTestId('drawer')).not.toBeNull(); - expect(screen.queryByText('Drawer heading')).toBeNull(); - expect(screen.queryByText('Drawer content')).toBeNull(); - expect(screen.getByTestId('drawer-spinner')).not.toBeNull(); -}); - -it('should close the drawer when the dismiss button is clicked', async () => { - const user = userEvent.setup(); - render(); - - expect(screen.getByTestId('drawer')).not.toBeNull(); - - const dismissButton = screen.getByTestId('drawer-dismiss-button'); - await act(() => user.click(dismissButton)); - - expect(mockedProps.onDismiss).toHaveBeenCalled(); -}); diff --git a/packages/manager-react-components/src/components/drawer/DrawerCollapsible.component.tsx b/packages/manager-react-components/src/components/drawer/DrawerCollapsible.component.tsx deleted file mode 100644 index f2dfce1d924a..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerCollapsible.component.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useState } from 'react'; -import clsx from 'clsx'; -import DrawerHandle from './DrawerHandle.component'; -import { DrawerBase, DrawerBaseProps } from './DrawerBase.component'; -import { DrawerCollapseState } from './Drawer.types'; -import './translations'; - -export type DrawerCollapsibleProps = Omit; - -export const DrawerCollapsible = (props: DrawerCollapsibleProps) => { - const [collapseState, setCollapseState] = - useState('visible'); - - const handleToggleCollapseState = () => { - setCollapseState((prevState) => - prevState === 'visible' ? 'collapsed' : 'visible', - ); - }; - - return ( -
- - - {props.isOpen && ( - - )} -
- ); -}; diff --git a/packages/manager-react-components/src/components/drawer/DrawerCollapsible.spec.tsx b/packages/manager-react-components/src/components/drawer/DrawerCollapsible.spec.tsx deleted file mode 100644 index bc60c8bf34e3..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerCollapsible.spec.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { mockedProps } from './DrawerBase.mock'; -import { DrawerCollapsible } from './DrawerCollapsible.component'; - -it('should display the drawer in its collapsible variant', () => { - render(); - expect(screen.getByTestId('drawer')).not.toBeNull(); - expect(screen.queryByTestId('drawer-backdrop')).toBeNull(); - expect(screen.queryByTestId('drawer-handle')).not.toBeNull(); -}); - -it('should collapse and reopen the drawer when the handle is clicked', async () => { - const user = userEvent.setup(); - - render(); - expect(screen.getByTestId('drawer')).not.toBeNull(); - - // Collapse the drawer - const handle = screen.getByTestId('drawer-handle'); - await act(() => user.click(handle)); - - await waitFor(() => { - const drawer = screen.getByTestId('drawer'); - const classList = Array.from(drawer.classList); - const hasTranslateX = classList.some((className) => - className.includes('translate-x'), - ); - expect(hasTranslateX).toBe(true); - }); - - // Reopen the drawer - await act(() => user.click(handle)); - - await waitFor(() => { - const drawer = screen.getByTestId('drawer'); - const classList = Array.from(drawer.classList); - const hasTranslateX = classList.some((className) => - className.includes('translate-x'), - ); - expect(hasTranslateX).toBe(false); - }); -}); - -it('should hide the handle immediately after the user presses the “Esc” key', () => { - render(); - expect(screen.getByTestId('drawer')).not.toBeNull(); - const handle = screen.getByTestId('drawer-handle'); - expect(handle).toBeVisible(); - act(() => { - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); - }); - waitFor(() => { - expect(handle).not.toBeVisible(); - expect(screen.queryByTestId('drawer-handle')).toBeNull(); - }); -}); diff --git a/packages/manager-react-components/src/components/drawer/DrawerHandle.component.tsx b/packages/manager-react-components/src/components/drawer/DrawerHandle.component.tsx deleted file mode 100644 index d48a9fda4944..000000000000 --- a/packages/manager-react-components/src/components/drawer/DrawerHandle.component.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useState } from 'react'; -import { OdsButton } from '@ovhcloud/ods-components/react'; -import { ODS_BUTTON_COLOR, ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; -import { DrawerCollapseState } from './Drawer.types'; -import './translations'; - -type DrawerHandleProps = { - onClick: () => void; - collapseState: DrawerCollapseState; -}; - -const DrawerHandle = ({ onClick, collapseState }: DrawerHandleProps) => { - const { t } = useTranslation('drawer'); - const [hasEscapeBeenPressed, setHasEscapeBeenPressed] = useState(false); - - // Handle Escape key press to hide the handle - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape' && collapseState === 'visible') { - setHasEscapeBeenPressed(true); - } - }; - - document.addEventListener('keydown', handleKeyDown); - - return () => document.removeEventListener('keydown', handleKeyDown); - }, [collapseState]); - - return ( -
-
-
- -
-
-
- ); -}; - -export default DrawerHandle; diff --git a/packages/manager-react-components/src/components/drawer/readme.md b/packages/manager-react-components/src/components/drawer/readme.md deleted file mode 100644 index ad83405f5437..000000000000 --- a/packages/manager-react-components/src/components/drawer/readme.md +++ /dev/null @@ -1,19 +0,0 @@ -# Drawer Component - -## Current Implementation - -The drawer component comes in two variants, accessible via two separate components: `Drawer` and `DrawerCollapsible`. - -Both components use the `DrawerBase` component. - -- `DrawerBase` should not be exposed to library consumers. -- `DrawerBase` implements all shared behaviors for the two exported components. - -## Where to Add New Features - -When adding a feature to the drawer: - -- If it is shared by both variants, add it to `DrawerBase`. -- If it is specific to one component, add it to the relevant component. -- If it is specific to one component but cannot be implemented outside of `DrawerBase`, add it to `DrawerBase` but only expose the related props in the relevant component. -- If it diverges too much from the existing variants, consider creating a new one. diff --git a/packages/manager-react-components/src/components/filters/filter-add.component.tsx b/packages/manager-react-components/src/components/filters/filter-add.component.tsx deleted file mode 100644 index 11affc282293..000000000000 --- a/packages/manager-react-components/src/components/filters/filter-add.component.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { - Filter, - FilterComparator, - FilterTypeCategories, -} from '@ovh-ux/manager-core-api'; -import { ODS_BUTTON_SIZE, ODS_INPUT_TYPE } from '@ovhcloud/ods-components'; - -import { - OdsButton, - OdsDatepicker, - OdsFormField, - OdsInput, - OdsSelect, -} from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; -import './translations'; -import { TagsFilterForm } from './tags-filter-form.component'; - -export type Option = { - label: string; - value: string; -}; - -export type ColumnFilter = { - id: string; - label: string; - comparators: FilterComparator[]; - type?: FilterTypeCategories; - options?: Option[]; -}; - -export type FilterAddProps = { - columns: ColumnFilter[]; - resourceType?: string; - onAddFilter: (filter: Filter, column: ColumnFilter) => void; -}; - -export function FilterAdd({ - columns, - onAddFilter, - resourceType, -}: Readonly) { - const { t } = useTranslation('filters'); - - const [selectedId, setSelectedId] = useState(columns?.[0]?.id || ''); - const [selectedComparator, setSelectedComparator] = useState( - columns?.[0]?.comparators?.[0] || FilterComparator.IsEqual, - ); - const [value, setValue] = useState(''); - const [dateValue, setDateValue] = useState(null); - const [tagKey, setTagKey] = useState(''); - - const selectedColumn = useMemo( - () => columns.find(({ id }) => selectedId === id), - [columns, selectedId], - ); - - const isInputValid = useMemo(() => { - if (selectedColumn?.type === FilterTypeCategories.Date) { - return dateValue !== null; - } - if (selectedColumn?.type === FilterTypeCategories.Numeric) { - // 0 is a valid number (though falsy) - // Empty string is not a valid number (though Number('') === 0) - return !Number.isNaN(Number(value)) && value !== ''; - } - - // If filter is a tag filter, we need to check if the tag key and value are valid - // If comparator is TagExists or TagNotExists, we need to only check for tagKey - if (selectedColumn?.type === FilterTypeCategories.Tags) { - return ( - (!!tagKey && !!value) || - (!!tagKey && - [FilterComparator.TagExists, FilterComparator.TagNotExists].includes( - selectedComparator, - )) - ); - } - - return value !== ''; - }, [selectedColumn, dateValue, value, tagKey, selectedComparator]); - - const submitAddFilter = () => { - if (!isInputValid) { - return; - } - onAddFilter( - { - key: selectedId, - comparator: selectedComparator, - value: - selectedColumn.type === FilterTypeCategories.Date - ? dateValue.toISOString() - : value, - type: selectedColumn.type, - tagKey, - }, - selectedColumn, - ); - setValue(''); - setTagKey(''); - setDateValue(null); - }; - - useEffect(() => { - setSelectedComparator(selectedColumn?.comparators[0]); - setValue(''); - setTagKey(''); - setDateValue(null); - }, [selectedColumn]); - - let inputComponent: JSX.Element; - if (selectedColumn?.type === FilterTypeCategories.Date) { - inputComponent = ( - setDateValue(e.detail.value)} - /> - ); - } else if (selectedColumn?.type === FilterTypeCategories.Numeric) { - inputComponent = ( - setValue(`${e.detail.value}`)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submitAddFilter(); - } - }} - /> - ); - } else if (selectedColumn?.options?.length > 0) { - inputComponent = ( - setValue(event.detail.value as string)} - > - {selectedColumn?.options.map((option) => ( - - ))} - - ); - } else { - inputComponent = ( - setValue(`${e.detail.value}`)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submitAddFilter(); - } - }} - /> - ); - } - - return ( - <> -
- -
- - {t('common_criteria_adder_column_label')} - -
- setSelectedId(event.detail.value as string)} - > - {columns.map(({ id, label }) => ( - - ))} - -
-
-
- -
- - {t('common_criteria_adder_operator_label')} - -
- {selectedColumn && ( -
- { - setSelectedComparator(event.detail.value as FilterComparator); - }} - data-testid={`add-operator-${selectedColumn.id}`} - > - {selectedColumn.comparators?.map((comp) => ( - - ))} - -
- )} -
-
- {selectedColumn?.type !== FilterTypeCategories.Tags && ( -
- -
- - {t('common_criteria_adder_value_label')} - -
- {inputComponent} -
-
- )} - {selectedColumn?.type === FilterTypeCategories.Tags && ( - - )} -
- -
- - ); -} diff --git a/packages/manager-react-components/src/components/filters/filter-add.spec.tsx b/packages/manager-react-components/src/components/filters/filter-add.spec.tsx deleted file mode 100644 index acfecd26e779..000000000000 --- a/packages/manager-react-components/src/components/filters/filter-add.spec.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useState } from 'react'; -import { vi, vitest } from 'vitest'; -import { act, fireEvent, waitFor } from '@testing-library/react'; -import { FilterAdd, FilterAddProps } from './filter-add.component'; -import { render } from '../../utils/test.provider'; - -vi.mock('./tags-filter-form.component', () => { - return { - TagsFilterForm: ({ setTagKey }) => { - setTagKey('tagKey'); - return
; - }, - }; -}); - -const renderComponent = (props: FilterAddProps) => { - return render(); -}; - -describe('FilterAdd tests', () => { - it('should deactivate the add filter button when value est undefined', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'username', - label: "Nom d'utilisateur", - comparators: [ - 'includes', - 'starts_with', - 'ends_with', - 'is_equal', - 'is_different', - ], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const addFilterButton = getByTestId('filter-add_submit'); - expect(addFilterButton).toHaveAttribute('is-disabled', 'true'); - }); - - it('should set the id of first columns items as value of the id select', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'username', - label: "Nom d'utilisateur", - comparators: [ - 'includes', - 'starts_with', - 'ends_with', - 'is_equal', - 'is_different', - ], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const idColumnSelect = getByTestId('add-filter_select_idColumn'); - expect(idColumnSelect).toHaveValue(props.columns[0].id); - }); - - it('should display a date picker when the filter type is Date', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'createdAt', - label: 'Created At', - type: 'Date', - comparators: ['is_before', 'is_after', 'is_equal'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const valueField = getByTestId('filter-add_value-date'); - expect(valueField.tagName).toBe('ODS-DATEPICKER'); - }); - - it('should return a valid date string when a date is set in the value field', async () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'createdAt', - label: 'Created At', - type: 'Date', - comparators: ['is_before', 'is_after', 'is_equal'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const valueField = getByTestId('filter-add_value-date'); - const addFilterButton = getByTestId('filter-add_submit'); - - const testDate = new Date('2023-10-01'); - const isoDate = testDate.toISOString(); - - act(() => { - fireEvent.change(valueField, { target: { value: testDate } }); - }); - - await waitFor(() => { - expect(addFilterButton).toHaveAttribute('is-disabled', 'false'); - }); - - act(() => { - addFilterButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); - }); - - expect(mockOnAddFilter).toHaveBeenCalledWith( - expect.objectContaining({ - key: 'createdAt', - value: isoDate, - }), - expect.objectContaining({ - id: 'createdAt', - type: 'Date', - }), - ); - }); -}); - -it('should disable submit button when the filter type is Numeric and the value is not', async () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'size', - label: 'Size', - type: 'Numeric', - comparators: ['is_lower', 'is_higher', 'is_equal'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const valueField = getByTestId('filter-add_value-numeric'); - const addFilterButton = getByTestId('filter-add_submit'); - - const badValue = 'foo'; - const goodValue = '-123.12'; - - // Submit button is initially disabled - await waitFor(() => { - expect(addFilterButton).toHaveAttribute('is-disabled', 'true'); - }); - - act(() => { - fireEvent.change(valueField, { target: { value: goodValue } }); - }); - - // Submit button is enabled with a valid number - await waitFor(() => { - expect(addFilterButton).toHaveAttribute('is-disabled', 'false'); - }); - - act(() => { - fireEvent.change(valueField, { target: { value: badValue } }); - }); - - // Submit button is disabled with an invalid number - await waitFor(() => { - expect(addFilterButton).toHaveAttribute('is-disabled', 'true'); - }); -}); - -it('should set the select option', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'status', - label: 'Status', - comparators: [ - 'includes', - 'starts_with', - 'ends_with', - 'is_equal', - 'is_different', - ], - options: [ - { label: 'option1', value: 'option_1' }, - { label: 'option2', value: 'option_2' }, - ], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const idSelect = getByTestId('filter-add_value-select'); - expect(idSelect).toBeDefined(); -}); - -it('should display tag filter form if the filter is a tag', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'tags', - label: 'Tags', - type: 'Tags', - comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const tagInputs = getByTestId('filter-tag-inputs'); - expect(tagInputs).toBeDefined(); -}); - -it('should display tags inputs if the filter is a tag', () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'tags', - label: 'Tags', - type: 'Tags', - comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - const tagInputs = getByTestId('filter-tag-inputs'); - expect(tagInputs).toBeDefined(); -}); - -it('should disable submit if tag value is not set and comparator is not EXISTS/NOT_EXISTS', async () => { - const mockOnAddFilter = vitest.fn(); - const props = { - columns: [ - { - id: 'tags', - label: 'Tags', - type: 'Tags', - comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], - }, - ], - onAddFilter: mockOnAddFilter, - } as FilterAddProps; - - const { getByTestId } = renderComponent(props); - - // Select tags column - const columnSelect = getByTestId('add-filter_select_idColumn'); - fireEvent.change(columnSelect, { target: { value: 'tags' } }); - - // Select EQ comparator - const operatorSelect = getByTestId('add-operator-tags'); - fireEvent.change(operatorSelect, { target: { value: 'EQ' } }); - - // Submit should be disabled when no tag value set - const submitButton = getByTestId('filter-add_submit'); - expect(submitButton).toHaveAttribute('is-disabled', 'true'); - - // Change to EXISTS comparator - act(() => { - fireEvent.change(operatorSelect, { target: { value: 'EXISTS' } }); - }); - - await waitFor(() => { - expect(submitButton).toHaveAttribute('is-disabled', 'false'); - }); - - // Change to NOT_EXISTS comparator - act(() => { - fireEvent.change(operatorSelect, { target: { value: 'NOT_EXISTS' } }); - }); - - await waitFor(() => { - expect(submitButton).toHaveAttribute('is-disabled', 'false'); - }); -}); diff --git a/packages/manager-react-components/src/components/filters/filter-list.component.tsx b/packages/manager-react-components/src/components/filters/filter-list.component.tsx deleted file mode 100644 index 9526b804af5c..000000000000 --- a/packages/manager-react-components/src/components/filters/filter-list.component.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { OdsTag } from '@ovhcloud/ods-components/react'; -import { ODS_TAG_COLOR } from '@ovhcloud/ods-components'; -import { useTranslation } from 'react-i18next'; -import { FilterWithLabel } from './interface'; -import './translations'; -import { formatFilter } from './format-filter'; - -export type FilterListProps = { - filters: FilterWithLabel[]; - onRemoveFilter: (filter: FilterWithLabel) => void; -}; - -export function FilterList({ - filters, - onRemoveFilter, -}: Readonly) { - const { t, i18n } = useTranslation('filters'); - const tComp = (comparator: string) => - t(`common_criteria_adder_operator_${comparator}`); - const locale = i18n.language?.replace('_', '-') || 'FR-fr'; - - return ( - <> - {filters?.map((filter, key) => ( - onRemoveFilter(filter)} - data-testid="filter-list_tag_item" - label={`${ - filter.label ? `${filter.label} ${tComp(filter.comparator)} ` : '' - } - ${formatFilter(filter, locale)}`} - /> - ))} - - ); -} diff --git a/packages/manager-react-components/src/components/filters/filter-list.spec.tsx b/packages/manager-react-components/src/components/filters/filter-list.spec.tsx deleted file mode 100644 index 94859328b22e..000000000000 --- a/packages/manager-react-components/src/components/filters/filter-list.spec.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { OdsTag } from '@ovhcloud/ods-components'; -import { act } from '@testing-library/react'; -import { FilterList, FilterListProps } from './filter-list.component'; -import { render } from '../../utils/test.provider'; - -const renderComponent = (props: FilterListProps) => { - return render(); -}; - -describe('FilterList tests', () => { - it('should not display tags when the filters props is empty', () => { - const propsWithEmptyFilters = { - filters: [], - onRemoveFilter: vitest.fn(), - } as FilterListProps; - - const { container } = renderComponent(propsWithEmptyFilters); - - expect(container).toBeEmptyDOMElement(); - }); - - it('should display 1 tag when the filters array props have one element', () => { - const propsWithOneFiltersItem = { - filters: [ - { - key: 'username', - comparator: 'includes', - value: 'temp_user', - label: "Nom d'utilisateur", - }, - ], - onRemoveFilter: vitest.fn(), - } as FilterListProps; - - const { container, getAllByTestId } = renderComponent( - propsWithOneFiltersItem, - ); - - const filterChipItems = getAllByTestId('filter-list_tag_item'); - - expect(container).not.toBeEmptyDOMElement(); - expect(filterChipItems).toHaveLength(1); - }); - - it('should display 2 tags when the filters array props have two elements', () => { - const propsWithTwoFiltersItem = { - filters: [ - { - key: 'username', - comparator: 'includes', - value: 'random_user', - label: "Nom d'utilisateur", - }, - { - key: 'username', - comparator: 'includes', - value: 'temp_user', - label: "Nom d'utilisateur", - }, - ], - onRemoveFilter: vitest.fn(), - } as FilterListProps; - - const { container, getAllByTestId } = renderComponent( - propsWithTwoFiltersItem, - ); - - const filterChipItems = getAllByTestId('filter-list_tag_item'); - - expect(container).not.toBeEmptyDOMElement(); - expect(filterChipItems).toHaveLength(2); - }); - - it('should display a formatted date when the filter type is a Date', () => { - const propsWithDateFilter = { - filters: [ - { - key: 'createdAt', - comparator: 'is_equal', - value: new Date('2023-10-01').toISOString(), - label: 'Creation Date', - type: 'Date', - }, - ], - onRemoveFilter: vitest.fn(), - } as FilterListProps; - - const { container, getByTestId } = renderComponent(propsWithDateFilter); - - const filterChipItem = getByTestId('filter-list_tag_item'); - - expect(container).not.toBeEmptyDOMElement(); - expect(filterChipItem.getAttribute('label')).toContain('Creation Date'); - expect(filterChipItem.getAttribute('label')).toContain('01/10/2023'); - }); - - it('should call onRemoveFilter function when the chip cross is clicked', () => { - const mockOnRemoveFilter = vitest.fn(); - const propsWithOneFiltersItem = { - filters: [ - { - key: 'username', - comparator: 'includes', - value: 'temp_user', - label: "Nom d'utilisateur", - }, - ], - onRemoveFilter: mockOnRemoveFilter, - } as FilterListProps; - - const { getByTestId } = renderComponent(propsWithOneFiltersItem); - - const filterChipItem = getByTestId( - 'filter-list_tag_item', - ) as unknown as OdsTag; - - act(() => { - filterChipItem.onClick(); - }); - - expect(mockOnRemoveFilter).toHaveBeenNthCalledWith(1, { - comparator: 'includes', - key: 'username', - label: "Nom d'utilisateur", - value: 'temp_user', - }); - }); -}); diff --git a/packages/manager-react-components/src/components/filters/filters.scss b/packages/manager-react-components/src/components/filters/filters.scss deleted file mode 100644 index 778662e57d2a..000000000000 --- a/packages/manager-react-components/src/components/filters/filters.scss +++ /dev/null @@ -1,3 +0,0 @@ -.filter-add-button-submit::part(button) { - width: 100%; -} diff --git a/packages/manager-react-components/src/components/filters/format-filter.ts b/packages/manager-react-components/src/components/filters/format-filter.ts deleted file mode 100644 index 5f11768dfe5b..000000000000 --- a/packages/manager-react-components/src/components/filters/format-filter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FilterTypeCategories } from '@ovh-ux/manager-core-api'; -import { FilterWithLabel } from './interface'; - -export function formatFilter(filter: FilterWithLabel, locale?: string): string { - if (!filter) return ''; - switch (filter.type) { - case FilterTypeCategories.Date: - return new Date(`${filter.value}`).toLocaleDateString(locale); - case FilterTypeCategories.Tags: - return filter.value - ? `${filter.tagKey}:${filter.value}` - : filter.tagKey || ''; - default: - return filter.value as string; - } -} diff --git a/packages/manager-react-components/src/components/filters/index.ts b/packages/manager-react-components/src/components/filters/index.ts deleted file mode 100644 index 98c78cd77f18..000000000000 --- a/packages/manager-react-components/src/components/filters/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { FilterAdd } from './filter-add.component'; -export { FilterList } from './filter-list.component'; -export { useColumnFilters } from './useColumnFilters'; -export * from './tags-filter-form.component'; diff --git a/packages/manager-react-components/src/components/filters/interface.ts b/packages/manager-react-components/src/components/filters/interface.ts deleted file mode 100644 index 9c829e3915b1..000000000000 --- a/packages/manager-react-components/src/components/filters/interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Filter } from '@ovh-ux/manager-core-api'; - -export type FilterWithLabel = Filter & { label: string }; diff --git a/packages/manager-react-components/src/components/filters/tags-filter-form.component.spec.tsx b/packages/manager-react-components/src/components/filters/tags-filter-form.component.spec.tsx deleted file mode 100644 index 7b0ab40d1434..000000000000 --- a/packages/manager-react-components/src/components/filters/tags-filter-form.component.spec.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import { vi, vitest } from 'vitest'; -import { fireEvent } from '@testing-library/react'; -import { - TagsFilterForm, - TagsFilterFormProps, -} from './tags-filter-form.component'; -import { render } from '../../utils/test.provider'; - -const mocks = vi.hoisted(() => ({ - useGetResourceTags: vi.fn(), -})); - -vi.mock('../../hooks/iam/useOvhIam', () => ({ - useGetResourceTags: mocks.useGetResourceTags, -})); - -const TestWrapper = () => { - const [tagKey, setTagKey] = React.useState(''); - const [value, setValue] = React.useState(''); - - const defaultProps: TagsFilterFormProps = { - resourceType: 'testResource', - tagKey, - setTagKey, - value, - setValue, - }; - - return ; -}; - -describe('FilterTagValue', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('shows loading skeletons when loading', async () => { - mocks.useGetResourceTags.mockReturnValue({ - tags: [], - isError: false, - isLoading: true, - }); - const { container } = render(); - expect(container.querySelectorAll('ods-skeleton').length).toBeGreaterThan( - 0, - ); - }); - - it('shows tag key options when loaded', () => { - mocks.useGetResourceTags.mockReturnValue({ - tags: [ - { key: 'env', values: ['prod', 'dev'] }, - { key: 'region', values: ['EU', 'US'] }, - ], - isError: false, - isLoading: false, - }); - const { getByTestId, getByText } = render(); - const tagKeyCombo = getByTestId('tags-filter-form-key-field'); - const tagValueCombo = getByTestId('tags-filter-form-value-field'); - expect(tagKeyCombo).toBeInTheDocument(); - expect(tagValueCombo).toBeInTheDocument(); - - fireEvent.click(tagKeyCombo); - expect(getByText('env')).toBeInTheDocument(); - expect(getByText('region')).toBeInTheDocument(); - }); - - it('disables tag value combobox until tag key is selected', () => { - mocks.useGetResourceTags.mockReturnValue({ - tags: [{ key: 'env', values: ['prod', 'dev'] }], - isError: false, - isLoading: false, - }); - const { getByTestId } = render(); - const tagValueCombo = getByTestId('tags-filter-form-value-field'); - const tagKeyCombo = getByTestId('tags-filter-form-key-field'); - expect(tagKeyCombo).not.toHaveAttribute('is-disabled'); - expect(tagValueCombo).toHaveAttribute('is-disabled'); - }); -}); diff --git a/packages/manager-react-components/src/components/filters/tags-filter-form.component.tsx b/packages/manager-react-components/src/components/filters/tags-filter-form.component.tsx deleted file mode 100644 index 5375438b0080..000000000000 --- a/packages/manager-react-components/src/components/filters/tags-filter-form.component.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useState } from 'react'; -import { - OdsComboboxItem, - OdsCombobox, - OdsSkeleton, - OdsFormField, -} from '@ovhcloud/ods-components/react'; - -import { useTranslation } from 'react-i18next'; -import { useGetResourceTags } from '../../hooks/iam/useOvhIam'; - -export type TagsFilterFormProps = { - resourceType: string; - tagKey: string; - setTagKey: (tagKey: string) => void; - value: string; - setValue: (value: string) => void; -}; - -export function TagsFilterForm({ - resourceType, - tagKey, - setTagKey, - value, - setValue, -}: TagsFilterFormProps) { - const { t } = useTranslation('filters'); - - const { - tags, - isError: isTagsError, - isLoading: isTagsLoading, - } = useGetResourceTags({ - resourceType, - }); - - return ( -
- -
- - {t('common_criteria_adder_key_label')} - -
- {isTagsLoading && } - {!isTagsLoading && ( - { - setTagKey(event.detail.value); - }} - data-testid="tags-filter-form-key-field" - > - {!isTagsError && - tags?.map((tag) => ( - - {tag.key} - - ))} - - )} -
- -
- - {t('common_criteria_adder_value_label')} - -
- {isTagsLoading && } - {!isTagsLoading && ( - { - setValue(event.detail.value); - }} - data-testid="tags-filter-form-value-field" - > - {tags - ?.find((tag) => tag.key === tagKey) - ?.values?.map((tagValue) => ( - - {tagValue} - - ))} - - )} -
-
- ); -} diff --git a/packages/manager-react-components/src/components/formatted-date/FormattedDate.tsx b/packages/manager-react-components/src/components/formatted-date/FormattedDate.tsx deleted file mode 100644 index 924d80052d48..000000000000 --- a/packages/manager-react-components/src/components/formatted-date/FormattedDate.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @deprecated Use useFormatDate with date fns format instead - */ -import { - FormattedDateProps, - useFormattedDate, -} from '../../hooks/date/useFormattedDate'; - -/** - * @deprecated Use useFormatDate with date fns format instead - */ -export const FormattedDate = (props: FormattedDateProps) => { - const formattedDate = useFormattedDate(props); - - return <>{formattedDate}; -}; diff --git a/packages/manager-react-components/src/components/guides-header/guides-header-item.component.tsx b/packages/manager-react-components/src/components/guides-header/guides-header-item.component.tsx deleted file mode 100644 index a8fd98d16c4e..000000000000 --- a/packages/manager-react-components/src/components/guides-header/guides-header-item.component.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { OdsLink } from '@ovhcloud/ods-components/react'; -import { ODS_ICON_NAME } from '@ovhcloud/ods-components'; -import { Guide } from './interface'; - -interface GuidesHeaderItemProps { - guide: Guide; - href: string; - label: string; - tracking?: string; - onClick?: (guide: Guide) => void; -} - -export function GuidesHeaderItem({ - guide, - href, - label, - onClick, -}: GuidesHeaderItemProps) { - return ( -
- { - if (onClick) { - onClick(guide); - } - }} - label={label} - /> -
- ); -} diff --git a/packages/manager-react-components/src/components/guides-header/guides-header.component.spec.tsx b/packages/manager-react-components/src/components/guides-header/guides-header.component.spec.tsx deleted file mode 100644 index 951cd5e0107d..000000000000 --- a/packages/manager-react-components/src/components/guides-header/guides-header.component.spec.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { fireEvent } from '@testing-library/react'; -import { GuidesHeader, GuidesHeaderProps } from './guides-header.component'; -import { render } from '../../utils/test.provider'; - -const renderComponent = (props: GuidesHeaderProps) => { - return render(); -}; - -const guides = { - foo: { - key: 'foo-guide', - url: { - EU: 'foo-eu', - US: 'foo-us', - }, - }, - bar: { - key: 'bar-guide', - url: { - EU: 'bar-eu', - US: 'bar-us', - }, - }, -}; - -describe('GuidesHeader tests', () => { - it('should display guides list', () => { - const { container } = renderComponent({ - label: 'hello', - guides, - ovhSubsidiary: 'EU', - getGuideLabel: (guide) => guide.key, - }); - expect(container.querySelector('[label="foo-guide"]')).not.toBeNull(); - expect(container.querySelector('[label="bar-guide"]')).not.toBeNull(); - }); - - it('should display guides urls depending on ovhSubsidiary', () => { - let { container } = renderComponent({ - label: 'hello', - guides, - ovhSubsidiary: 'EU', - getGuideLabel: (guide) => guide.key, - }); - expect(container.querySelector('[href=foo-us]')).toBeNull(); - expect(container.querySelector('[href=foo-eu]')).not.toBeNull(); - expect(container.querySelector('[href=bar-us]')).toBeNull(); - expect(container.querySelector('[href=bar-eu]')).not.toBeNull(); - container = renderComponent({ - label: 'hello', - guides, - ovhSubsidiary: 'US', - getGuideLabel: (guide) => guide.key, - }).container; - expect(container.querySelector('[href=foo-us]')).not.toBeNull(); - expect(container.querySelector('[href=foo-eu]')).toBeNull(); - expect(container.querySelector('[href=bar-us]')).not.toBeNull(); - expect(container.querySelector('[href=bar-eu]')).toBeNull(); - }); - - it('should trigger onGuideClick', () => { - const onGuideClick = vitest.fn(); - const { container } = renderComponent({ - label: 'hello', - guides, - ovhSubsidiary: 'EU', - getGuideLabel: (guide) => guide.key, - onGuideClick, - }); - expect(onGuideClick).not.toHaveBeenCalled(); - fireEvent.click(container.querySelector('[href=foo-eu]')); - expect(onGuideClick).toHaveBeenCalled(); - }); - - it('should not trigger onGuideClick if it is undefined', () => { - const onGuideClick = vitest.fn(); - const { container } = renderComponent({ - label: 'hello', - guides, - ovhSubsidiary: 'EU', - getGuideLabel: (guide) => guide.key, - onGuideClick: undefined, - }); - expect(onGuideClick).not.toHaveBeenCalled(); - fireEvent.click(container.querySelector('[href=foo-eu]')); - expect(onGuideClick).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/manager-react-components/src/components/guides-header/guides-header.component.tsx b/packages/manager-react-components/src/components/guides-header/guides-header.component.tsx deleted file mode 100644 index 7cfd0669c9a6..000000000000 --- a/packages/manager-react-components/src/components/guides-header/guides-header.component.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { OdsButton, OdsPopover } from '@ovhcloud/ods-components/react'; -import { ODS_BUTTON_VARIANT, ODS_ICON_NAME } from '@ovhcloud/ods-components'; - -import { GuidesHeaderItem } from './guides-header-item.component'; -import { Guide } from './interface'; - -export interface GuidesHeaderProps { - label: string; - guides: Record; - ovhSubsidiary: string; - getGuideLabel: (guide: Guide) => string; - onGuideClick?: (guide: Guide) => void; -} - -/** - * @deprecated Use `GuideMenu` component from MRC V3 instead. - */ -export function GuidesHeader({ - label, - guides, - ovhSubsidiary, - getGuideLabel, - onGuideClick, -}: GuidesHeaderProps) { - return ( - <> -
- -
- - {Object.keys(guides).map((guide) => ( - { - if (onGuideClick) { - onGuideClick(g); - } - }} - /> - ))} - - - ); -} diff --git a/packages/manager-react-components/src/components/guides-header/index.ts b/packages/manager-react-components/src/components/guides-header/index.ts deleted file mode 100644 index 69a1fc3038bd..000000000000 --- a/packages/manager-react-components/src/components/guides-header/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GuidesHeader } from './guides-header.component'; -export { PciGuidesHeader } from './pci/pci-guides-header.component'; diff --git a/packages/manager-react-components/src/components/guides-header/interface.ts b/packages/manager-react-components/src/components/guides-header/interface.ts deleted file mode 100644 index 35482f440d67..000000000000 --- a/packages/manager-react-components/src/components/guides-header/interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Guide { - key: string; // guide uniquer identifier - url: Record; // mapping of ovhSubsidiary and URLs - tracking?: string; // optional data tracking to be sent -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.component.tsx b/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.component.tsx deleted file mode 100644 index 4eda62a1bb55..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.component.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { - useEnvironment, - useTracking, -} from '@ovh-ux/manager-react-shell-client'; -import { useTranslation } from 'react-i18next'; -import { GuidesHeader } from '../guides-header.component'; -import { GUIDES_LIST } from './pci-guides-header.constants'; -import { Guide } from '../interface'; -import './translations'; - -interface PciGuidesHeaderProps { - category: string; - onGuideClick?: (key: string) => void; -} - -/** - * @deprecated Use `GuideMenu` component from MRC V3 instead. - */ -export function PciGuidesHeader({ - category, - onGuideClick, -}: PciGuidesHeaderProps) { - const { ovhSubsidiary } = useEnvironment().getUser(); - const { trackClick } = useTracking(); - const [t] = useTranslation('pci-guides-header'); - return ( - - t(`pci_project_guides_header_${guide.key}`) - } - onGuideClick={(guide: Guide) => { - if (onGuideClick) { - onGuideClick(guide.key); - } else { - trackClick({ - name: `public-cloud_credit_and_vouchers${guide.tracking}`, - type: 'action', - }); - } - }} - /> - ); -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.constants.ts b/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.constants.ts deleted file mode 100644 index affaa5a2d657..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/pci-guides-header.constants.ts +++ /dev/null @@ -1,773 +0,0 @@ -import { Guide } from '../interface'; - -type GuideLinks = Record; - -export const PUBLIC_CLOUD_GUIDES: GuideLinks = { - FR: 'https://docs.ovh.com/fr/public-cloud/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/', - DE: 'https://docs.ovh.com/de/public-cloud/', - ES: 'https://docs.ovh.com/es/public-cloud/', - IT: 'https://docs.ovh.com/it/public-cloud/', - PL: 'https://docs.ovh.com/pl/public-cloud/', - PT: 'https://docs.ovh.com/pt/public-cloud/', - IE: 'https://docs.ovh.com/ie/en/public-cloud/', - DEFAULT: 'https://docs.ovh.com/gb/en/public-cloud/', - US: 'https://support.us.ovhcloud.com/hc/en-us/categories/115000515130-Public-Cloud-Services', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/', - AU: 'https://docs.ovh.com/au/en/public-cloud/', - CA: 'https://docs.ovh.com/ca/en/public-cloud/', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/', - SG: 'https://docs.ovh.com/sg/en/public-cloud/', - WE: 'https://docs.ovh.com/us/en/public-cloud/', - WS: 'https://docs.ovh.com/us/es/public-cloud/', - MA: 'https://docs.ovh.com/fr/public-cloud/', - TN: 'https://docs.ovh.com/fr/public-cloud/', - SN: 'https://docs.ovh.com/fr/public-cloud/', - IN: 'https://docs.ovh.com/asia/en/public-cloud/', -}; - -export const PUBLIC_CLOUD_STORAGE_GUIDES: GuideLinks = { - ASIA: 'https://help.ovhcloud.com/csm/asia-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - DE: 'https://help.ovhcloud.com/csm/de-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - AU: 'https://help.ovhcloud.com/csm/en-au-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - CA: 'https://help.ovhcloud.com/csm/en-ca-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - GB: 'https://help.ovhcloud.com/csm/en-gb-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - IE: 'https://help.ovhcloud.com/csm/en-ie-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - NL: 'https://help.ovhcloud.com/csm/en-nl-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - SG: 'https://help.ovhcloud.com/csm/en-sg-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - WE: 'https://help.ovhcloud.com/csm/en-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - ES: 'https://help.ovhcloud.com/csm/es-es-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - WS: 'https://help.ovhcloud.com/csm/es-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - QC: 'https://help.ovhcloud.com/csm/fr-ca-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - MA: 'https://help.ovhcloud.com/csm/fr-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - FR: 'https://help.ovhcloud.com/csm/fr-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - SN: 'https://help.ovhcloud.com/csm/fr-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - TN: 'https://help.ovhcloud.com/csm/fr-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - IT: 'https://help.ovhcloud.com/csm/it-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - PL: 'https://help.ovhcloud.com/csm/pl-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - PT: 'https://help.ovhcloud.com/csm/pt-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - IN: 'https://help.ovhcloud.com/csm/en-in-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', - US: 'https://support.us.ovhcloud.com/hc/en-us/sections/23498311424275-Object-Storage', - DEFAULT: - 'https://help.ovhcloud.com/csm/en-gb-documentation-public-cloud-storage-object-storage?id=kb_browse_cat&kb_id=574a8325551974502d4c6e78b7421938&kb_category=6f34d555f49801102d4ca4d466a7fd9b&spa=1', -}; - -export const FIRST_STEPS_WITH_STORAGES: GuideLinks = { - ASIA: 'https://help.ovhcloud.com/csm/asia-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047333', - DE: 'https://help.ovhcloud.com/csm/de-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047336', - AU: 'https://help.ovhcloud.com/csm/en-au-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047335', - CA: 'https://help.ovhcloud.com/csm/en-ca-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047343', - GB: 'https://help.ovhcloud.com/csm/en-gb-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0034674', - IE: 'https://help.ovhcloud.com/csm/en-ie-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047339', - NL: 'https://help.ovhcloud.com/csm/en-ie-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047339', - SG: 'https://help.ovhcloud.com/csm/en-sg-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047361', - WE: 'https://help.ovhcloud.com/csm/en-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047348', - ES: 'https://help.ovhcloud.com/csm/es-es-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047353', - WS: 'https://help.ovhcloud.com/csm/es-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047352', - QC: 'https://help.ovhcloud.com/csm/fr-ca-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047355', - MA: 'https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047354', - FR: 'https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047354', - SN: 'https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047354', - TN: 'https://help.ovhcloud.com/csm/fr-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047354', - IT: 'https://help.ovhcloud.com/csm/it-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047356', - PL: 'https://help.ovhcloud.com/csm/pl-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047359', - PT: 'https://help.ovhcloud.com/csm/pt-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0047346', - IN: 'https://help.ovhcloud.com/csm/en-in-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0069681', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/4603838122643-Getting-started-with-Object-Storage', - DEFAULT: - 'https://help.ovhcloud.com/csm/en-gb-public-cloud-storage-s3-getting-started-object-storage?id=kb_article_view&sysparm_article=KB0034674', -}; - -export const STORAGES_VOLUME_BACKUP_GUIDES: GuideLinks = { - DEFAULT: 'https://docs.ovh.com/gb/en/public-cloud/volume-backup', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/volume-backup', - AU: 'https://docs.ovh.com/au/en/public-cloud/volume-backup', - CA: 'https://docs.ovh.com/ca/en/public-cloud/volume-backup', - DE: 'https://docs.ovh.com/de/public-cloud/volume-backup/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/volume-backup', - IE: 'https://docs.ovh.com/ie/en/public-cloud/volume-backup', - SG: 'https://docs.ovh.com/sg/en/public-cloud/volume-backup', - ES: 'https://docs.ovh.com/es/public-cloud/volume-backup', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/15694887096851-How-to-Create-a-Volume-Backup', - FR: 'https://docs.ovh.com/fr/public-cloud/volume-backup', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/volume-backup', - IT: 'https://docs.ovh.com/it/public-cloud/volume-backup', - PL: 'https://docs.ovh.com/pl/public-cloud/volume-backup', - PT: 'https://docs.ovh.com/pt/public-cloud/volume-backup', - WS: 'https://docs.ovh.com/us/es/public-cloud/volume-backup/', -}; - -export const FIRST_STEPS_WITH_INSTANCES: GuideLinks = { - FR: 'https://docs.ovh.com/fr/public-cloud/premiers-pas-instance-public-cloud/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/public-cloud-first-steps/', - DE: 'https://docs.ovh.com/de/public-cloud/public-cloud-erste-schritte/', - ES: 'https://docs.ovh.com/es/public-cloud/public-cloud-primeros-pasos/', - IT: 'https://docs.ovh.com/it/public-cloud/primi-passi-public-cloud/', - PL: 'https://docs.ovh.com/pl/public-cloud/public-cloud-pierwsze-kroki/', - PT: 'https://docs.ovh.com/pt/public-cloud/public-cloud-primeiros-passos/', - IE: 'https://docs.ovh.com/ie/en/public-cloud/public-cloud-first-steps/', - DEFAULT: 'https://docs.ovh.com/gb/en/public-cloud/public-cloud-first-steps/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/4481009956243-How-to-Manage-Your-Public-Cloud-Instance', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/public-cloud-first-steps/', - AU: 'https://docs.ovh.com/au/en/public-cloud/public-cloud-first-steps/', - CA: 'https://docs.ovh.com/ca/en/public-cloud/public-cloud-first-steps/', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/premiers-pas-instance-public-cloud/', - SG: 'https://docs.ovh.com/sg/en/public-cloud/public-cloud-first-steps/', - WE: 'https://docs.ovh.com/us/en/public-cloud/public-cloud-first-steps/', - WS: 'https://docs.ovh.com/us/es/public-cloud/public-cloud-primeros-pasos/', - IN: 'https://docs.ovh.com/asia/en/public-cloud/public-cloud-first-steps/', -}; - -export const IP_FAIL_OVER: GuideLinks = { - FR: 'https://docs.ovh.com/fr/public-cloud/configurer_une_ip_failover/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/configure_a_failover_ip/', - DE: 'https://docs.ovh.com/de/public-cloud/failover-ip-konfigurieren-pci/', - ES: 'https://docs.ovh.com/es/public-cloud/configurer-une-ip-failover/', - IT: 'https://docs.ovh.com/it/public-cloud/configura-un-ip-failover/', - PL: 'https://docs.ovh.com/pl/public-cloud/konfiguracja-adresu-ip-failover/', - PT: 'https://docs.ovh.com/pt/public-cloud/configurer-une-ip-failover/', - IE: 'https://docs.ovh.com/ie/en/public-cloud/configure_a_failover_ip/', - DEFAULT: 'https://docs.ovh.com/gb/en/public-cloud/configure_a_failover_ip/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/115001588270-How-to-Order-Failover-IPs', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/configure_a_failover_ip/', - AU: 'https://docs.ovh.com/au/en/public-cloud/configure_a_failover_ip/', - CA: 'https://docs.ovh.com/ca/en/public-cloud/configure_a_failover_ip/', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/configurer_une_ip_failover/', - SG: 'https://docs.ovh.com/sg/en/public-cloud/configure_a_failover_ip/', - WE: 'https://docs.ovh.com/us/en/public-cloud/configure_a_failover_ip/', - WS: 'https://docs.ovh.com/us/es/public-cloud/configurer-une-ip-failover/', - IN: 'https://docs.ovh.com/asia/en/public-cloud/configure_a_failover_ip/', -}; - -export const USER_ROOT_AND_PASSWORD: GuideLinks = { - FR: 'https://docs.ovh.com/fr/public-cloud/passer-root-et-definir-un-mot-de-passe/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/become_the_root_user_and_select_a_password/', - DE: 'https://docs.ovh.com/de/public-cloud/root-rechte_erlangen_und_passwort_festlegen/', - ES: 'https://docs.ovh.com/es/public-cloud/conectarse_como_usuario_root_y_establecer_una_contrasena/', - IT: 'https://docs.ovh.com/it/public-cloud/imposta_una_password_amministratore/', - PL: 'https://docs.ovh.com/pl/public-cloud/dostep_root_i_zdefiniowanie_hasla/', - PT: 'https://docs.ovh.com/pt/public-cloud/tornar-se_root_e_definir_uma_palavra-passe/', - IE: 'https://docs.ovh.com/ie/en/public-cloud/become_the_root_user_and_select_a_password/', - DEFAULT: - 'https://docs.ovh.com/gb/en/public-cloud/become_the_root_user_and_select_a_password/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/360002208690-How-to-Access-a-Public-Cloud-Instance-via-VNC', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/become_the_root_user_and_select_a_password/', - AU: 'https://docs.ovh.com/au/en/public-cloud/become_the_root_user_and_select_a_password/', - CA: 'https://docs.ovh.com/ca/en/public-cloud/become_the_root_user_and_select_a_password/', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/passer-root-et-definir-un-mot-de-passe/', - SG: 'https://docs.ovh.com/sg/en/public-cloud/become_the_root_user_and_select_a_password/', - WE: 'https://docs.ovh.com/us/en/public-cloud/become_the_root_user_and_select_a_password/', - WS: 'https://docs.ovh.com/us/es/public-cloud/conectarse_como_usuario_root_y_establecer_una_contrasena/', - IN: 'https://docs.ovh.com/asia/en/public-cloud/become_the_root_user_and_select_a_password/', -}; - -export const REVERSE_DNS: GuideLinks = { - FR: 'https://docs.ovh.com/fr/public-cloud/configurer-le-reverse-dns-dune-instance/', - GB: 'https://docs.ovh.com/gb/en/public-cloud/configure-reverse-dns-instance/', - DE: 'https://docs.ovh.com/de/public-cloud/reverse-dns-konfigurieren-instanz/', - ES: 'https://docs.ovh.com/es/public-cloud/configurar-el-inverso-dns-de-una-instancia/', - IT: 'https://docs.ovh.com/it/public-cloud/configura_il_reverse_dns_della_tua_istanza/', - PL: 'https://docs.ovh.com/pl/public-cloud/konfiguracja_rewersu_dns_instancji/', - PT: 'https://docs.ovh.com/pt/public-cloud/configurar_a_reverse_dns_de_uma_instancia/', - IE: 'https://docs.ovh.com/ie/en/public-cloud/configure-reverse-dns-instance/', - DEFAULT: - 'https://docs.ovh.com/gb/en/public-cloud/configure-reverse-dns-instance/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/360002181530-How-to-Configure-Reverse-DNS', - ASIA: 'https://docs.ovh.com/asia/en/public-cloud/configure-reverse-dns-instance/', - AU: 'https://docs.ovh.com/au/en/public-cloud/configure-reverse-dns-instance/', - CA: 'https://docs.ovh.com/ca/en/public-cloud/configure-reverse-dns-instance/', - QC: 'https://docs.ovh.com/ca/fr/public-cloud/configurer-le-reverse-dns-dune-instance/', - SG: 'https://docs.ovh.com/sg/en/public-cloud/configure-reverse-dns-instance/', - WE: 'https://docs.ovh.com/us/en/public-cloud/configure-reverse-dns-instance/', - WS: 'https://docs.ovh.com/us/es/public-cloud/configurar-el-inverso-dns-de-una-instancia/', - IN: 'https://docs.ovh.com/asia/en/public-cloud/configure-reverse-dns-instance/', -}; - -export const FIRST_STEPS_WITH_DATABASES: GuideLinks = { - FR: 'https://docs.ovh.com/fr/publiccloud/databases/getting-started/', - GB: 'https://docs.ovh.com/gb/en/publiccloud/databases/getting-started/', - DE: 'https://docs.ovh.com/de/publiccloud/databases/getting-started/', - ES: 'https://docs.ovh.com/es/publiccloud/databases/getting-started/', - IT: 'https://docs.ovh.com/it/publiccloud/databases/getting-started/', - PL: 'https://docs.ovh.com/pl/publiccloud/databases/getting-started/', - PT: 'https://docs.ovh.com/pt/publiccloud/databases/getting-started/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/databases/getting-started/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/databases/getting-started/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/databases/getting-started/', - AU: 'https://docs.ovh.com/au/en/publiccloud/databases/getting-started/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/databases/getting-started/', - QC: 'https://docs.ovh.com/ca/fr/publiccloud/databases/getting-started/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/databases/getting-started/', - WE: 'https://docs.ovh.com/us/en/publiccloud/databases/getting-started/', - WS: 'https://docs.ovh.com/us/es/publiccloud/databases/getting-started/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/databases/getting-started/', -}; - -export const MONGO_DB_CAPABILITIES_AND_LIMITATIONS: GuideLinks = { - FR: 'https://docs.ovh.com/fr/publiccloud/databases/mongodb/capabilities/', - GB: 'https://docs.ovh.com/gb/en/publiccloud/databases/mongodb/capabilities/', - DE: 'https://docs.ovh.com/de/publiccloud/databases/mongodb/capabilities/', - ES: 'https://docs.ovh.com/es/publiccloud/databases/mongodb/capabilities/', - IT: 'https://docs.ovh.com/it/publiccloud/databases/mongodb/capabilities/', - PL: 'https://docs.ovh.com/pl/publiccloud/databases/mongodb/capabilities/', - PT: 'https://docs.ovh.com/pt/publiccloud/databases/mongodb/capabilities/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/databases/mongodb/capabilities/', - DEFAULT: - 'https://docs.ovh.com/gb/en/publiccloud/databases/mongodb/capabilities/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/databases/mongodb/capabilities/', - AU: 'https://docs.ovh.com/au/en/publiccloud/databases/mongodb/capabilities/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/databases/mongodb/capabilities/', - QC: 'https://docs.ovh.com/ca/fr/publiccloud/databases/mongodb/capabilities/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/databases/mongodb/capabilities/', - WE: 'https://docs.ovh.com/us/en/publiccloud/databases/mongodb/capabilities/', - WS: 'https://docs.ovh.com/us/es/publiccloud/databases/mongodb/capabilities/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/databases/mongodb/capabilities/', -}; - -export const MYSQL_CAPABILITIES_AND_LIMITATIONS: GuideLinks = { - FR: 'https://docs.ovh.com/fr/publiccloud/databases/mysql/capabilities/', - GB: 'https://docs.ovh.com/gb/en/publiccloud/databases/mysql/capabilities/', - DE: 'https://docs.ovh.com/de/publiccloud/databases/mysql/capabilities/', - ES: 'https://docs.ovh.com/es/publiccloud/databases/mysql/capabilities/', - IT: 'https://docs.ovh.com/it/publiccloud/databases/mysql/capabilities/', - PL: 'https://docs.ovh.com/pl/publiccloud/databases/mysql/capabilities/', - PT: 'https://docs.ovh.com/pt/publiccloud/databases/mysql/capabilities/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/databases/mysql/capabilities/', - DEFAULT: - 'https://docs.ovh.com/gb/en/publiccloud/databases/mysql/capabilities/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/databases/mysql/capabilities/', - AU: 'https://docs.ovh.com/au/en/publiccloud/databases/mysql/capabilities/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/databases/mysql/capabilities/', - QC: 'https://docs.ovh.com/ca/fr/publiccloud/databases/mysql/capabilities/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/databases/mysql/capabilities/', - WE: 'https://docs.ovh.com/us/en/publiccloud/databases/mysql/capabilities/', - WS: 'https://docs.ovh.com/us/es/publiccloud/databases/mysql/capabilities/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/databases/mysql/capabilities/', -}; - -export const CREATE_A_CLUSTER: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/kubernetes/creating-a-cluster/', - IE: 'https://docs.ovh.com/ie/en/kubernetes/creating-a-cluster/', - DEFAULT: 'https://docs.ovh.com/gb/en/kubernetes/creating-a-cluster/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/1500004767902-How-to-Create-a-Cluster-in-OVHcloud-Managed-Kubernetes', - ASIA: 'https://docs.ovh.com/asia/en/kubernetes/creating-a-cluster/', - AU: 'https://docs.ovh.com/au/en/kubernetes/creating-a-cluster/', - CA: 'https://docs.ovh.com/ca/en/kubernetes/creating-a-cluster/', - SG: 'https://docs.ovh.com/sg/en/kubernetes/creating-a-cluster/', - WE: 'https://docs.ovh.com/us/en/kubernetes/creating-a-cluster/', -}; - -export const DEPLOY_AN_APPLICATION: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/kubernetes/deploying-an-application/', - IE: 'https://docs.ovh.com/ie/en/kubernetes/deploying-an-application/', - DEFAULT: 'https://docs.ovh.com/gb/en/kubernetes/deploying-an-application/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/1500004771762-How-to-Deploy-an-Application-on-an-OVHcloud-Managed-Kubernetes-Cluster', - ASIA: 'https://docs.ovh.com/asia/en/kubernetes/deploying-an-application/', - AU: 'https://docs.ovh.com/au/en/kubernetes/deploying-an-application/', - CA: 'https://docs.ovh.com/ca/en/kubernetes/deploying-an-application/', - SG: 'https://docs.ovh.com/sg/en/kubernetes/deploying-an-application/', - WE: 'https://docs.ovh.com/us/en/kubernetes/deploying-an-application/', - IN: 'https://docs.ovh.com/asia/en/kubernetes/deploying-an-application/', -}; - -export const LOADBALANCER_KUBE: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/kubernetes/using-lb/', - IE: 'https://docs.ovh.com/ie/en/kubernetes/using-lb/', - DEFAULT: 'https://docs.ovh.com/gb/en/kubernetes/using-lb/', - US: 'https://support.us.ovhcloud.com/hc/en-us/articles/1500004806361-How-to-Use-the-OVHcloud-Managed-Kubernetes-Load-Balancer', - ASIA: 'https://docs.ovh.com/asia/en/kubernetes/using-lb/', - AU: 'https://docs.ovh.com/au/en/kubernetes/using-lb/', - CA: 'https://docs.ovh.com/ca/en/kubernetes/using-lb/', - SG: 'https://docs.ovh.com/sg/en/kubernetes/using-lb/', - WE: 'https://docs.ovh.com/us/en/kubernetes/using-lb/', - IN: 'https://docs.ovh.com/asia/en/kubernetes/using-lb/', -}; - -export const FAQ_MANAGED_PRIVATE_REGISTRY: GuideLinks = { - FR: 'https://docs.ovh.com/fr/private-registry/managed-private-registry-faq/', - GB: 'https://docs.ovh.com/gb/en/private-registry/managed-private-registry-faq/', - IE: 'https://docs.ovh.com/ie/en/private-registry/managed-private-registry-faq/', - DEFAULT: - 'https://docs.ovh.com/gb/en/private-registry/managed-private-registry-faq/', - ASIA: 'https://docs.ovh.com/asia/en/private-registry/managed-private-registry-faq/', - AU: 'https://docs.ovh.com/au/en/private-registry/managed-private-registry-faq/', - CA: 'https://docs.ovh.com/ca/en/private-registry/managed-private-registry-faq/', - SG: 'https://docs.ovh.com/sg/en/private-registry/managed-private-registry-faq/', - WE: 'https://docs.ovh.com/us/en/private-registry/managed-private-registry-faq/', - IN: 'https://docs.ovh.com/asia/en/private-registry/managed-private-registry-faq/', -}; - -export const CREATE_A_MANAGED_PRIVATE_REGISTER: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/private-registry/creating-a-private-registry/', - IE: 'https://docs.ovh.com/ie/en/private-registry/creating-a-private-registry/', - DEFAULT: - 'https://docs.ovh.com/gb/en/private-registry/creating-a-private-registry/', - ASIA: 'https://docs.ovh.com/asia/en/private-registry/creating-a-private-registry/', - AU: 'https://docs.ovh.com/au/en/private-registry/creating-a-private-registry/', - CA: 'https://docs.ovh.com/ca/en/private-registry/creating-a-private-registry/', - SG: 'https://docs.ovh.com/sg/en/private-registry/creating-a-private-registry/', - WE: 'https://docs.ovh.com/us/en/private-registry/creating-a-private-registry/', - IN: 'https://docs.ovh.com/asia/en/private-registry/creating-a-private-registry/', -}; - -export const CREATE_AND_USE_A_PRIVATE_IMAGE: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/private-registry/creating-and-using-a-private-image/', - IE: 'https://docs.ovh.com/ie/en/private-registry/creating-and-using-a-private-image/', - DEFAULT: - 'https://docs.ovh.com/gb/en/private-registry/creating-and-using-a-private-image/', - ASIA: 'https://docs.ovh.com/asia/en/private-registry/creating-and-using-a-private-image/', - AU: 'https://docs.ovh.com/au/en/private-registry/creating-and-using-a-private-image/', - CA: 'https://docs.ovh.com/ca/en/private-registry/creating-and-using-a-private-image/', - SG: 'https://docs.ovh.com/sg/en/private-registry/creating-and-using-a-private-image/', - WE: 'https://docs.ovh.com/us/en/private-registry/creating-and-using-a-private-image/', -}; - -export const DIFFERENCES_BETWEEN_AI_NOTEBOOKS_AI_TRAINING_AI_APPS: GuideLinks = - { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/ai-comparative-tables/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/ai-comparative-tables/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/ai/ai-comparative-tables/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/ai-comparative-tables/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/ai-comparative-tables/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/ai-comparative-tables/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/ai-comparative-tables/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/ai-comparative-tables/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/ai-comparative-tables/', - }; - -export const AI_APPS_CAPABILITIES_AND_LIMITATIONS: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/apps/capabilities/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/apps/capabilities/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/ai/apps/capabilities/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/apps/capabilities/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/apps/capabilities/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/apps/capabilities/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/apps/capabilities/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/apps/capabilities/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/apps/capabilities/', -}; - -export const ACCESSING_YOUR_AI_APPS_WITH_TOKENS: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/ai-apps-tokens/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/ai-apps-tokens/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/ai/ai-apps-tokens/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/ai-apps-tokens/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/ai-apps-tokens/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/ai-apps-tokens/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/ai-apps-tokens/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/ai-apps-tokens/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/ai-apps-tokens/', -}; - -export const PRESENTATION_OF_DATA_PROCESSING: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/data-processing/overview/', - IE: 'https://docs.ovh.com/ie/en/data-processing/overview/', - DEFAULT: 'https://docs.ovh.com/gb/en/data-processing/overview/', - ASIA: 'https://docs.ovh.com/asia/en/data-processing/overview/', - AU: 'https://docs.ovh.com/au/en/data-processing/overview/', - CA: 'https://docs.ovh.com/ca/en/data-processing/overview/', - SG: 'https://docs.ovh.com/sg/en/data-processing/overview/', - WE: 'https://docs.ovh.com/us/en/data-processing/overview/', - IN: 'https://docs.ovh.com/asia/en/data-processing/overview/', -}; - -export const DATA_PROCESSING_CAPABILITIES_AND_LIMITATIONS: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/data-processing/capabilities/', - IE: 'https://docs.ovh.com/ie/en/data-processing/capabilities/', - DEFAULT: 'https://docs.ovh.com/gb/en/data-processing/capabilities/', - ASIA: 'https://docs.ovh.com/asia/en/data-processing/capabilities/', - AU: 'https://docs.ovh.com/au/en/data-processing/capabilities/', - CA: 'https://docs.ovh.com/ca/en/data-processing/capabilities/', - SG: 'https://docs.ovh.com/sg/en/data-processing/capabilities/', - WE: 'https://docs.ovh.com/us/en/data-processing/capabilities/', - IN: 'https://docs.ovh.com/asia/en/data-processing/capabilities/', -}; - -export const SUBMIT_A_JAVA_SCALA_JOB: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/data-processing/submit-javascala/', - IE: 'https://docs.ovh.com/ie/en/data-processing/submit-javascala/', - DEFAULT: 'https://docs.ovh.com/gb/en/data-processing/submit-javascala/', - ASIA: 'https://docs.ovh.com/asia/en/data-processing/submit-javascala/', - AU: 'https://docs.ovh.com/au/en/data-processing/submit-javascala/', - CA: 'https://docs.ovh.com/ca/en/data-processing/submit-javascala/', - SG: 'https://docs.ovh.com/sg/en/data-processing/submit-javascala/', - WE: 'https://docs.ovh.com/us/en/data-processing/submit-javascala/', - IN: 'https://docs.ovh.com/asia/en/data-processing/submit-javascala/', -}; - -export const AI_NOTEBOOKS_STARTUP: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/cli/getting-started-cli/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/cli/getting-started-cli/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/ai/cli/getting-started-cli/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/cli/getting-started-cli/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/cli/getting-started-cli/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/cli/getting-started-cli/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/cli/getting-started-cli/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/cli/getting-started-cli/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/cli/getting-started-cli/', -}; - -export const AI_NOTEBOOKS_DEFINITION: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/notebooks/definition/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/notebooks/definition/', - DEFAULT: 'https://docs.ovh.com/gb/en/publiccloud/ai/notebooks/definition/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/notebooks/definition/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/notebooks/definition/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/notebooks/definition/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/notebooks/definition/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/notebooks/definition/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/notebooks/definition/', -}; - -export const USING_DATA_FORM_OBJECT_STORAGE: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/notebooks/manage-data-ui/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/notebooks/manage-data-ui/', - DEFAULT: - 'https://docs.ovh.com/gb/en/publiccloud/ai/notebooks/manage-data-ui/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/notebooks/manage-data-ui/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/notebooks/manage-data-ui/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/notebooks/manage-data-ui/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/notebooks/manage-data-ui/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/notebooks/manage-data-ui/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/notebooks/manage-data-ui/', -}; - -export const AI_TRAINING_CAPABILITIES_AND_LIMITATIONS: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/training/capabilities/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/training/capabilities/', - DEFAULT: 'https://docs.ovh.com/gb/en/ai-training/capabilities/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/capabilities/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/training/capabilities/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/training/capabilities/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/training/capabilities/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/training/capabilities/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/capabilities/', -}; - -export const SUBMIT_A_JOB_VIA_THE_USER_INTERFACE: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/training/submit-job/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/training/submit-job/', - DEFAULT: 'https://docs.ovh.com/gb/en/ai-training/submit-job/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/submit-job/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/training/submit-job/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/training/submit-job/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/training/submit-job/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/training/submit-job/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/submit-job/', -}; - -export const MANAGING_A_CUSTOM_IMAGE: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/publiccloud/ai/training/build-use-custom-image/', - IE: 'https://docs.ovh.com/ie/en/publiccloud/ai/training/build-use-custom-image/', - DEFAULT: 'https://docs.ovh.com/gb/en/ai-training/build-use-custom-image/', - ASIA: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/build-use-custom-image/', - AU: 'https://docs.ovh.com/au/en/publiccloud/ai/training/build-use-custom-image/', - CA: 'https://docs.ovh.com/ca/en/publiccloud/ai/training/build-use-custom-image/', - SG: 'https://docs.ovh.com/sg/en/publiccloud/ai/training/build-use-custom-image/', - WE: 'https://docs.ovh.com/us/en/publiccloud/ai/training/build-use-custom-image/', - IN: 'https://docs.ovh.com/asia/en/publiccloud/ai/training/build-use-custom-image/', -}; - -export const DEPLOYING_A_CUSTOM_MODEL: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/ml-serving/deploy-serialized-models/', - IE: 'https://docs.ovh.com/ie/en/ml-serving/deploy-serialized-models/', - DEFAULT: 'https://docs.ovh.com/gb/en/ml-serving/deploy-serialized-models/', - ASIA: 'https://docs.ovh.com/asia/en/ml-serving/deploy-serialized-models/', - AU: 'https://docs.ovh.com/au/en/ml-serving/deploy-serialized-models/', - CA: 'https://docs.ovh.com/ca/en/ml-serving/deploy-serialized-models/', - SG: 'https://docs.ovh.com/sg/en/ml-serving/deploy-serialized-models/', - WE: 'https://docs.ovh.com/us/en/ml-serving/deploy-serialized-models/', - IN: 'https://docs.ovh.com/asia/en/ml-serving/deploy-serialized-models/', -}; - -export const MODELS_DEFINITION: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/ml-serving/models/', - IE: 'https://docs.ovh.com/ie/en/ml-serving/models/', - DEFAULT: 'https://docs.ovh.com/gb/en/ml-serving/models/', - ASIA: 'https://docs.ovh.com/asia/en/ml-serving/models/', - AU: 'https://docs.ovh.com/au/en/ml-serving/models/', - CA: 'https://docs.ovh.com/ca/en/ml-serving/models/', - SG: 'https://docs.ovh.com/sg/en/ml-serving/models/', - WE: 'https://docs.ovh.com/us/en/ml-serving/models/', - IN: 'https://docs.ovh.com/asia/en/ml-serving/models/', -}; - -export const EXPORTING_A_TENSORFLOW_MODEL: GuideLinks = { - GB: 'https://docs.ovh.com/gb/en/ml-serving/export-tensorflow-models/', - IE: 'https://docs.ovh.com/ie/en/ml-serving/export-tensorflow-models/', - DEFAULT: 'https://docs.ovh.com/gb/en/ml-serving/export-tensorflow-models/', - ASIA: 'https://docs.ovh.com/asia/en/ml-serving/export-tensorflow-models/', - AU: 'https://docs.ovh.com/au/en/ml-serving/export-tensorflow-models/', - CA: 'https://docs.ovh.com/ca/en/ml-serving/export-tensorflow-models/', - SG: 'https://docs.ovh.com/sg/en/ml-serving/export-tensorflow-models/', - WE: 'https://docs.ovh.com/us/en/ml-serving/export-tensorflow-models/', - IN: 'https://docs.ovh.com/asia/en/ml-serving/export-tensorflow-models/', -}; - -export const SAVINGS_PLANS: GuideLinks = { - FR: 'https://help.ovhcloud.com/csm/fr-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066650', - GB: 'https://help.ovhcloud.com/csm/en-gb-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066656', - DE: 'https://help.ovhcloud.com/csm/de-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066645', - ES: 'https://help.ovhcloud.com/csm/es-es-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066648', - IT: 'https://help.ovhcloud.com/csm/it-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066654', - PL: 'https://help.ovhcloud.com/csm/pl-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066653', - PT: 'https://help.ovhcloud.com/csm/pt-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066652', - IE: 'https://help.ovhcloud.com/csm/en-ie-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066655', - DEFAULT: - 'https://help.ovhcloud.com/csm/en-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066650', - ASIA: 'https://help.ovhcloud.com/csm/asia-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066657', - AU: 'https://help.ovhcloud.com/csm/en-au-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066658', - CA: 'https://help.ovhcloud.com/csm/en-ca-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066651', - QC: 'https://help.ovhcloud.com/csm/fr-ca-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066644', - SG: 'https://help.ovhcloud.com/csm/en-sg-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066655', - WE: 'https://us.ovhcloud.com/support/', - WS: 'https://help.ovhcloud.com/csm/es-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066649', - IN: 'https://help.ovhcloud.com/csm/en-ie-public-cloud-compute-savings-plan?id=kb_article_view&sysparm_article=KB0066655', -}; - -export const PREFIX_UNIVERSE_NAME = 'public-cloud'; - -export const DEFAULT_GUIDES = { - public_cloud_guides: { - url: PUBLIC_CLOUD_GUIDES, - key: 'all_guides', - tracking: '::guides::go_to_all_guides', - }, - first_steps_with_instances: { - url: FIRST_STEPS_WITH_INSTANCES, - key: 'first_steps_with_instances', - tracking: '::guides::go_to_instances_guide', - }, -}; - -export const GUIDES_LIST: Record> = { - storage: { - public_cloud_storage_guides: { - url: PUBLIC_CLOUD_STORAGE_GUIDES, - key: 'all_storage_guides', - tracking: '::guides::go_to_storage', - }, - first_steps_with_instances: { - url: FIRST_STEPS_WITH_INSTANCES, - key: 'first_steps_with_instances', - tracking: '::guides::go_to_instances_guide', - }, - ip_fail_over: { - url: IP_FAIL_OVER, - key: 'ip_fail_over', - tracking: '::guides::go_to_configure_a_failover_ip', - }, - user_root_and_password: { - url: USER_ROOT_AND_PASSWORD, - key: 'user_root_and_password', - tracking: '::guides::go_to_become_the_root_user_and_select_a_password', - }, - reverse_dns: { - url: REVERSE_DNS, - key: 'reverse_dns', - tracking: '::guides::go_to_configure_reverse_dns_instance', - }, - }, - volumeBackup: { - storages_volume_backup_overview: { - url: STORAGES_VOLUME_BACKUP_GUIDES, - key: 'storages_volume_backup_overview', - tracking: '::guides::go_to_storages_volume_backup_overview', - }, - public_cloud_storage_guides: { - url: PUBLIC_CLOUD_STORAGE_GUIDES, - key: 'all_storage_guides', - tracking: '::guides::go_to_storage', - }, - first_steps_with_instances: { - url: FIRST_STEPS_WITH_INSTANCES, - key: 'first_steps_with_instances', - tracking: '::guides::go_to_instances_guide', - }, - }, - objectStorage: { - public_cloud_storage_guides: { - url: PUBLIC_CLOUD_STORAGE_GUIDES, - key: 'all_storage_guides', - tracking: '::guides::go_to_storage', - }, - first_steps_with_storages: { - url: FIRST_STEPS_WITH_STORAGES, - key: 'first_steps_with_storages', - tracking: '::guides::go_to_storages_guide', - }, - }, - instances: { - ...DEFAULT_GUIDES, - ip_fail_over: { - url: IP_FAIL_OVER, - key: 'ip_fail_over', - tracking: '::guides::go_to_configure_a_failover_ip', - }, - user_root_and_password: { - url: USER_ROOT_AND_PASSWORD, - key: 'user_root_and_password', - tracking: '::guides::go_to_become_the_root_user_and_select_a_password', - }, - reverse_dns: { - url: REVERSE_DNS, - key: 'reverse_dns', - tracking: '::guides::go_to_configure_reverse_dns_instance', - }, - }, - databases: { - ...DEFAULT_GUIDES, - first_steps_with_databases: { - url: FIRST_STEPS_WITH_DATABASES, - key: 'first_steps_with_databases', - tracking: '::guides::go_to_getting_started', - }, - mongo_db_capabilities_and_limitations: { - url: MONGO_DB_CAPABILITIES_AND_LIMITATIONS, - key: 'mongo_db_capabilities_and_limitations', - tracking: '::guides::go_to_mongodb_capabilities', - }, - mysql_capabilities_and_limitations: { - url: MYSQL_CAPABILITIES_AND_LIMITATIONS, - key: 'mysql_capabilities_and_limitations', - tracking: '::guides::go_to_mysql_capabilities', - }, - }, - kubernetes: { - ...DEFAULT_GUIDES, - create_a_cluster: { - url: CREATE_A_CLUSTER, - key: 'create_a_cluster', - tracking: '::guides::go_to_creating_a_cluster', - }, - deploy_an_application: { - url: DEPLOY_AN_APPLICATION, - key: 'deploy_an_application', - tracking: '::guides::go_to_deploying_an_application', - }, - loadbalancer_kube: { - url: LOADBALANCER_KUBE, - key: 'loadbalancer_kube', - tracking: '::guides::go_to_using_lb', - }, - }, - private_registry: { - ...DEFAULT_GUIDES, - faq_managed_private_registry: { - url: FAQ_MANAGED_PRIVATE_REGISTRY, - key: 'faq_managed_private_registry', - tracking: '::guides::go_to_managed_private_registry_faq', - }, - create_a_managed_private_register: { - url: CREATE_A_MANAGED_PRIVATE_REGISTER, - key: 'create_a_managed_private_register', - tracking: '::guides::go_to_creating_a_private_registry', - }, - create_and_use_a_private_image: { - url: CREATE_AND_USE_A_PRIVATE_IMAGE, - key: 'create_and_use_a_private_image', - tracking: '::guides::go_to_creating_and_using_a_private_image', - }, - }, - ai_machine_learning: { - ...DEFAULT_GUIDES, - differences_between_ai_notebooks_ai_training_ai_apps: { - url: DIFFERENCES_BETWEEN_AI_NOTEBOOKS_AI_TRAINING_AI_APPS, - key: 'differences_between_ai_notebooks_ai_training_ai_apps', - tracking: '::guides::go_to_ai_comparative_tables', - }, - ai_apps_capabilities_and_limitations: { - url: AI_APPS_CAPABILITIES_AND_LIMITATIONS, - key: 'ai_apps_capabilities_and_limitations', - tracking: '::guides::go_to_capabilities', - }, - accessing_your_ai_apps_with_tokens: { - url: ACCESSING_YOUR_AI_APPS_WITH_TOKENS, - key: 'accessing_your_ai_apps_with_tokens', - tracking: '::guides::go_to_ai_apps_tokens', - }, - }, - data_processing: { - ...DEFAULT_GUIDES, - presentation_of_data_processing: { - url: PRESENTATION_OF_DATA_PROCESSING, - key: 'presentation_of_data_processing', - tracking: '::guides::go_to_overview', - }, - data_processing_capabilities_and_limitations: { - url: DATA_PROCESSING_CAPABILITIES_AND_LIMITATIONS, - key: 'data_processing_capabilities_and_limitations', - tracking: '::guides::go_to_capabilities', - }, - submit_a_java_scala_job: { - url: SUBMIT_A_JAVA_SCALA_JOB, - key: 'submit_a_java_scala_job', - tracking: '::guides::go_to_submit_javascala', - }, - }, - ai_notenooks: { - ...DEFAULT_GUIDES, - ai_notebooks_startup: { - url: AI_NOTEBOOKS_STARTUP, - key: 'ai_notebooks_startup', - tracking: '::guides::go_to_getting_started_cli', - }, - ai_notebooks_definition: { - url: AI_NOTEBOOKS_DEFINITION, - key: 'ai_notebooks_definition', - tracking: '::guides::go_to_definition', - }, - using_data_form_object_storage: { - url: USING_DATA_FORM_OBJECT_STORAGE, - key: 'using_data_form_object_storage', - tracking: '::guides::go_to_access_object_storage_data', - }, - }, - ai_training: { - ...DEFAULT_GUIDES, - ai_training_capabilities_and_limitations: { - url: AI_TRAINING_CAPABILITIES_AND_LIMITATIONS, - key: 'ai_training_capabilities_and_limitations', - tracking: '::guides::go_to_capabilities', - }, - submit_a_job_via_the_user_interface: { - url: SUBMIT_A_JOB_VIA_THE_USER_INTERFACE, - key: 'submit_a_job_via_the_user_interface', - tracking: '::guides::go_to_submit_job', - }, - managing_a_custom_image: { - url: MANAGING_A_CUSTOM_IMAGE, - key: 'managing_a_custom_image', - tracking: '::guides::go_to_build_use_custom_image', - }, - }, - ml_serving: { - ...DEFAULT_GUIDES, - deploying_a_custom_model: { - url: DEPLOYING_A_CUSTOM_MODEL, - key: 'deploying_a_custom_model', - tracking: '::guides::go_to_deploy_serialized_models', - }, - models_definition: { - url: MODELS_DEFINITION, - key: 'models_definition', - tracking: '::guides::go_to_models', - }, - exporting_a_tensorflow_model: { - url: EXPORTING_A_TENSORFLOW_MODEL, - key: 'exporting_a_tensorflow_model', - tracking: '::guides::go_to_export_tensorflow_models', - }, - }, - private_network: { - ...DEFAULT_GUIDES, - }, - savings_plans: { - ...DEFAULT_GUIDES, - savings_plans: { - url: SAVINGS_PLANS, - key: 'savings_plans', - tracking: '::guides::go_to_savings_plans', - }, - }, -}; diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_de_DE.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_de_DE.json deleted file mode 100644 index f851329c58ef..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_de_DE.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guides", - "pci_project_guides_header_all_guides": "Alle Anleitungen", - "pci_project_guides_header_all_storage_guides": "Alle Guides zu Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Erste Schritte mit den Instanzen", - "pci_project_guides_header_storages_volume_backup_overview": "Backup eines Volumes erstellen", - "pci_project_guides_header_ip_fail_over": "Failover-IP", - "pci_project_guides_header_user_root_and_password": "Root-Benutzer und Passwort", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Erste Schritte mit den Datenbanken (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB – Kapazitäten und Einschränkungen (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL – Kapazitäten und Einschränkungen (EN)", - "pci_project_guides_header_create_a_cluster": "Einen Cluster erstellen (EN)", - "pci_project_guides_header_deploy_an_application": "Eine Anwendung deployen (EN)", - "pci_project_guides_header_loadbalancer_kube": "Kubernetes Load Balancer", - "pci_project_guides_header_faq_managed_private_registry": "Managed Private Registry (Harbor) FAQ", - "pci_project_guides_header_create_a_managed_private_register": "Managed Private Registry erstellen (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Privates Image erstellen und verwenden (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Die Unterschiede zwischen AI Notebooks, AI Training und AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Kapazitäten und Einschränkungen (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Per Token auf Ihre KI-Anwendung zugreifen", - "pci_project_guides_header_presentation_of_data_processing": "Einführung Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing – Kapazitäten und Einschränkungen (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Java/Scala Job senden (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Erste Schritte (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definition (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Object Storage Daten verwenden", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training – Kapazitäten und Einschränkungen (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Einen Job über das Benutzerinterface senden (EN)", - "pci_project_guides_header_managing_a_custom_image": "Individuelles Image verwalten (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Benutzerdefiniertes Modell einrichten (EN)", - "pci_project_guides_header_models_definition": "Modelle – Definition (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "TensorFlow-Modell exportieren (EN)", - "pci_project_guides_header_public_cloud_guides": "Alle Anleitungen zu Public Cloud", - "pci_project_guides_header_savings_plans": "Wie funktionieren die Savings Plans?", - "pci_project_guides_header_first_steps_with_storages": "Erste Schritte mit Object Storage" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_en_GB.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_en_GB.json deleted file mode 100644 index e223baa9c8ea..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_en_GB.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guides", - "pci_project_guides_header_all_guides": "All guides", - "pci_project_guides_header_all_storage_guides": "All Object Storage guides", - "pci_project_guides_header_first_steps_with_instances": "Getting started with instances", - "pci_project_guides_header_storages_volume_backup_overview": "Create a volume backup", - "pci_project_guides_header_ip_fail_over": "Failover IP", - "pci_project_guides_header_user_root_and_password": "Root user and password", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Getting started with databases (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - Capabilities and limitations (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - Capabilities and limitations (EN)", - "pci_project_guides_header_create_a_cluster": "Creating a cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Deploying an application (EN)", - "pci_project_guides_header_loadbalancer_kube": "Kubernetes Load Balancer", - "pci_project_guides_header_faq_managed_private_registry": "Managed Private Registry FAQ (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Creating a Managed Private Registry (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Creating and using a private image (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Differences between AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Capabilities and limitations (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Accessing your AI application with tokens", - "pci_project_guides_header_presentation_of_data_processing": "Introduction to Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - Capabilities and limitations (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Submitting a Java/Scala job (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Startup (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definition (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Using Object Storage data (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - Capabilities and limitations (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Submitting a job via the user interface (EN)", - "pci_project_guides_header_managing_a_custom_image": "Managing a custom image (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Deploying a custom model (EN)", - "pci_project_guides_header_models_definition": "Models - definition (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Exporting a TensorFlow model (EN)", - "pci_project_guides_header_public_cloud_guides": "All Public Cloud guides", - "pci_project_guides_header_savings_plans": "How do Savings Plans work?", - "pci_project_guides_header_first_steps_with_storages": "Getting started with Object Storage" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_es_ES.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_es_ES.json deleted file mode 100644 index a526debfe5e1..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_es_ES.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guías", - "pci_project_guides_header_all_guides": "Todas las guías", - "pci_project_guides_header_all_storage_guides": "Todas las guías de Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Primeros pasos con instancias", - "pci_project_guides_header_storages_volume_backup_overview": "Crear una copia de seguridad de un volumen", - "pci_project_guides_header_ip_fail_over": "IP failover", - "pci_project_guides_header_user_root_and_password": "Usuario root y contraseña", - "pci_project_guides_header_reverse_dns": "Registro DNS inverso", - "pci_project_guides_header_first_steps_with_databases": "Primeros pasos con bases de datos (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB: capacidades y limitaciones (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL: capacidades y limitaciones (EN)", - "pci_project_guides_header_create_a_cluster": "Crear un cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Desplegar una app (EN)", - "pci_project_guides_header_loadbalancer_kube": "Load Balancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ sobre Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Crear un registro privado administrado (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Crear y utilizar una imagen privada (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Diferencias entre AI Notebooks, AI Training y AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy: capacidades y limitaciones (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Acceder a una app de IA con tokens", - "pci_project_guides_header_presentation_of_data_processing": "Data Processing: presentación (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing: capacidades y limitaciones (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Enviar un job Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Primeros pasos (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definición (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Utilizar los datos de Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training: capacidades y limitaciones (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Enviar un job a través de la interfaz de usuario (EN)", - "pci_project_guides_header_managing_a_custom_image": "Gestión de una imagen personalizada (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Desplegar un modelo personalizado (EN)", - "pci_project_guides_header_models_definition": "Modelos: definición (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Exportar un modelo TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Todas las guías de Public Cloud", - "pci_project_guides_header_savings_plans": "¿Cómo funcionan los Savings Plans?", - "pci_project_guides_header_first_steps_with_storages": "Primeros pasos con Object Storage" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_CA.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_CA.json deleted file mode 100644 index 7a467380a4c6..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_CA.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guides", - "pci_project_guides_header_all_guides": "Tous les guides", - "pci_project_guides_header_all_storage_guides": "Tous les guides Object Storage", - "pci_project_guides_header_first_steps_with_storages": "Premiers pas avec Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Premiers pas avec les instances", - "pci_project_guides_header_storages_volume_backup_overview": "Créer une sauvegarde d’un volume", - "pci_project_guides_header_ip_fail_over": "IP fail-over", - "pci_project_guides_header_user_root_and_password": "Utilisateur root et mot de passe", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Premiers pas avec les bases de données (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - Capacités et limitations (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - Capacités et limitations (EN)", - "pci_project_guides_header_create_a_cluster": "Créer un cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Déployer une application (EN)", - "pci_project_guides_header_loadbalancer_kube": "LoadBalancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Créer un Registre privé géré (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Créer et utiliser une image privée (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Différences entre AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Capacités et limitations (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Accéder à votre application IA avec des tokens", - "pci_project_guides_header_presentation_of_data_processing": "Présentation de Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - Capacités et limitations (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Soumettre un job Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Démarrage (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Définition (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Utiliser les données de l'Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - Capacités et limitations (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Soumettre un job via l'interface utilisateur (EN)", - "pci_project_guides_header_managing_a_custom_image": "Gestion d'une image personnalisée (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Déployer un modèle personnalisé (EN)", - "pci_project_guides_header_models_definition": "Modèles - définition (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Exporter un modèle TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Tous les guides Public Cloud", - "pci_project_guides_header_savings_plans": "Comment fonctionnent les Savings Plans ?" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_FR.json deleted file mode 100644 index 7a467380a4c6..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_fr_FR.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guides", - "pci_project_guides_header_all_guides": "Tous les guides", - "pci_project_guides_header_all_storage_guides": "Tous les guides Object Storage", - "pci_project_guides_header_first_steps_with_storages": "Premiers pas avec Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Premiers pas avec les instances", - "pci_project_guides_header_storages_volume_backup_overview": "Créer une sauvegarde d’un volume", - "pci_project_guides_header_ip_fail_over": "IP fail-over", - "pci_project_guides_header_user_root_and_password": "Utilisateur root et mot de passe", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Premiers pas avec les bases de données (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - Capacités et limitations (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - Capacités et limitations (EN)", - "pci_project_guides_header_create_a_cluster": "Créer un cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Déployer une application (EN)", - "pci_project_guides_header_loadbalancer_kube": "LoadBalancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Créer un Registre privé géré (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Créer et utiliser une image privée (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Différences entre AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Capacités et limitations (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Accéder à votre application IA avec des tokens", - "pci_project_guides_header_presentation_of_data_processing": "Présentation de Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - Capacités et limitations (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Soumettre un job Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Démarrage (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Définition (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Utiliser les données de l'Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - Capacités et limitations (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Soumettre un job via l'interface utilisateur (EN)", - "pci_project_guides_header_managing_a_custom_image": "Gestion d'une image personnalisée (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Déployer un modèle personnalisé (EN)", - "pci_project_guides_header_models_definition": "Modèles - définition (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Exporter un modèle TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Tous les guides Public Cloud", - "pci_project_guides_header_savings_plans": "Comment fonctionnent les Savings Plans ?" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_it_IT.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_it_IT.json deleted file mode 100644 index 94e90be74c1d..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_it_IT.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Guide", - "pci_project_guides_header_all_guides": "Tutte le guide", - "pci_project_guides_header_all_storage_guides": "Tutte le guide Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Iniziare a utilizzare le istanze", - "pci_project_guides_header_storages_volume_backup_overview": "Creare il backup di un volume", - "pci_project_guides_header_ip_fail_over": "IP Failover", - "pci_project_guides_header_user_root_and_password": "Utente root e password", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Iniziare a utilizzare i database (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - Funzionalità e limiti (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - Funzionalità e limiti (EN)", - "pci_project_guides_header_create_a_cluster": "Creare un cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Eseguire un'applicazione (EN)", - "pci_project_guides_header_loadbalancer_kube": "Load Balancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Creare un Registro privato gestito (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Creare e utilizzare un'immagine privata (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Differenze tra AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Capacità e limitazioni (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Accedere a un’applicazione di IA tramite token", - "pci_project_guides_header_presentation_of_data_processing": "Presentazione di Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - Funzionalità e limiti (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Inviare un job Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Primi passi (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definizione (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Utilizzare i dati dell'Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - Funzionalità e limiti (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Inviare un job tramite l'interfaccia utente (EN)", - "pci_project_guides_header_managing_a_custom_image": "Gestire un'immagine personalizzata (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Eseguire un modello personalizzato (EN)", - "pci_project_guides_header_models_definition": "Modelli - definizione (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Esportare un modello TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Tutte le guide Public Cloud", - "pci_project_guides_header_savings_plans": "Come funzionano i Savings Plan?", - "pci_project_guides_header_first_steps_with_storages": "Primi passi con Object Storage" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pl_PL.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pl_PL.json deleted file mode 100644 index 20219c26dbdd..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pl_PL.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Przewodniki", - "pci_project_guides_header_all_guides": "Wszystkie przewodniki", - "pci_project_guides_header_all_storage_guides": "Wszystkie przewodniki Object Storage", - "pci_project_guides_header_first_steps_with_instances": "Pierwsze kroki z instancjami", - "pci_project_guides_header_storages_volume_backup_overview": "Utwórz kopię zapasową wolumenu", - "pci_project_guides_header_ip_fail_over": "IP Failover", - "pci_project_guides_header_user_root_and_password": "Użytkownik root i hasło", - "pci_project_guides_header_reverse_dns": "Rewers DNS", - "pci_project_guides_header_first_steps_with_databases": "Pierwsze kroki z bazami danych (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - możliwości i ograniczenia (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - możliwości i ograniczenia (EN)", - "pci_project_guides_header_create_a_cluster": "Utwórz klaster (EN)", - "pci_project_guides_header_deploy_an_application": "Uruchom aplikację (EN)", - "pci_project_guides_header_loadbalancer_kube": "Load Balancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Utwórz zarządzany Prywatny rejestr (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Utwórz i korzystaj z prywatnego obrazu (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Różnice między AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Możliwości i ograniczenia (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Dostęp do aplikacji AI z tokenami", - "pci_project_guides_header_presentation_of_data_processing": "Prezentacja Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - możliwości i ograniczenia (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Dodaj zadanie Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Uruchomienie (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definicja (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Korzystaj z danych Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - możliwości i ograniczenia (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Dodaj zadanie za pomocą interfejsu użytkownika (EN)", - "pci_project_guides_header_managing_a_custom_image": "Zarządzanie spersonalizowanym obrazem (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Uruchom spersonalizowany model (EN)", - "pci_project_guides_header_models_definition": "Modele - definicja (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Eksportuj model TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Wszystkie przewodniki Public Cloud", - "pci_project_guides_header_savings_plans": "Jak działa program Savings Plans?", - "pci_project_guides_header_first_steps_with_storages": "Pierwsze kroki z Object Storage" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pt_PT.json b/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pt_PT.json deleted file mode 100644 index 2738690d9cfe..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/Messages_pt_PT.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "pci_project_guides_header": "Manuais", - "pci_project_guides_header_all_guides": "Todos os manuais", - "pci_project_guides_header_all_storage_guides": "Todos os guias de Armazenamento de Objetos", - "pci_project_guides_header_first_steps_with_instances": "Primeiros passos com as instâncias", - "pci_project_guides_header_storages_volume_backup_overview": "Fazer backup de um volume", - "pci_project_guides_header_ip_fail_over": "IP Failover", - "pci_project_guides_header_user_root_and_password": "Utilizador root e palavra-passe", - "pci_project_guides_header_reverse_dns": "Reverse DNS", - "pci_project_guides_header_first_steps_with_databases": "Primeiros passos com as bases de dados (EN)", - "pci_project_guides_header_mongo_db_capabilities_and_limitations": "MongoDB - Capacidades e limitações (EN)", - "pci_project_guides_header_mysql_capabilities_and_limitations": "MySQL - Capacidades e limitações (EN)", - "pci_project_guides_header_create_a_cluster": "Criar um cluster (EN)", - "pci_project_guides_header_deploy_an_application": "Implementar uma aplicação (EN)", - "pci_project_guides_header_loadbalancer_kube": "LoadBalancer Kubernetes", - "pci_project_guides_header_faq_managed_private_registry": "FAQ Managed Private Registry (Harbor)", - "pci_project_guides_header_create_a_managed_private_register": "Criar um registo privado gerido (EN)", - "pci_project_guides_header_create_and_use_a_private_image": "Criar e utilizar uma imagem privada (EN)", - "pci_project_guides_header_differences_between_ai_notebooks_ai_training_ai_apps": "Diferenças entre AI Notebooks, AI Training, AI Deploy (EN)", - "pci_project_guides_header_ai_apps_capabilities_and_limitations": "AI Deploy - Capacidades e limitações (EN)", - "pci_project_guides_header_accessing_your_ai_apps_with_tokens": "Aceder à sua aplicação IA com tokens", - "pci_project_guides_header_presentation_of_data_processing": "Apresentação do Data Processing (EN)", - "pci_project_guides_header_data_processing_capabilities_and_limitations": "Data Processing - Capacidades e limitações (EN)", - "pci_project_guides_header_submit_a_java_scala_job": "Submeter um job Java/Scala (EN)", - "pci_project_guides_header_ai_notebooks_startup": "Arranque (EN)", - "pci_project_guides_header_ai_notebooks_definition": "Definição (EN)", - "pci_project_guides_header_using_data_form_object_storage": "Utilizar os dados do Object Storage (EN)", - "pci_project_guides_header_ai_training_capabilities_and_limitations": "AI Training - Capacidades e limitações (EN)", - "pci_project_guides_header_submit_a_job_via_the_user_interface": "Submeter um job através da interface de utilizador (EN)", - "pci_project_guides_header_managing_a_custom_image": "Gestão de uma imagem personalizada (EN)", - "pci_project_guides_header_deploying_a_custom_model": "Implementar um modelo personalizado (EN)", - "pci_project_guides_header_models_definition": "Modelos - definição (EN)", - "pci_project_guides_header_exporting_a_tensorflow_model": "Exportar um modelo TensorFlow (EN)", - "pci_project_guides_header_public_cloud_guides": "Todos os manuais do Public Cloud", - "pci_project_guides_header_savings_plans": "Como funcionam os Savings Plans?", - "pci_project_guides_header_first_steps_with_storages": "Primeiros passos com Armazenamento de Objetos" -} diff --git a/packages/manager-react-components/src/components/guides-header/pci/translations/index.ts b/packages/manager-react-components/src/components/guides-header/pci/translations/index.ts deleted file mode 100644 index 20ad049208ef..000000000000 --- a/packages/manager-react-components/src/components/guides-header/pci/translations/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'pci-guides-header'); diff --git a/packages/manager-react-components/src/components/index.ts b/packages/manager-react-components/src/components/index.ts deleted file mode 100644 index e7006fe44cce..000000000000 --- a/packages/manager-react-components/src/components/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -export * from './action-banner'; -export * from './redirection-guard'; -export * from './breadcrumb/breadcrumb.component'; - -export * from './clipboard/clipboard.component'; -export * from './container'; -export * from './input'; - -export * from './content'; -export * from './navigation'; -export * from './templates'; -export * from './typography'; - -export * from './datagrid/datagrid.component'; -export * from './datagrid/text-cell.component'; -export * from './datagrid/useDatagrid'; -export * from './datagrid/useDatagridSearchParams'; -export * from './datagrid/clipboard-cell.component'; - -export * from './drawer/Drawer.component'; -export * from './drawer/DrawerCollapsible.component'; - -export * from './guides-header'; - -export * from './notifications/notifications.component'; -export * from './notifications/useNotifications'; - -export * from './filters'; - -export * from './ManagerButton/ManagerButton'; -export * from './ManagerLink/ManagerLink.component'; -export * from './ManagerText/ManagerText'; - -export * from './ServiceStateBadge/ServiceStateBadge.component'; - -export * from './pci-maintenance-banner'; -export * from './region/region.component'; -export * from './order'; - -export * from './badge'; -export * from './Modal'; -export * from './tags-list'; -export * from './tags-modal'; -export * from './tags-tile'; diff --git a/packages/manager-react-components/src/components/input/index.ts b/packages/manager-react-components/src/components/input/index.ts deleted file mode 100644 index 821be73f8153..000000000000 --- a/packages/manager-react-components/src/components/input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tiles/TilesInput.component'; diff --git a/packages/manager-react-components/src/components/input/tiles/TilesInput.component.tsx b/packages/manager-react-components/src/components/input/tiles/TilesInput.component.tsx deleted file mode 100644 index 74026a823b0f..000000000000 --- a/packages/manager-react-components/src/components/input/tiles/TilesInput.component.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import isEqual from 'lodash.isequal'; -import { TabsComponent } from '../../container/tabs/Tabs.component'; -import { - SimpleTilesInputComponent, - TSimpleProps, -} from './components/SimpleTilesInput.component'; - -type TProps = TSimpleProps & { - group?: { - by: (item: T) => G; - label: (group: G, items: T[]) => JSX.Element | string; - value?: G; - showAllTab: boolean; - onChange?: (group: G) => void; - }; -}; - -type TState = { - selectedGroup: G; - selectedStack: S; -}; - -export const TilesInputComponent = function TilesInputComponent< - T, - S = void, - G = void, ->({ - id, - items, - value, - onInput, - label, - tileClass, - stack, - group, -}: TProps): JSX.Element { - const [state, setState] = useState>({ - selectedGroup: group?.value, - selectedStack: stack?.value, - }); - - const groups = useMemo(() => { - const newGroups = new Map(); - if (group && typeof group.by === 'function') { - if (group.showAllTab) { - newGroups.set(undefined, [...items]); - } - - items.forEach((item) => { - const groupId = group.by(item); - if (!newGroups.has(groupId)) { - newGroups.set(groupId, []); - } - newGroups.get(groupId).push(item); - }); - } - - return newGroups; - }, [items, group]); - - return ( - <> - {group ? ( - - items={[...groups?.keys()]} - titleElement={(key) => group.label(key, groups.get(key))} - contentElement={(item: G) => ( - { - setState((prev) => ({ ...prev, selectedStack: s })); - if (stack?.onChange) stack?.onChange(s); - }, - } - : undefined - } - /> - )} - onChange={(g) => { - setState((prev) => ({ ...prev, selectedGroup: g })); - if (group.onChange && !isEqual(state.selectedGroup, g)) - group.onChange(g); - }} - /> - ) : ( - - id={id} - items={items} - value={value} - onInput={onInput} - label={label} - tileClass={tileClass} - stack={stack} - /> - )} - - ); -}; - -export default TilesInputComponent; diff --git a/packages/manager-react-components/src/components/input/tiles/components/SimpleTilesInput.component.tsx b/packages/manager-react-components/src/components/input/tiles/components/SimpleTilesInput.component.tsx deleted file mode 100644 index 52876617fee5..000000000000 --- a/packages/manager-react-components/src/components/input/tiles/components/SimpleTilesInput.component.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { OdsCard } from '@ovhcloud/ods-components/react'; -import { clsx } from 'clsx'; -import isEqual from 'lodash.isequal'; -import { hashCode } from '../../../../utils'; - -const uniqBy = function uniqBy(items: I[], cb: (item: I) => U): I[] { - return [ - ...items - .reduce((map: Map, item?: I) => { - if (!map.has(cb(item))) map.set(cb(item), item); - - return map; - }, new Map()) - .values(), - ]; -}; - -const stackItems = function stackItems( - items: I[], - cb: (item: I) => U, -): Map { - const stacks = new Map(); - - if (cb) { - const uniques = uniqBy(items, cb); - uniques.forEach((unique) => { - const key = cb(unique); - stacks.set(key, []); - stacks.get(key).push(...items.filter((item) => isEqual(key, cb(item)))); - }); - } else { - stacks.set(undefined, items); - } - - return stacks; -}; - -export type TSimpleProps = { - id?: (() => string) | string; - items: T[]; - value: T | null; - onInput: (value: T) => void; - label: (item: T) => JSX.Element | string; - tileClass?: { - active?: string; - inactive?: string; - }; - stack?: { - by: (item: T) => S; - label: (stack: S, items: T[]) => JSX.Element | string; - title: (stack: S, items: T[]) => JSX.Element | string; - value?: S; - onChange?: (stack: S) => void; - }; -}; - -type IState = { - stacks: Map; - selectedStack: S; - activeClass: string; - inactiveClass: string; -}; - -export const SimpleTilesInputComponent = function SimpleTilesInputComponent< - T, - S, ->({ - items, - value, - onInput, - label, - tileClass, - stack, - id, -}: TSimpleProps): JSX.Element { - const [state, setState] = useState>({ - stacks: stackItems(items, stack?.by), - selectedStack: stack?.value, - activeClass: `cursor-pointer font-bold bg-[--ods-color-blue-100] border-[--ods-color-blue-600] ${tileClass?.active}`, - inactiveClass: `cursor-pointer border-[--ods-color-blue-100] hover:bg-[--ods-color-blue-100] hover:border-[--ods-color-blue-600] ${tileClass?.inactive}`, - }); - - const set = { - selectedStack: (s: S) => { - setState((prev) => ({ ...prev, selectedStack: s })); - }, - value: (t: T) => onInput(t), - }; - - const is = { - stack: { - checked: useCallback( - (s: S) => - state.stacks?.get(s)?.length > 1 - ? isEqual(state.selectedStack, s) - : isEqual(state.stacks.get(s)[0], value), - [state.stacks, state.selectedStack, value], - ), - singleton: useCallback( - (s: S) => state.stacks.get(s)?.length === 1, - [state.stacks], - ), - }, - }; - - // Update stacks from props - useEffect(() => { - setState((prev) => ({ ...prev, stacks: stackItems(items, stack?.by) })); - }, [items, stack]); - - // Update active/inactive class from props - useEffect(() => { - if (tileClass) { - setState((prev) => ({ - ...prev, - activeClass: `cursor-pointer font-bold bg-[--ods-color-blue-100] border-[--ods-color-blue-600] ${tileClass?.active}`, - inactiveClass: `cursor-pointer border-[--ods-color-blue-100] hover:bg-[--ods-color-blue-100] hover:border-[--ods-color-blue-600] ${tileClass?.inactive}`, - })); - } - }, [tileClass]); - - // Warn parent on stack change - useEffect(() => { - if (typeof stack?.onChange === 'function') { - stack.onChange(state.selectedStack); - } - }, [state.selectedStack]); - - // Update selected stack from value - useEffect(() => { - if (stack) { - set.selectedStack(value ? stack.by(value) : undefined); - } - }, [value]); - - // Update value from selected stack - useEffect(() => { - if ( - stack && - state.stacks.get(state.selectedStack)?.length && - !isEqual(state.selectedStack, stack.by(value)) - ) { - set.value(state.stacks.get(state.selectedStack)[0]); - } - }, [state.selectedStack]); - - return ( -
-
    - {stack - ? [...state.stacks.keys()].map((key) => ( -
  • - - is.stack.singleton(key) - ? set.value(state.stacks.get(key)[0]) - : set.selectedStack(key) - } - className={`${clsx( - is.stack.checked(key) - ? state.activeClass - : state.inactiveClass, - )} w-full px-[24px] py-[16px]`} - > - {is.stack.singleton(key) - ? label(state.stacks.get(key)[0]) - : stack?.label(key, state.stacks.get(key))} - -
  • - )) - : items.map((item: T) => ( -
  • - set.value(item)} - className={`${clsx( - isEqual(value, item) - ? state.activeClass - : state.inactiveClass, - )} w-full px-[24px] py-[16px]`} - > - {label(item)} - -
  • - ))} -
- {state.selectedStack && - state.stacks.get(state.selectedStack)?.length > 1 && ( - <> -
- - {stack.title( - state.selectedStack, - state.stacks.get(state.selectedStack), - )} - -
- - - )} -
- ); -}; diff --git a/packages/manager-react-components/src/components/navigation/card/card.component.tsx b/packages/manager-react-components/src/components/navigation/card/card.component.tsx deleted file mode 100644 index d00b79a6c254..000000000000 --- a/packages/manager-react-components/src/components/navigation/card/card.component.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { OdsBadge, OdsCard } from '@ovhcloud/ods-components/react'; -import { LinkType, Links } from '../../typography'; -import './translations/translations'; - -export interface IBadge { - text: string; -} - -export interface ImageDetails { - src?: string; - alt?: string; -} - -export interface CardProps { - href: string; - isExternalHref?: boolean; - hrefLabel?: string; - img?: ImageDetails; - texts: { - title: string; - description?: string; - category: string; - }; - badges?: IBadge[]; - hoverable?: boolean; - onClick?: (event: React.MouseEvent) => void; - trackingLabel?: string; -} - -export const Card: React.FC = ({ - href, - isExternalHref, - hrefLabel, - img, - badges, - texts, - hoverable, - onClick, - trackingLabel, - ...props -}) => { - const { title, description, category } = texts; - const { t } = useTranslation('card'); - - return ( - - -
- {img?.src && ( - {img.alt} - )} -
- - {category} - - - {badges?.map((b) => ( - - ))} - -
- - - {title} - - {description && ( -

- {description} -

- )} -
- -
-
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/navigation/card/card.spec.tsx b/packages/manager-react-components/src/components/navigation/card/card.spec.tsx deleted file mode 100644 index 03531633f973..000000000000 --- a/packages/manager-react-components/src/components/navigation/card/card.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { Card, CardProps } from './card.component'; -import { render } from '../../../utils/test.provider'; - -export const defaultProps: CardProps = { - texts: { - title: 'Titre du produit', - description: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", - category: 'NAS', - }, - href: 'https://ovh.com', - img: { - alt: 'offer', - src: 'https://www.ovhcloud.com/sites/default/files/styles/offer_range_card/public/2021-06/1886_AI_Notebook1_Hero_600x400.png', - }, -}; - -const setupSpecTest = async (customProps?: Partial) => - waitFor(() => render()); - -describe('specs:Card', () => { - it('renders without error', async () => { - const screen = await setupSpecTest(); - const title = screen.getByText('Titre du produit'); - expect(title).not.toBeNull(); - }); - - describe('contents', () => { - it('should have a badges if provided', async () => { - const { container } = await setupSpecTest({ - badges: [{ text: 'Beta' }], - }); - waitFor(() => { - expect( - container.querySelector('.card-badges-section .ods-badge'), - ).toBeInTheDocument(); - }); - }); - - it('should have design system correctly set for texts', async () => { - const cardProps: Partial = { - texts: { - title: 'my title', - description: 'my decription', - category: 'my category', - }, - }; - - const { getByText, container } = await setupSpecTest(cardProps); - const titleElement = getByText(cardProps.texts.title); - expect(titleElement).toBeVisible(); - - const descElement = getByText(cardProps.texts.description); - expect(descElement).toBeVisible(); - - const catElement = getByText(cardProps.texts.category); - expect(catElement).toBeVisible(); - - expect(container.querySelector('[label="En savoir plus"]')).toBeDefined(); - }); - - it('should override href label', async () => { - const cardProps: Partial = { - texts: { - title: 'my title', - description: 'my decription', - category: 'my category', - }, - hrefLabel: 'custom label', - }; - - const { container } = await setupSpecTest(cardProps); - expect(container.querySelector('[label="custom label"]')).toBeDefined(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/navigation/card/translations/translations.ts b/packages/manager-react-components/src/components/navigation/card/translations/translations.ts deleted file mode 100644 index 312b762997f2..000000000000 --- a/packages/manager-react-components/src/components/navigation/card/translations/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'card'); diff --git a/packages/manager-react-components/src/components/navigation/index.ts b/packages/manager-react-components/src/components/navigation/index.ts deleted file mode 100644 index 70fa9f9c0324..000000000000 --- a/packages/manager-react-components/src/components/navigation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './card/card.component'; -export * from './menus'; diff --git a/packages/manager-react-components/src/components/navigation/menus/action/action.component.tsx b/packages/manager-react-components/src/components/navigation/menus/action/action.component.tsx deleted file mode 100644 index f64c161fde11..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/action/action.component.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { - ODS_BUTTON_VARIANT, - ODS_BUTTON_SIZE, - ODS_ICON_NAME, - ODS_BUTTON_COLOR, - ODS_POPOVER_POSITION, -} from '@ovhcloud/ods-components'; -import { OdsButton, OdsPopover } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; -import '../translations/translation'; - -import { ManagerButton } from '../../../ManagerButton/ManagerButton'; - -export interface ActionMenuItem { - id: number; - rel?: string; - href?: string; - download?: string; - target?: string; - onClick?: () => void; - label: string; - variant?: ODS_BUTTON_VARIANT; - iamActions?: string[]; - urn?: string; - className?: string; - isDisabled?: boolean; - isLoading?: boolean; - color?: ODS_BUTTON_COLOR; - 'data-testid'?: string; -} - -export interface ActionMenuProps { - items: ActionMenuItem[]; - isCompact?: boolean; - icon?: ODS_ICON_NAME; - variant?: ODS_BUTTON_VARIANT; - id: string; - isDisabled?: boolean; - isLoading?: boolean; - popoverPosition?: ODS_POPOVER_POSITION; - label?: string; -} - -const MenuItem = ({ - item, - isTrigger, - id, -}: { - item: Omit; - isTrigger: boolean; - id: number; -}) => { - const buttonProps = { - size: ODS_BUTTON_SIZE.sm, - variant: ODS_BUTTON_VARIANT.ghost, - displayTooltip: false, - className: 'menu-item-button w-full', - ...item, - }; - - if (item.href) { - return ( - - - - ); - } - - return !item?.iamActions || item?.iamActions?.length === 0 ? ( - - ) : ( - - ); -}; - -export const ActionMenu: React.FC = ({ - items, - isCompact, - icon, - variant = ODS_BUTTON_VARIANT.outline, - isDisabled = false, - isLoading = false, - id, - popoverPosition, - label, -}) => { - const { t } = useTranslation('buttons'); - const [isTrigger, setIsTrigger] = React.useState(false); - - return ( - <> -
- setIsTrigger(true)} - {...(!isCompact && { label: label || t('common_actions') })} - icon={ - icon || - (isCompact - ? ODS_ICON_NAME.ellipsisVertical - : ODS_ICON_NAME.chevronDown) - } - aria-label={label || t('common_actions')} - /> -
- -
- {items.map(({ id: itemId, ...item }) => ( - - ))} -
-
- - ); -}; - -export default ActionMenu; diff --git a/packages/manager-react-components/src/components/navigation/menus/action/action.scss b/packages/manager-react-components/src/components/navigation/menus/action/action.scss deleted file mode 100644 index 9b58b24a805b..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/action/action.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Customize -.menu-item-button { - &::part(button) { - width: 100%; - justify-content: left; - height: 32px; - border-radius: 0; - } -} diff --git a/packages/manager-react-components/src/components/navigation/menus/action/action.spec.tsx b/packages/manager-react-components/src/components/navigation/menus/action/action.spec.tsx deleted file mode 100644 index 1cec01031756..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/action/action.spec.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { waitFor, screen, fireEvent } from '@testing-library/react'; -import { ODS_ICON_NAME, ODS_POPOVER_POSITION } from '@ovhcloud/ods-components'; -import { ActionMenu, ActionMenuProps } from './action.component'; -import { render } from '../../../../utils/test.provider'; -import { useAuthorizationIam } from '../../../../hooks/iam'; -import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; - -vitest.mock('../../../../hooks/iam'); - -const actionItems: ActionMenuProps = { - id: 'action-menu-test-id', - items: [ - { - id: 1, - onClick: () => window.open('/'), - label: 'Action 1', - urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', - iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], - }, - { - id: 2, - onClick: () => window.open('/'), - label: 'Action 2', - urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', - iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], - }, - ], -}; - -const setupSpecTest = async (customProps?: Partial) => - waitFor(() => render()); - -const mockedHook = - useAuthorizationIam as unknown as jest.Mock; - -describe('ActionMenu', () => { - it('renders menu actions correctly', async () => { - mockedHook.mockReturnValue({ - isAuthorized: true, - isLoading: true, - isFetched: true, - }); - await setupSpecTest(); - - const actionMenuIcon = screen.getByTestId( - 'navigation-action-trigger-action', - ); - fireEvent.click(actionMenuIcon); - - // Wait for the button text to update - await waitFor(() => { - const action1 = screen.getAllByTestId('manager-button')[0]; - const action2 = screen.getAllByTestId('manager-button')[1]; - expect(action1).toBeInTheDocument(); - expect(action2).toBeInTheDocument(); - expect(actionMenuIcon.getAttribute('icon')).toBe('chevron-down'); - }); - }); - - it('renders compact menu with classic ellipsis correctly', async () => { - await setupSpecTest({ isCompact: true }); - const actionMenuIcon = screen.getByTestId( - 'navigation-action-trigger-action', - ); - expect(actionMenuIcon.getAttribute('icon')).toBe('ellipsis-vertical'); - }); - - it('renders compact menu with custom icon menu correctly', async () => { - await setupSpecTest({ - icon: ODS_ICON_NAME.ellipsisHorizontal, - }); - const actionMenuIcon = screen.getByTestId( - 'navigation-action-trigger-action', - ); - expect(actionMenuIcon.getAttribute('icon')).toBe('ellipsis-horizontal'); - }); - - it('renders compact menu with popover position bottom-end', async () => { - await setupSpecTest({ - popoverPosition: ODS_POPOVER_POSITION.bottomEnd, - }); - const popover = screen.getByTestId( - 'navigation-action-trigger-action-popover', - ); - expect(popover.getAttribute('position')).toBe('bottom-end'); - }); -}); diff --git a/packages/manager-react-components/src/components/navigation/menus/changelog/changelog.component.tsx b/packages/manager-react-components/src/components/navigation/menus/changelog/changelog.component.tsx deleted file mode 100644 index 21dda0b1ece4..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/changelog/changelog.component.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_POPOVER_POSITION, -} from '@ovhcloud/ods-components'; -import { OdsPopover, OdsButton } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; -import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; -import { Links, LinkType } from '../../../typography'; -import '../translations/translation'; - -export interface ChangelogLinks { - changelog: string; - roadmap: string; - 'feature-request': string; -} - -export interface ChangelogButtonProps { - links: ChangelogLinks; - chapters?: string[]; - prefixes?: string[]; -} - -export const CHANGELOG_PREFIXES = ['tile-changelog-roadmap', 'external-link']; -const GO_TO = (link: string) => `go-to-${link}`; - -export const ChangelogButton: React.FC = ({ - links, - chapters = [], - prefixes, -}) => { - const { t } = useTranslation('buttons'); - const { trackClick } = useOvhTracking(); - return ( - <> - - - - {Object.entries(links).map(([key, value]) => ( -
- - trackClick({ - actionType: 'navigation', - actions: [ - ...chapters, - ...(prefixes || CHANGELOG_PREFIXES), - GO_TO(key), - ], - }) - } - /> -
- ))} -
- - ); -}; - -export default ChangelogButton; diff --git a/packages/manager-react-components/src/components/navigation/menus/guide/guide.component.tsx b/packages/manager-react-components/src/components/navigation/menus/guide/guide.component.tsx deleted file mode 100644 index b4ac45c18026..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/guide/guide.component.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { - ODS_BUTTON_VARIANT, - ODS_BUTTON_SIZE, - ODS_ICON_NAME, - ODS_POPOVER_POSITION, -} from '@ovhcloud/ods-components'; -import { OdsPopover, OdsButton } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; -import { Links, LinksProps, LinkType } from '../../../typography'; -import '../translations/translation'; - -export interface GuideItem extends Omit { - id: number; - href: string; - download?: string; - target?: string; - rel?: string; - label: string; - onClick?: () => void; -} - -export interface GuideButtonProps { - items: GuideItem[]; - isLoading?: boolean; -} - -/** - * @deprecated Use `GuideMenu` component from MRC V3 instead. - */ -export const GuideButton: React.FC = ({ - isLoading, - items, -}) => { - const { t } = useTranslation('buttons'); - return ( - <> - - - -
- {items.map(({ id, onClick, ...rest }) => ( - - ))} -
-
- - ); -}; - -export default GuideButton; diff --git a/packages/manager-react-components/src/components/navigation/menus/index.ts b/packages/manager-react-components/src/components/navigation/menus/index.ts deleted file mode 100644 index b40aeec9069d..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './action/action.component'; -export * from './guide/guide.component'; -export * from './changelog/changelog.component'; diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_de_DE.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_de_DE.json deleted file mode 100644 index d2adc743b520..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_de_DE.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Aktionen", - "user_account_guides_header": "Anleitungen", - "mrc_changelog_header": "Roadmap und Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature Request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_en_GB.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_en_GB.json deleted file mode 100644 index 26322640c499..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_en_GB.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Actions", - "user_account_guides_header": "Guides", - "mrc_changelog_header": "Roadmap & Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_es_ES.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_es_ES.json deleted file mode 100644 index d025515b6baf..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_es_ES.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Acciones", - "user_account_guides_header": "Guías", - "mrc_changelog_header": "Roadmap & Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature Request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_CA.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_CA.json deleted file mode 100644 index 9863a6f4cb51..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_CA.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Actions", - "user_account_guides_header": "Guides", - "mrc_changelog_header": "Roadmap & Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_FR.json deleted file mode 100644 index 9863a6f4cb51..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_fr_FR.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Actions", - "user_account_guides_header": "Guides", - "mrc_changelog_header": "Roadmap & Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_it_IT.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_it_IT.json deleted file mode 100644 index ddf610d48868..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_it_IT.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Azioni", - "user_account_guides_header": "Guide", - "mrc_changelog_header": "Roadmap e Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature Request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pl_PL.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pl_PL.json deleted file mode 100644 index 043c1f65573b..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pl_PL.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Operacje", - "user_account_guides_header": "Przewodniki", - "mrc_changelog_header": "Roadmap & changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Propozycja wdrożenia nowej funkcji" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pt_PT.json b/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pt_PT.json deleted file mode 100644 index 7c76bf441fe8..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/Messages_pt_PT.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "common_actions": "Ações", - "user_account_guides_header": "Manuais", - "mrc_changelog_header": "Roadmap & Changelog", - "mrc_changelog_roadmap": "Roadmap", - "mrc_changelog_changelog": "Changelog", - "mrc_changelog_feature-request": "Feature request" -} diff --git a/packages/manager-react-components/src/components/navigation/menus/translations/translation.ts b/packages/manager-react-components/src/components/navigation/menus/translations/translation.ts deleted file mode 100644 index 3b02267a6fe1..000000000000 --- a/packages/manager-react-components/src/components/navigation/menus/translations/translation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'buttons'); diff --git a/packages/manager-react-components/src/components/notifications/notifications.component.tsx b/packages/manager-react-components/src/components/notifications/notifications.component.tsx deleted file mode 100644 index f58a02971c62..000000000000 --- a/packages/manager-react-components/src/components/notifications/notifications.component.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useEffect, useState, FC } from 'react'; -import { useLocation } from 'react-router-dom'; -import { OdsNotification } from './ods-notification'; -import { useNotifications } from './useNotifications'; - -interface NotificationProps { - /** Clear notifications once they have been displayed (on location changes) */ - clearAfterRead?: boolean; -} - -/** - * This component display the list of notifications. It acts - * as a "flash" component because by default once the notifications have been - * shown they are cleared. It means that you can use this component on multiple - * pages, switching page won't display notifications twice. - * - * It replicates the current behavior of public cloud notifications for - * actions (success / errors / etc) - */ -export const Notifications: FC = ({ - clearAfterRead = true, -}) => { - const location = useLocation(); - const [originLocation] = useState(location); - const { notifications, clearNotifications } = useNotifications(); - - useEffect(() => { - if (clearAfterRead && originLocation.pathname !== location.pathname) - clearNotifications(); - }, [clearAfterRead, location.pathname]); - - return ( - <> - {notifications.map((notification) => ( - - ))} - - ); -}; - -export default Notifications; diff --git a/packages/manager-react-components/src/components/notifications/notifications.spec.tsx b/packages/manager-react-components/src/components/notifications/notifications.spec.tsx deleted file mode 100644 index c199457feca9..000000000000 --- a/packages/manager-react-components/src/components/notifications/notifications.spec.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { vitest } from 'vitest'; -import React from 'react'; -import { act, render, renderHook } from '@testing-library/react'; -import { - useNotifications, - NotificationType, - NOTIFICATION_MINIMAL_DISPLAY_TIME, -} from './useNotifications'; -import { Notifications } from './notifications.component'; - -vitest.useFakeTimers(); - -vitest.mock('react-router-dom', async () => ({ - ...(await vitest.importActual('react-router-dom')), - useLocation: () => ({ - pathname: '/foo', - }), -})); - -describe('notifications component', () => { - it('should list notifications', async () => { - let { container } = render(); - const { result } = renderHook(() => useNotifications()); - act(() => { - result.current.addNotification( - 'Notification-1', - NotificationType.Success, - ); - result.current.addNotification( - 'Notification-2', - NotificationType.Warning, - ); - }); - container = render().container; - expect(container.children.length).toBe(2); - }); - - it('should not clear unseen notifications', async () => { - let { container } = render(); - const { result } = renderHook(() => useNotifications()); - act(() => { - result.current.clearNotifications(); - }); - container = render().container; - expect(container.children.length).toBe(2); - }); - - it('should clear notifications', async () => { - let { container } = render(); - expect(container.children.length).not.toBe(0); - const { result } = renderHook(() => useNotifications()); - act(() => { - vitest.advanceTimersByTime(NOTIFICATION_MINIMAL_DISPLAY_TIME + 1); - result.current.clearNotifications(); - }); - container = render().container; - expect(container.children.length).toBe(0); - }); -}); diff --git a/packages/manager-react-components/src/components/notifications/ods-notification.tsx b/packages/manager-react-components/src/components/notifications/ods-notification.tsx deleted file mode 100644 index 2b441555c8c7..000000000000 --- a/packages/manager-react-components/src/components/notifications/ods-notification.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { OdsMessage } from '@ovhcloud/ods-components/react'; -import { ODS_MESSAGE_COLOR } from '@ovhcloud/ods-components'; -import { - Notification, - NotificationType, - useNotifications, -} from './useNotifications'; - -type OdsNotificationProps = { - notification: Notification; -}; - -const getOdsMessageColor = (type: NotificationType) => { - switch (type) { - case NotificationType.Success: - return ODS_MESSAGE_COLOR.success; - case NotificationType.Error: - return ODS_MESSAGE_COLOR.danger; - case NotificationType.Warning: - return ODS_MESSAGE_COLOR.warning; - case NotificationType.Info: - return ODS_MESSAGE_COLOR.information; - default: - return ODS_MESSAGE_COLOR.information; - } -}; - -export const OdsNotification: React.FC = ({ - notification, -}) => { - const { clearNotification } = useNotifications(); - return ( - clearNotification(notification.uid)} - > - {notification.content} - - ); -}; - -export default OdsNotification; diff --git a/packages/manager-react-components/src/components/notifications/useNotifications.ts b/packages/manager-react-components/src/components/notifications/useNotifications.ts deleted file mode 100644 index c8b99387e609..000000000000 --- a/packages/manager-react-components/src/components/notifications/useNotifications.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ReactNode } from 'react'; -import { create } from 'zustand'; - -export enum NotificationType { - Success = 'success', - Error = 'error', - Info = 'info', - Warning = 'warning', -} - -export interface Notification { - /** unique notification identifier */ - uid: number; - content: ReactNode; - type: NotificationType; - dismissable?: boolean; - creationTimestamp?: number; -} - -export interface NotificationState { - uid: number; - notifications: Notification[]; - addNotification: ( - content: ReactNode, - type: NotificationType, - dismissable?: boolean, - ) => void; - addSuccess: (content: ReactNode, dismissable?: boolean) => void; - addError: (content: ReactNode, dismissable?: boolean) => void; - addWarning: (content: ReactNode, dismissable?: boolean) => void; - addInfo: (content: ReactNode, dismissable?: boolean) => void; - clearNotification: (uid: number) => void; - clearNotifications: () => void; -} - -export const NOTIFICATION_MINIMAL_DISPLAY_TIME = 1000; - -export const useNotifications = create((set, get) => ({ - uid: 0, - notifications: [], - addNotification: ( - content: ReactNode, - type: NotificationType, - dismissable = false, - ) => - set((state) => ({ - uid: state.uid + 1, - notifications: [ - ...state.notifications, - { - uid: state.uid, - content, - type, - dismissable, - creationTimestamp: Date.now(), - }, - ], - })), - addSuccess: (content: ReactNode, dismissable = false) => - get().addNotification(content, NotificationType.Success, dismissable), - addError: (content: ReactNode, dismissable = false) => - get().addNotification(content, NotificationType.Error, dismissable), - addWarning: (content: ReactNode, dismissable = false) => - get().addNotification(content, NotificationType.Warning, dismissable), - addInfo: (content: ReactNode, dismissable = false) => - get().addNotification(content, NotificationType.Info, dismissable), - clearNotification: (toRemoveUid: number) => - set((state) => ({ - notifications: state.notifications.filter( - ({ uid }) => uid !== toRemoveUid, - ), - })), - clearNotifications: () => - set((state) => ({ - notifications: state.notifications.filter( - (notification) => - Date.now() - notification.creationTimestamp < - NOTIFICATION_MINIMAL_DISPLAY_TIME, - ), - })), -})); - -export default useNotifications; diff --git a/packages/manager-react-components/src/components/order/Order.component.spec.tsx b/packages/manager-react-components/src/components/order/Order.component.spec.tsx deleted file mode 100644 index 408081e18a37..000000000000 --- a/packages/manager-react-components/src/components/order/Order.component.spec.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { fireEvent } from '@testing-library/react'; -import { vi, vitest } from 'vitest'; -import { Order } from './Order.component'; -import { render } from '../../utils/test.provider'; - -describe(' tests suite', () => { - // Mock global window.open - vi.stubGlobal('open', vi.fn()); - - const onCancelSpy = vi.fn(); - const onValidateSpy = vi.fn(); - const onFinishSpy = vi.fn(); - const onClickLinkSpy = vi.fn(); - const orderLink = 'https://order-link'; - - afterEach(() => { - vitest.resetAllMocks(); - }); - - const renderComponent = ( - isValid: boolean, - link: string, - productName: string, - ) => - render( - - -

Order steps

-
- -
, - ); - - it.each([{ valid: true }, { valid: false }])( - 'when order configuration validity is $valid confirm button disabled attribute should be $valid', - ({ valid }) => { - const { getByTestId, getByText } = renderComponent(valid, null, null); - - expect(getByText('Order steps')).toBeVisible(); - - const orderButton = getByTestId('cta-order-configuration-order'); - expect(orderButton).toHaveAttribute('label', 'Commander'); - expect(orderButton).toHaveAttribute('is-disabled', `${!valid}`); - }, - ); - - it('confirm button should be enabled and clickable when order configuration is valid', () => { - const { getByTestId } = renderComponent(true, null, null); - - fireEvent.click(getByTestId('cta-order-configuration-order')); - - expect(onValidateSpy).toHaveBeenCalled(); - }); - - it('should cancel order configuration when cancel button is clicked', () => { - const { getByTestId } = renderComponent(false, null, null); - - fireEvent.click(getByTestId('cta-order-configuration-cancel')); - - expect(onCancelSpy).toHaveBeenCalled(); - }); - - it('should open order link and display order summary when order configuration is confirmed ', () => { - vi.spyOn(window, 'open'); - const { getByTestId, queryByText } = renderComponent(true, orderLink, null); - - fireEvent.click(getByTestId('cta-order-configuration-order')); - - // order configuration is hidden - expect(queryByText('Order steps')).not.toBeInTheDocument(); - - expect(getByTestId('order-summary-title')).toBeVisible(); - expect(getByTestId('order-summary-link')).toBeVisible(); - - expect(window.open).toHaveBeenCalledTimes(1); - expect(window.open).toHaveBeenCalledWith( - orderLink, - '_blank', - 'noopener,noreferrer', - ); - }); - - it('should open order link when order link is clicked', () => { - vi.spyOn(window, 'open'); - const { getByTestId } = renderComponent(true, orderLink, null); - - fireEvent.click(getByTestId('cta-order-configuration-order')); - fireEvent.click(getByTestId('order-summary-link')); - - expect(onClickLinkSpy).toHaveBeenCalled(); - }); - - it('should close order summary when finish button is clicked', () => { - vi.spyOn(window, 'open'); - const { getByTestId } = renderComponent(true, orderLink, null); - - fireEvent.click(getByTestId('cta-order-configuration-order')); - fireEvent.click(getByTestId('cta-order-summary-finish')); - - expect(onFinishSpy).toHaveBeenCalled(); - expect(getByTestId('cta-order-configuration-order')).toBeVisible(); - }); - - it.each([{ productName: '' }, { productName: 'OVHcloud product' }])( - 'should display given product name with value $productName', - ({ productName }) => { - vi.spyOn(window, 'open'); - const { getByTestId, getByText } = renderComponent( - true, - orderLink, - productName, - ); - - fireEvent.click(getByTestId('cta-order-configuration-order')); - fireEvent.click(getByTestId('order-summary-link')); - - const product = productName || 'service'; - expect(getByText(`Commande de votre ${product} initiée`)).toBeVisible(); - }, - ); -}); diff --git a/packages/manager-react-components/src/components/order/Order.component.tsx b/packages/manager-react-components/src/components/order/Order.component.tsx deleted file mode 100644 index dfec56b2575e..000000000000 --- a/packages/manager-react-components/src/components/order/Order.component.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { PropsWithChildren } from 'react'; -import { OrderContextProvider } from './Order.context'; -import { OrderConfiguration } from './OrderConfiguration.component'; -import { OrderSummary } from './OrderSummary.component'; -import './translations'; - -export const Order = ({ children }: PropsWithChildren) => { - return {children}; -}; - -Order.Configuration = OrderConfiguration; -Order.Summary = OrderSummary; diff --git a/packages/manager-react-components/src/components/order/Order.context.tsx b/packages/manager-react-components/src/components/order/Order.context.tsx deleted file mode 100644 index 05b47a00143a..000000000000 --- a/packages/manager-react-components/src/components/order/Order.context.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { createContext, useContext, useMemo, useState } from 'react'; - -export type TOrderContext = { - setIsOrderInitialized: (isOrderInitialized: boolean) => void; - isOrderInitialized: boolean; -}; - -const OrderContext = createContext({} as TOrderContext); - -export const OrderContextProvider = ({ children }: React.PropsWithChildren) => { - const [isOrderInitialized, setIsOrderInitialized] = useState(false); - - const context = useMemo( - () => ({ - isOrderInitialized, - setIsOrderInitialized, - }), - [isOrderInitialized], - ); - - return ( - {children} - ); -}; - -export const useOrderContext = (): TOrderContext => { - const context = useContext(OrderContext); - if (context === undefined) { - throw new Error('Order-related components must be used within '); - } - return context; -}; diff --git a/packages/manager-react-components/src/components/order/OrderConfiguration.component.tsx b/packages/manager-react-components/src/components/order/OrderConfiguration.component.tsx deleted file mode 100644 index d7e4a03b1cf2..000000000000 --- a/packages/manager-react-components/src/components/order/OrderConfiguration.component.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - ODS_BUTTON_COLOR, - ODS_BUTTON_ICON_ALIGNMENT, - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, -} from '@ovhcloud/ods-components'; -import { OdsButton } from '@ovhcloud/ods-components/react'; -import React, { ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useOrderContext } from './Order.context'; - -export type TOrderConfiguration = { - children: ReactNode; - onCancel: () => void; - onConfirm: () => void; - isValid: boolean; -}; - -export const OrderConfiguration: React.FC = ({ - children, - onCancel, - onConfirm, - isValid, -}: TOrderConfiguration): JSX.Element => { - const { isOrderInitialized, setIsOrderInitialized } = useOrderContext(); - const { t } = useTranslation('order'); - - if (isOrderInitialized) { - return <>; - } - - return ( - <> - {children} -
- - { - onConfirm(); - setIsOrderInitialized(true); - }} - icon={ODS_ICON_NAME.externalLink} - iconAlignment={ODS_BUTTON_ICON_ALIGNMENT.left} - label={t('order_configuration_order')} - data-testid="cta-order-configuration-order" - /> -
- - ); -}; diff --git a/packages/manager-react-components/src/components/order/OrderSummary.component.tsx b/packages/manager-react-components/src/components/order/OrderSummary.component.tsx deleted file mode 100644 index c4b21084fe37..000000000000 --- a/packages/manager-react-components/src/components/order/OrderSummary.component.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { - ODS_BUTTON_COLOR, - ODS_BUTTON_SIZE, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { OdsButton, OdsText } from '@ovhcloud/ods-components/react'; -import { Trans, useTranslation } from 'react-i18next'; -import React, { useEffect } from 'react'; -import { useOrderContext } from './Order.context'; -import { Links, LinkType } from '../typography'; - -export type TOrderSummary = { - onFinish: () => void; - onClickLink?: () => void; - orderLink: string; - productName?: string; -}; - -export const OrderSummary: React.FC = ({ - onFinish, - onClickLink, - orderLink, - productName, -}: TOrderSummary): JSX.Element => { - const { t } = useTranslation('order'); - const { isOrderInitialized, setIsOrderInitialized } = useOrderContext(); - - useEffect(() => { - if (orderLink && isOrderInitialized) { - window.open(orderLink, '_blank', 'noopener,noreferrer'); - } - }, [orderLink, isOrderInitialized]); - - if (!isOrderInitialized) { - return <>; - } - - // set default label if no product name provided - const product = productName || t('order_summary_product_default_label'); - - return ( -
-
- - {t('order_summary_order_initiated_title', { product })} - - - - ), - }} - /> - - - {t('order_summary_order_initiated_info', { product })} - -
- { - onFinish(); - setIsOrderInitialized(false); - }} - label={t('order_summary_finish')} - /> -
- ); -}; diff --git a/packages/manager-react-components/src/components/order/index.ts b/packages/manager-react-components/src/components/order/index.ts deleted file mode 100644 index d86520124e6f..000000000000 --- a/packages/manager-react-components/src/components/order/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Order.component'; -export { useOrderContext } from './Order.context'; diff --git a/packages/manager-react-components/src/components/order/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/order/translations/Messages_fr_FR.json deleted file mode 100644 index 3e2e69d9b9e2..000000000000 --- a/packages/manager-react-components/src/components/order/translations/Messages_fr_FR.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "order_configuration_cancel": "Annuler", - "order_configuration_order": "Commander", - "order_summary_finish": "Terminer", - "order_summary_product_default_label": "service", - "order_summary_order_initiated_title": "Commande de votre {{product}} initiée", - "order_summary_order_initiated_subtitle": "Si vous n'avez pas pu finaliser votre commande, merci de la compléter en cliquant sur le lien suivant", - "order_summary_order_initiated_info": "Nous vous informerons de la disponibilité de votre {{product}} par e-mail." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.spec.tsx b/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.spec.tsx deleted file mode 100644 index dd85ca7a2043..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.spec.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { vitest } from 'vitest'; -import React, { screen } from '@testing-library/react'; -import { PciMaintenanceBanner } from './PciMaintenanceBanner.component'; -import { render } from '../../utils/test.provider'; - -vitest.mock('react-i18next', async () => { - const original = await vitest.importActual('react-i18next'); - return { - ...original, - useTranslation: () => ({ - t: (translationKey: string) => translationKey, - i18n: { - changeLanguage: () => new Promise(() => {}), - }, - }), - }; -}); - -describe('PciMaintenanceBanner', () => { - it('should display maintenance url', () => { - const url = 'www.ovhcloud.com'; - render(); - const link = screen.getByTestId('pci-maintenance-banner-link'); - expect(link).toHaveAttribute('href', url); - expect(link).toHaveAttribute('target', '_blank'); - }); - - it('should display project name', () => { - const url = 'www.ovhcloud.com'; - const name = 'foobar'; - render(); - expect( - screen.getByText('pci_projects_maintenance_banner_info_project_page'), - ).toBeDefined(); - }); - - it('should display product name', () => { - const url = 'www.ovhcloud.com'; - const name = 'hello'; - render(); - expect( - screen.getByText('pci_projects_maintenance_banner_info_list_page'), - ).toBeDefined(); - }); - - it('should display service name', () => { - const url = 'www.ovhcloud.com'; - const name = 'world'; - render(); - expect( - screen.getByText('pci_projects_maintenance_banner_info_product_page'), - ).toBeDefined(); - }); -}); diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.tsx b/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.tsx deleted file mode 100644 index ad22a2b1996b..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/PciMaintenanceBanner.component.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { ODS_MESSAGE_COLOR } from '@ovhcloud/ods-components'; -import { OdsLink, OdsMessage } from '@ovhcloud/ods-components/react'; -import { useTranslation } from 'react-i18next'; - -import './translations'; - -interface PciMaintenanceProps { - productName?: string; - projectName?: string; - serviceName?: string; - maintenanceURL: string; -} - -/** - * @deprecated PciMaintenanceBanner will be moved in @ovh-ux/manager-pci-common v3 - */ -export function PciMaintenanceBanner({ - productName, - projectName, - serviceName, - maintenanceURL, -}: Readonly) { - const { t } = useTranslation('pci-maintenance-banner'); - - return ( - - {projectName && ( - - )} - {productName && ( - ${productName}`, - }), - }} - /> - )} - {serviceName && ( - - )} - - - - - ); -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/index.ts b/packages/manager-react-components/src/components/pci-maintenance-banner/index.ts deleted file mode 100644 index 0df37569b0d5..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PciMaintenanceBanner } from './PciMaintenanceBanner.component'; diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_de_DE.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_de_DE.json deleted file mode 100644 index bdd3343c0427..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_de_DE.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Wartungsarbeiten: Dies betrifft mindestens einen der Dienste, die mit Ihrem Projekt {{projectName}} verbundenen sind.", - "pci_projects_maintenance_banner_info_list_page": "Wartungsarbeiten: Mindestens eine Ihrer Dienste {{productName}} ist betroffen.", - "pci_projects_maintenance_banner_info_product_page": "Wartungsarbeiten: Ihr Dienst {{productServiceName}} ist gerade von Wartungsarbeiten betroffen.", - "pci_projects_maintenance_banner_info_link": "Mehr erfahren." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_en_GB.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_en_GB.json deleted file mode 100644 index 6c1775d1054c..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_en_GB.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Maintenance: At least one of the services associated with your {{projectName}} project is affected.", - "pci_projects_maintenance_banner_info_list_page": "Maintenance: At least one of your {{productName}} services is affected.", - "pci_projects_maintenance_banner_info_product_page": "Maintenance: your {{productServiceName}} service is affected by maintenance.", - "pci_projects_maintenance_banner_info_link": "Find out more." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_es_ES.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_es_ES.json deleted file mode 100644 index 7cdd732ed83c..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_es_ES.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Mantenimiento: al menos uno de los servicios asociados al proyecto {{projectName}} está afectado.", - "pci_projects_maintenance_banner_info_list_page": "Mantenimiento: al menos uno de sus servicios {{productName}} está afectado.", - "pci_projects_maintenance_banner_info_product_page": "Mantenimiento: el servicio {{productServiceName}} está afectado por una operación de mantenimiento.", - "pci_projects_maintenance_banner_info_link": "Más información." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_CA.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_CA.json deleted file mode 100644 index 3d20918cf96e..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_CA.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Maintenance : au moins un des services associé à votre projet {{projectName}} est impacté.", - "pci_projects_maintenance_banner_info_list_page": "Maintenance : au moins un de vos services {{productName}} est impacté.", - "pci_projects_maintenance_banner_info_product_page": "Maintenance : votre service {{productServiceName}} est impacté par une maintenance.", - "pci_projects_maintenance_banner_info_link": "En savoir plus." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_FR.json deleted file mode 100644 index 3d20918cf96e..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_fr_FR.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Maintenance : au moins un des services associé à votre projet {{projectName}} est impacté.", - "pci_projects_maintenance_banner_info_list_page": "Maintenance : au moins un de vos services {{productName}} est impacté.", - "pci_projects_maintenance_banner_info_product_page": "Maintenance : votre service {{productServiceName}} est impacté par une maintenance.", - "pci_projects_maintenance_banner_info_link": "En savoir plus." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_it_IT.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_it_IT.json deleted file mode 100644 index 57ea2c869879..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_it_IT.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Manutenzione: l’operazione ha impatto su almeno uno dei servizi associati al progetto {{projectName}}.", - "pci_projects_maintenance_banner_info_list_page": "Manutenzione: l’operazione ha impatto su almeno uno dei servizi {{productName}}.", - "pci_projects_maintenance_banner_info_product_page": "Manutenzione: il servizio {{productServiceName}} è interessato da un intervento di manutenzione.", - "pci_projects_maintenance_banner_info_link": "Scopri di più" -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pl_PL.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pl_PL.json deleted file mode 100644 index 65cc5ce767f2..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pl_PL.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Utrzymanie: dotyczy co najmniej jednej z usług przypisanych do Twojego projektu {{projectName}}.", - "pci_projects_maintenance_banner_info_list_page": "Konserwacja: dotyczy co najmniej jednej z Twoich usług {{productName}}.", - "pci_projects_maintenance_banner_info_product_page": "Konserwacja: dotyczy Twojej usługi {{productServiceName}}.", - "pci_projects_maintenance_banner_info_link": "Dowiedz się więcej." -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pt_PT.json b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pt_PT.json deleted file mode 100644 index fdc728315a40..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/Messages_pt_PT.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pci_projects_maintenance_banner_info_project_page": "Manutenção: pelo menos um dos serviços associados ao seu projeto {{projectName}} foi afetado.", - "pci_projects_maintenance_banner_info_list_page": "Manutenção: pelo menos um dos seus serviços {{productName}} foi afetado.", - "pci_projects_maintenance_banner_info_product_page": "Manutenção: o seu serviço {{productServiceName}} foi afetado por uma manutenção.", - "pci_projects_maintenance_banner_info_link": "Saber mais" -} diff --git a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/index.ts b/packages/manager-react-components/src/components/pci-maintenance-banner/translations/index.ts deleted file mode 100644 index 0044556de89f..000000000000 --- a/packages/manager-react-components/src/components/pci-maintenance-banner/translations/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'pci-maintenance-banner'); diff --git a/packages/manager-react-components/src/components/redirection-guard/index.ts b/packages/manager-react-components/src/components/redirection-guard/index.ts deleted file mode 100644 index c7a708a9207d..000000000000 --- a/packages/manager-react-components/src/components/redirection-guard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RedirectionGuard } from './redirection-guard.component'; diff --git a/packages/manager-react-components/src/components/redirection-guard/redirection-guard.component.tsx b/packages/manager-react-components/src/components/redirection-guard/redirection-guard.component.tsx deleted file mode 100644 index 4c44e82275c0..000000000000 --- a/packages/manager-react-components/src/components/redirection-guard/redirection-guard.component.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; -import { OdsSpinner } from '@ovhcloud/ods-components/react'; -import { ODS_SPINNER_SIZE } from '@ovhcloud/ods-components'; - -export type RedirectionGuardProps = { - children: React.ReactNode; - condition: boolean; - isLoading: boolean; - route: string; - isError?: boolean; - errorComponent?: React.ReactNode; -}; - -export function RedirectionGuard({ - route, - condition, - isLoading, - children, - isError, - errorComponent, -}: RedirectionGuardProps): JSX.Element { - if (isLoading) { - return ( - - ); - } - - if (isError && errorComponent) { - return <>{errorComponent}; - } - - return condition ? : <>{children}; -} diff --git a/packages/manager-react-components/src/components/redirection-guard/redirection-guard.spec.tsx b/packages/manager-react-components/src/components/redirection-guard/redirection-guard.spec.tsx deleted file mode 100644 index 6093fabf6989..000000000000 --- a/packages/manager-react-components/src/components/redirection-guard/redirection-guard.spec.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { vi } from 'vitest'; -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { Navigate } from 'react-router-dom'; -import { RedirectionGuard } from './redirection-guard.component'; -import '@testing-library/jest-dom'; - -vi.mock('react-router-dom', () => ({ - Navigate: vi.fn(() => null), -})); - -describe('RedirectionGuard', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should render children when condition is false', () => { - render( - -
Test Child
-
, - ); - - expect(screen.getByText('Test Child')).toBeInTheDocument(); - expect(Navigate).not.toHaveBeenCalled(); - }); - - it('should navigate when condition is true', () => { - render( - -
Test Child
-
, - ); - - expect(Navigate).toHaveBeenCalledWith({ to: '/test' }, {}); - }); - - it('should render spinner when isLoading is true', () => { - render( - -
Test Child
-
, - ); - expect(screen.getByTestId('redirectionGuard_spinner')).toBeInTheDocument(); - }); - - it('should render errorComponent when isError is true', () => { - render( - Test Error
} - route="/test" - > -
Test Child
- , - ); - expect(screen.getByText('Test Error')).toBeInTheDocument(); - expect(Navigate).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/manager-react-components/src/components/region/region.component.tsx b/packages/manager-react-components/src/components/region/region.component.tsx deleted file mode 100644 index 84124286dedb..000000000000 --- a/packages/manager-react-components/src/components/region/region.component.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Region - * @deprecated use translation from common-translations instead - */ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import './translations/region'; -import './translations/datacenter'; - -/** - * Region - * @deprecated use translation from common-translations instead - */ -export interface RegionProps { - mode: 'region' | 'datacenter'; - name: string; - micro?: number; -} - -/** - * Region - * @deprecated use translation from common-translations instead - */ -export const Region: React.FC = ({ - mode = 'region', - name, - micro, -}: RegionProps) => { - const { t } = useTranslation(mode === 'region' ? 'region' : 'datacenter'); - - return <>{t(`region_${name}`, { micro })}; -}; diff --git a/packages/manager-react-components/src/components/region/region.spec.tsx b/packages/manager-react-components/src/components/region/region.spec.tsx deleted file mode 100644 index 95e34b4fee80..000000000000 --- a/packages/manager-react-components/src/components/region/region.spec.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { screen } from '@testing-library/react'; -import { render } from '../../utils/test.provider'; -import { Region, RegionProps } from './region.component'; -import translatedRegion from './translations/region/Messages_fr_FR.json'; - -const renderComponent = (props: RegionProps) => { - return render(); -}; - -describe('Region component', () => { - it('renders region correctly for region key', async () => { - renderComponent({ - mode: 'region', - name: 'ca-east-bhs', - }); - const regionElement = screen.getByText( - translatedRegion[ - `region_${'ca-east-bhs'}` as keyof typeof translatedRegion - ], - ); - expect(regionElement).toBeVisible(); - }); - - it('renders region correctly for datacenter key', async () => { - renderComponent({ - mode: 'datacenter', - name: 'RBX', - micro: 2, - }); - const regionElement = screen.getByText('Roubaix (RBX2) - France'); - expect(regionElement).toBeVisible(); - }); -}); diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_de_DE.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_de_DE.json deleted file mode 100644 index ca6df7600c9e..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_de_DE.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "London (ERI{{micro}}) – England", - "region_BHS": "Beauharnois (BHS{{micro}}) – Kanada", - "region_DC": "Beauharnois (DC{{micro}}) – Kanada", - "region_P": "Paris (P{micro}) – Frankreich", - "region_GRA": "Gravelines (GRA{{micro}}) – Frankreich", - "region_HIL": "Hillsboro (HIL{{micro}}) – USA", - "region_LIM": "Limburg (LIM{{micro}}) – Deutschland", - "region_RBX": "Roubaix (RBX{{micro}}) – Frankreich", - "region_SBG": "Straßburg (SBG{{micro}}) – Frankreich", - "region_SGP": "Singapur (SGP{{micro}}) – Asien", - "region_SYD": "Sydney (SYD{{micro}}) – Australien", - "region_VIN": "Vint Hill (VIN{{micro}}) – USA", - "region_WAW": "Warschau (WAW{{micro}}) – Polen", - "region_RBX_HZ": "Roubaix (RBXHZ) – Frankreich", - "region_GSW": "(GSW) – Frankreich", - "region_YNM": "Mumbai (YNM{{micro}}) – Indien" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_en_GB.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_en_GB.json deleted file mode 100644 index b67d88630d9b..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_en_GB.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "London (ERI{{micro}}) – England", - "region_BHS": "Beauharnois (BHS{{micro}}) – Canada", - "region_DC": "Beauharnois (DC{{micro}}) – Canada", - "region_P": "Paris (P{{micro}}) – France", - "region_GRA": "Gravelines (GRA{{micro}}) – France", - "region_HIL": "Hillsboro (HIL{{micro}}) – USA", - "region_LIM": "Limburg (LIM{{micro}}) – Germany", - "region_RBX": "Roubaix (RBX{{micro}}) – France", - "region_SBG": "Strasbourg (SBG{{micro}}) – France", - "region_SGP": "Singapore (SGP{{micro}}) – Asia", - "region_SYD": "Sydney (SYD{{micro}}) – Australia", - "region_VIN": "Vint Hill (VIN{{micro}}) – United States", - "region_WAW": "Warsaw (WAW{{micro}}) – Poland", - "region_RBX_HZ": "Roubaix (RBXHZ) – France", - "region_GSW": "(GSW) – France", - "region_YNM": "Mumbai (YNM{{micro}}) – India" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_es_ES.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_es_ES.json deleted file mode 100644 index 675ed5e2ff37..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_es_ES.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londres (ERI{{micro}}) - Reino Unido", - "region_BHS": "Beauharnois (BHS{{micro}}) - Canadá", - "region_DC": "Beauharnois (DC{{micro}}) - Canadá", - "region_P": "París (P{{micro}}) - Francia", - "region_GRA": "Gravelines (GRA{{micro}}) - Francia", - "region_HIL": "Hillsboro (HIL{{micro}}) - Estados Unidos", - "region_LIM": "Limburgo (LIM{{micro}}) - Alemania", - "region_RBX": "Roubaix (RBX{{micro}}) - Francia", - "region_SBG": "Estrasburgo (SBG{{micro}}) - Francia", - "region_SGP": "Singapur (SGP{{micro}}) - Asia", - "region_SYD": "Sídney (SYD{{micro}}) - Australia", - "region_VIN": "Vint Hill (VIN{{micro}}) - Estados Unidos", - "region_WAW": "Varsovia (WAW{{micro}}) - Polonia", - "region_RBX_HZ": "Roubaix (RBXHZ) - Francia", - "region_GSW": "(GSW) - Francia", - "region_YNM": "Mumbai (YNM{{micro}}) - India" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_CA.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_CA.json deleted file mode 100644 index 69e838c00b1e..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_CA.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londres (ERI{{micro}}) - Angleterre", - "region_BHS": "Beauharnois (BHS{{micro}}) - Canada", - "region_DC": "Beauharnois (DC{{micro}}) - Canada", - "region_P": "Paris (P{{micro}}) - France", - "region_GRA": "Gravelines (GRA{{micro}}) - France", - "region_HIL": "Hillsboro (HIL{{micro}}) - États-Unis", - "region_LIM": "Limburg (LIM{{micro}}) - Allemagne", - "region_RBX": "Roubaix (RBX{{micro}}) - France", - "region_SBG": "Strasbourg (SBG{{micro}}) - France", - "region_SGP": "Singapour (SGP{{micro}}) - Asie", - "region_SYD": "Sydney (SYD{{micro}}) - Australie", - "region_VIN": "Vint Hill (VIN{{micro}}) - États-Unis", - "region_WAW": "Varsovie (WAW{{micro}}) - Pologne", - "region_RBX_HZ": "Roubaix (RBXHZ) - France", - "region_GSW": "(GSW) - France", - "region_YNM": "Mumbai (YNM{{micro}}) - Inde" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_FR.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_FR.json deleted file mode 100644 index 69e838c00b1e..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_fr_FR.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londres (ERI{{micro}}) - Angleterre", - "region_BHS": "Beauharnois (BHS{{micro}}) - Canada", - "region_DC": "Beauharnois (DC{{micro}}) - Canada", - "region_P": "Paris (P{{micro}}) - France", - "region_GRA": "Gravelines (GRA{{micro}}) - France", - "region_HIL": "Hillsboro (HIL{{micro}}) - États-Unis", - "region_LIM": "Limburg (LIM{{micro}}) - Allemagne", - "region_RBX": "Roubaix (RBX{{micro}}) - France", - "region_SBG": "Strasbourg (SBG{{micro}}) - France", - "region_SGP": "Singapour (SGP{{micro}}) - Asie", - "region_SYD": "Sydney (SYD{{micro}}) - Australie", - "region_VIN": "Vint Hill (VIN{{micro}}) - États-Unis", - "region_WAW": "Varsovie (WAW{{micro}}) - Pologne", - "region_RBX_HZ": "Roubaix (RBXHZ) - France", - "region_GSW": "(GSW) - France", - "region_YNM": "Mumbai (YNM{{micro}}) - Inde" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_it_IT.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_it_IT.json deleted file mode 100644 index abf8b9ef69d6..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_it_IT.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londra (ERI{{micro}}) - Inghilterra", - "region_BHS": "Beauharnois (BHS{{micro}}) - Canada", - "region_DC": "Beauharnois (DC{{micro}}) - Canada", - "region_P": "Parigi (P{{micro}}) - Francia", - "region_GRA": "Gravelines (GRA{{micro}}) - Francia", - "region_HIL": "Hillsboro (HIL{{micro}}) - Stati Uniti", - "region_LIM": "Limburg (LIM{{micro}}) - Germania", - "region_RBX": "Roubaix (RBX{{micro}}) - Francia", - "region_SBG": "Strasburgo (SBG{{micro}}) - Francia", - "region_SGP": "Singapore (SGP{{micro}}) - Asia", - "region_SYD": "Sydney (SYD{{micro}}) - Australia", - "region_VIN": "Vint Hill (VIN{{micro}}) - Stati Uniti", - "region_WAW": "Varsavia (WAW{{micro}}) - Polonia", - "region_RBX_HZ": "Roubaix (RBXHZ) - Francia", - "region_GSW": "(GSW) - Francia", - "region_YNM": "Mumbai (YNM{{micro}}) - India" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pl_PL.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pl_PL.json deleted file mode 100644 index b18f4f90a3c8..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pl_PL.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londyn (ERI {{micro}}) - Wielka Brytania", - "region_BHS": "Beauharnois (BHS {{micro}}) - Kanada", - "region_DC": "Beauharnois (DC {{micro}}) - Kanada", - "region_P": "Paryż (P {{micro}}) - Francja", - "region_GRA": "Gravelines (GRA {{micro}}) - Francja", - "region_HIL": "Hillsboro (HIL {{micro}}) - Stany Zjednoczone", - "region_LIM": "Limburg (LIM {{micro}}) - Niemcy", - "region_RBX": "Roubaix (RBX {{micro}}) - Francja", - "region_SBG": "Strasburg (SBG {{micro}}) - Francja", - "region_SGP": "Singapur (SGP {{micro}}) - Azja", - "region_SYD": "Sydney (SYD {{micro}}) - Australia", - "region_VIN": "Vint Hill (VIN {{micro}}) - Stany Zjednoczone", - "region_WAW": "Warszawa (WAW {{micro}}) - Polska", - "region_RBX_HZ": "Roubaix (RBXHZ) - Francja", - "region_GSW": "(GSW) - Francja", - "region_YNM": "Mumbaj (YNM {{micro}}) - Indie" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pt_PT.json b/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pt_PT.json deleted file mode 100644 index bafeba51aaaa..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/Messages_pt_PT.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "region_ERI": "Londres (ERI{{micro}}) - Inglaterra", - "region_BHS": "Beauharnois (BHS{{micro}}) - Canadá", - "region_DC": "Beauharnois (DC{{micro}}) - Canadá", - "region_P": "Paris (P{{micro}}) - França", - "region_GRA": "Gravelines (GRA{{micro}}) - França", - "region_HIL": "Hillsboro (HIL{{micro}}) - Estados Unidos", - "region_LIM": "Limburgo (LIM{{micro}}) - Alemanha", - "region_RBX": "Roubaix (RBX{{micro}}) - França", - "region_SBG": "Estrasburgo (SBG{{micro}}) - França", - "region_SGP": "Singapura (SGP{{micro}}) - Ásia", - "region_SYD": "Sydney (SYD{{micro}}) - Austrália", - "region_VIN": "Vint Hill (VIN{{micro}}) - Estados Unidos", - "region_WAW": "Varsóvia (WAW{{micro}}) - Polónia", - "region_RBX_HZ": "Roubaix (RBXHZ) - França", - "region_GSW": "(GSW) - França", - "region_YNM": "Mumbai (YNM{{micro}}) - Índia" -} diff --git a/packages/manager-react-components/src/components/region/translations/datacenter/index.ts b/packages/manager-react-components/src/components/region/translations/datacenter/index.ts deleted file mode 100644 index 2671e9228f9a..000000000000 --- a/packages/manager-react-components/src/components/region/translations/datacenter/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'datacenter'); diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_de_DE.json b/packages/manager-react-components/src/components/region/translations/region/Messages_de_DE.json deleted file mode 100644 index 3beb6aa82850..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_de_DE.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europa (Frankreich – Paris)", - "region_eu-west-gra": "Europa (Frankreich – Gravelines)", - "region_eu-west-rbx": "Europa (Frankreich – Roubaix)", - "region_eu-west-sbg": "Europa (Frankreich – Straßburg)", - "region_eu-west-lim": "Europa (Deutschland – Limburg)", - "region_eu-central-waw": "Europa (Polen – Warschau)", - "region_eu-west-eri": "Europa (UK – Erith)", - "region_us-east-vin": "Nordamerika (USA – Osten – Vinthill)", - "region_us-west-hil": "Nordamerika (USA – Westen – Hillsboro)", - "region_ca-east-bhs": "Nordamerika (Kanada – Osten – Beauharnois)", - "region_ap-southeast-sgp": "Asien-Pazifik (Singapur – Singapur)", - "region_ap-southeast-syd": "Asien-Pazifik (Australien – Sydney)", - "region_eu-west-rbx-snc": "Europa (Frankreich – Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europa (Frankreich – Straßburg) (snc)", - "region_ca-east-tor": "Nordamerika (Kanada – Osten – Toronto)", - "region_ap-south-mum": "Asien-Pazifik (Indien – Mumbai)", - "region_labeu-west-1-preprod": "LABEU (Frankreich – Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (Frankreich – Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (Frankreich – Croix) (dev-1)", - "region_eu-west-lz-bru": "Europa (Belgien – Brüssel) (lz)", - "region_eu-west-lz-mad": "Europa (Spanien – Madrid) (lz)", - "region_eu-west-gra-snc": "Europa (Frankreich – Gravelines) (snc)", - "region_us-east-lz-dal": "Nordamerika (USA – Osten – Dallas)", - "region_us-west-lz-lax": "Nordamerika (USA – Westen – Los Angeles)", - "region_us-east-lz-chi": "Nordamerika (USA – Osten – Chicago)", - "region_us-east-lz-nyc": "Nordamerika (USA – Osten – New York)", - "region_us-east-lz-mia": "Nordamerika (USA – Osten – Miami)", - "region_us-west-lz-pao": "Nordamerika (USA – Westen – Palo Alto)", - "region_us-west-lz-den": "Nordamerika (USA – Westen – Denver)", - "region_us-east-lz-atl": "Nordamerika (USA – Osten – Atlanta)", - "region_eu-west-lz-mrs": "Europa (Frankreich – Marseille)", - "region_eu-south-mil": "Europa (Italien – Mailand)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_en_GB.json b/packages/manager-react-components/src/components/region/translations/region/Messages_en_GB.json deleted file mode 100644 index 5d3b3836b49f..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_en_GB.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europe (France – Paris)", - "region_eu-west-gra": "Europe (France – Gravelines)", - "region_eu-west-rbx": "Europe (France – Roubaix)", - "region_eu-west-sbg": "Europe (France – Strasbourg)", - "region_eu-west-lim": "Europe (Germany – Limburg)", - "region_eu-central-waw": "Europe (Poland – Warsaw)", - "region_eu-west-eri": "Europe (UK – Erith)", - "region_us-east-vin": "North America (US – East – Vinthill)", - "region_us-west-hil": "North America (US – West – Hillsboro)", - "region_ca-east-bhs": "North America (Canada – East – Beauharnois)", - "region_ap-southeast-sgp": "Asia Pacific (Singapore – Singapore)", - "region_ap-southeast-syd": "Asia Pacific (Australia – Sydney)", - "region_eu-west-rbx-snc": "Europe (France – Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europe (France – Strasbourg) (snc)", - "region_ca-east-tor": "North America (Canada – East – Toronto)", - "region_ap-south-mum": "Asia Pacific (India – Mumbai)", - "region_labeu-west-1-preprod": "LABEU (France – Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (France – Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (France – Croix) (dev-1)", - "region_eu-west-lz-bru": "Europe (Belgium – Brussels) (lz)", - "region_eu-west-lz-mad": "Europe (Spain – Madrid) (lz)", - "region_eu-west-gra-snc": "Europe (France – Gravelines) (snc)", - "region_us-east-lz-dal": "North America (US – East – Dallas)", - "region_us-west-lz-lax": "North America (US – West – Los Angeles)", - "region_us-east-lz-chi": "North America (US – East – Chicago)", - "region_us-east-lz-nyc": "North America (US – East – New York)", - "region_us-east-lz-mia": "North America (US – East – Miami)", - "region_us-west-lz-pao": "North America (US – West – Palo Alto)", - "region_us-west-lz-den": "North America (US – West – Denver)", - "region_us-east-lz-atl": "North America (US – East – Atlanta)", - "region_eu-west-lz-mrs": "Europe (France – Marseille)", - "region_eu-south-mil": "Europe (Italy - Milan)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_es_ES.json b/packages/manager-react-components/src/components/region/translations/region/Messages_es_ES.json deleted file mode 100644 index e04e5bf744cd..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_es_ES.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europa (Francia - París)", - "region_eu-west-gra": "Europa (Francia - Gravelines)", - "region_eu-west-rbx": "Europa (Francia - Roubaix)", - "region_eu-west-sbg": "Europa (Francia - Estrasburgo)", - "region_eu-west-lim": "Europa (Alemania - Limburgo)", - "region_eu-central-waw": "Europa (Polonia - Varsovia)", - "region_eu-west-eri": "Europa (Reino Unido - Erith)", - "region_us-east-vin": "Norteamérica (Estados Unidos - Este - Vint Hill)", - "region_us-west-hil": "Norteamérica (Estados Unidos - Oeste - Hillsboro)", - "region_ca-east-bhs": "Norteamérica (Canadá - Este - Beauharnois)", - "region_ap-southeast-sgp": "Asia-Pacífico (Singapur - Singapur)", - "region_ap-southeast-syd": "Asia-Pacífico (Australia - Sídney)", - "region_eu-west-rbx-snc": "Europa (Francia - Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europa (Francia - Estrasburgo) (snc)", - "region_ca-east-tor": "Norteamérica (Canadá - Este - Toronto)", - "region_ap-south-mum": "Asia-Pacífico (India - Mumbai)", - "region_labeu-west-1-preprod": "LABEU (Francia - Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (Francia - Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (Francia - Croix) (dev-1)", - "region_eu-west-lz-bru": "Europa (Bélgica - Bruselas) (lz)", - "region_eu-west-lz-mad": "Europa (España - Madrid) (lz)", - "region_eu-west-gra-snc": "Europa (Francia - Gravelines) (snc)", - "region_us-east-lz-dal": "Norteamérica (Estados Unidos - Este - Dallas)", - "region_us-west-lz-lax": "Norteamérica (Estados Unidos - Oeste - Los Ángeles)", - "region_us-east-lz-chi": "Norteamérica (Estados Unidos - Este - Chicago)", - "region_us-east-lz-nyc": "Norteamérica (Estados Unidos - Este - Nueva York)", - "region_us-east-lz-mia": "Norteamérica (Estados Unidos - Este - Miami)", - "region_us-west-lz-pao": "Norteamérica (Estados Unidos - Oeste - Palo Alto)", - "region_us-west-lz-den": "Norteamérica (Estados Unidos - Oeste - Denver)", - "region_us-east-lz-atl": "Norteamérica (Estados Unidos - Este - Atlanta)", - "region_eu-west-lz-mrs": "Europa (Francia - Marsella)", - "region_eu-south-mil": "Europa (Italia - Milán)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_fr_CA.json b/packages/manager-react-components/src/components/region/translations/region/Messages_fr_CA.json deleted file mode 100644 index a88841b97a5c..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_fr_CA.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europe (France - Paris)", - "region_eu-west-gra": "Europe (France - Gravelines)", - "region_eu-west-rbx": "Europe (France - Roubaix)", - "region_eu-west-sbg": "Europe (France - Strasbourg)", - "region_eu-west-lim": "Europe (Germany - Limburg)", - "region_eu-south-mil": "Europe (Italy - Milan)", - "region_eu-central-waw": "Europe (Poland - Warsaw)", - "region_eu-west-eri": "Europe (UK - Erith)", - "region_us-east-vin": "North America (US - East - Vinthill)", - "region_us-west-hil": "North America (US - West - Hillsboro)", - "region_ca-east-bhs": "North America (Canada - East - Beauharnois)", - "region_ap-southeast-sgp": "Asia Pacific (Singapore - Singapore)", - "region_ap-southeast-syd": "Asia Pacific (Australia - Sydney)", - "region_eu-west-rbx-snc": "Europe (France - Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europe (France - Strasbourg) (snc)", - "region_ca-east-tor": "North America (Canada - East - Toronto)", - "region_ap-south-mum": "Asia Pacific (India - Mumbai)", - "region_labeu-west-1-preprod": "LABEU (France - Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (France - Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (France - Croix) (dev-1)", - "region_eu-west-lz-bru": "Europe (Belgium - Brussels) (lz)", - "region_eu-west-lz-mad": "Europe (Spain - Madrid) (lz)", - "region_eu-west-gra-snc": "Europe (France - Gravelines) (snc)", - "region_us-east-lz-dal": "North America (US - East - Dallas)", - "region_us-west-lz-lax": "North America (US - West - Los Angeles)", - "region_us-east-lz-chi": "North America (US - East - Chicago)", - "region_us-east-lz-nyc": "North America (US - East - New York)", - "region_us-east-lz-mia": "North America (US - East - Miami)", - "region_us-west-lz-pao": "North America (US - West - Palo Alto)", - "region_us-west-lz-den": "North America (US - West - Denver)", - "region_us-east-lz-atl": "North America (US - East - Atlanta)", - "region_eu-west-lz-mrs": "Europe (France - Marseille)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_fr_FR.json b/packages/manager-react-components/src/components/region/translations/region/Messages_fr_FR.json deleted file mode 100644 index a88841b97a5c..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_fr_FR.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europe (France - Paris)", - "region_eu-west-gra": "Europe (France - Gravelines)", - "region_eu-west-rbx": "Europe (France - Roubaix)", - "region_eu-west-sbg": "Europe (France - Strasbourg)", - "region_eu-west-lim": "Europe (Germany - Limburg)", - "region_eu-south-mil": "Europe (Italy - Milan)", - "region_eu-central-waw": "Europe (Poland - Warsaw)", - "region_eu-west-eri": "Europe (UK - Erith)", - "region_us-east-vin": "North America (US - East - Vinthill)", - "region_us-west-hil": "North America (US - West - Hillsboro)", - "region_ca-east-bhs": "North America (Canada - East - Beauharnois)", - "region_ap-southeast-sgp": "Asia Pacific (Singapore - Singapore)", - "region_ap-southeast-syd": "Asia Pacific (Australia - Sydney)", - "region_eu-west-rbx-snc": "Europe (France - Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europe (France - Strasbourg) (snc)", - "region_ca-east-tor": "North America (Canada - East - Toronto)", - "region_ap-south-mum": "Asia Pacific (India - Mumbai)", - "region_labeu-west-1-preprod": "LABEU (France - Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (France - Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (France - Croix) (dev-1)", - "region_eu-west-lz-bru": "Europe (Belgium - Brussels) (lz)", - "region_eu-west-lz-mad": "Europe (Spain - Madrid) (lz)", - "region_eu-west-gra-snc": "Europe (France - Gravelines) (snc)", - "region_us-east-lz-dal": "North America (US - East - Dallas)", - "region_us-west-lz-lax": "North America (US - West - Los Angeles)", - "region_us-east-lz-chi": "North America (US - East - Chicago)", - "region_us-east-lz-nyc": "North America (US - East - New York)", - "region_us-east-lz-mia": "North America (US - East - Miami)", - "region_us-west-lz-pao": "North America (US - West - Palo Alto)", - "region_us-west-lz-den": "North America (US - West - Denver)", - "region_us-east-lz-atl": "North America (US - East - Atlanta)", - "region_eu-west-lz-mrs": "Europe (France - Marseille)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_it_IT.json b/packages/manager-react-components/src/components/region/translations/region/Messages_it_IT.json deleted file mode 100644 index 6957e67924ef..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_it_IT.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europa (Francia - Parigi)", - "region_eu-west-gra": "Europa (Francia - Gravelines)", - "region_eu-west-rbx": "Europa (Francia - Roubaix)", - "region_eu-west-sbg": "Europa (Francia - Strasburgo)", - "region_eu-west-lim": "Europa (Germania - Limburgo)", - "region_eu-central-waw": "Europa (Polonia - Varsavia)", - "region_eu-west-eri": "Europa (Regno Unito - Erith)", - "region_us-east-vin": "Nord America (Stati Uniti - Est - Vinthill)", - "region_us-west-hil": "Nord America (Stati Uniti - Ovest - Hillsboro)", - "region_ca-east-bhs": "Nord America (Canada - Est - Beauharnois)", - "region_ap-southeast-sgp": "Asia Pacifica (Singapore - Singapore)", - "region_ap-southeast-syd": "Asia Pacifica (Australia - Sydney)", - "region_eu-west-rbx-snc": "Europa (Francia - Roubaix) (SNC)", - "region_eu-west-sbg-snc": "Europa (Francia - Strasburgo) (SNC)", - "region_ca-east-tor": "Nord America (Canada - Est - Toronto)", - "region_ap-south-mum": "Asia Pacifica (India - Mumbai)", - "region_labeu-west-1-preprod": "LABEU (Francia - Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (Francia - Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (Francia - Croix) (dev-1)", - "region_eu-west-lz-bru": "Europa (Belgio - Bruxelles) (LZ)", - "region_eu-west-lz-mad": "Europa (Spagna - Madrid) (LZ)", - "region_eu-west-gra-snc": "Europa (Francia - Gravelines) SNC", - "region_us-east-lz-dal": "Nord America (Stati Uniti - Est - Dallas)", - "region_us-west-lz-lax": "Nord America (Stati Uniti - Ovest - Los Angeles)", - "region_us-east-lz-chi": "Nord America (Stati Uniti - Est - Chicago)", - "region_us-east-lz-nyc": "Nord America (Stati Uniti - Est - New York)", - "region_us-east-lz-mia": "Nord America (Stati Uniti - Est - Miami)", - "region_us-west-lz-pao": "Nord America (Stati Uniti - Ovest - Palo Alto)", - "region_us-west-lz-den": "Nord America (Stati Uniti - Ovest - Denver)", - "region_us-east-lz-atl": "Nord America (Stati Uniti - Est - Atlanta)", - "region_eu-west-lz-mrs": "Europa (Francia - Marsiglia)", - "region_eu-south-mil": "Europa (Italia - Milano)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_pl_PL.json b/packages/manager-react-components/src/components/region/translations/region/Messages_pl_PL.json deleted file mode 100644 index 0cc1c52d2339..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_pl_PL.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europa (Francja - Paryż)", - "region_eu-west-gra": "Europa (Francja - Gravelines)", - "region_eu-west-rbx": "Europa (Francja - Roubaix)", - "region_eu-west-sbg": "Europa (Francja - Strasburg)", - "region_eu-west-lim": "Europa (Niemcy - Limburg)", - "region_eu-central-waw": "Europa (Polska - Warszawa)", - "region_eu-west-eri": "Europa (UK - Erith)", - "region_us-east-vin": "Ameryka Północna (US - East - Vinthill)", - "region_us-west-hil": "Ameryka Północna (US - West - Hillsboro)", - "region_ca-east-bhs": "North America (Kanada - East - Beauharnois)", - "region_ap-southeast-sgp": "Azja-Pacyfik (Singapur - Singapur)", - "region_ap-southeast-syd": "Azja-Pacyfik (Australia-Sidney)", - "region_eu-west-rbx-snc": "Europa (Francja - Roubaix) (snc)", - "region_eu-west-sbg-snc": "Europa (Francja - Strasburg) (snc)", - "region_ca-east-tor": "North America (Kanada - East - Toronto)", - "region_ap-south-mum": "Azja-Pacyfik (Indie-Mumbaj)", - "region_labeu-west-1-preprod": "LABEU (Francja - Croix) (preprod)", - "region_labeu-west-1-dev-2": "LABEU (Francja - Croix) (dev-2)", - "region_labeu-west-1-dev-1": "LABEU (Francja - Croix) (dev-1)", - "region_eu-west-lz-bru": "Europa (Belgia - Brussels) (lz)", - "region_eu-west-lz-mad": "Europa (Hiszpania - Madryt) (lz)", - "region_eu-west-gra-snc": "Europa (Francja - Gravelines) (snc)", - "region_us-east-lz-dal": "Ameryka Północna (US - East - Dallas)", - "region_us-west-lz-lax": "North America (US - West - Los Angeles)", - "region_us-east-lz-chi": "Ameryka Północna (US - East - Chicago)", - "region_us-east-lz-nyc": "Ameryka Północna (US - East - Nowy Jork)", - "region_us-east-lz-mia": "Ameryka Północna (US - East - Miami)", - "region_us-west-lz-pao": "North America (US - West - Palo Alto)", - "region_us-west-lz-den": "North America (US - West - Denver)", - "region_us-east-lz-atl": "Ameryka Północna (US - East - Atlanta)", - "region_eu-west-lz-mrs": "Europa (Francja - Marsylia)", - "region_eu-south-mil": "Europa (Włochy - Mediolan)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/Messages_pt_PT.json b/packages/manager-react-components/src/components/region/translations/region/Messages_pt_PT.json deleted file mode 100644 index eb217c184c16..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/Messages_pt_PT.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "region_eu-west-par": "Europa (França - Paris)", - "region_eu-west-gra": "Europa (França - Gravelines)", - "region_eu-west-rbx": "Europa (França - Roubaix)", - "region_eu-west-sbg": "Europa (França - Estrasburgo)", - "region_eu-west-lim": "Europa (Alemanha - Limburgo)", - "region_eu-central-waw": "Europa (Polónia - Varsóvia)", - "region_eu-west-eri": "Europa (Reino Unido - Erith)", - "region_us-east-vin": "América do Norte (EUA - East - Vinthill)", - "region_us-west-hil": "América do Norte (EUA - West - Hillboro)", - "region_ca-east-bhs": "América do Norte (Canadá - East - Beauharnois)", - "region_ap-southeast-sgp": "Ásia-Pacífico (Singapura - Singapura)", - "region_ap-southeast-syd": "Ásia-Pacífico (Austrália - Sydney)", - "region_eu-west-rbx-snc": "Europa (França - Roubaix) (SNC)", - "region_eu-west-sbg-snc": "Europa (França - Estrasburgo) (SNC)", - "region_ca-east-tor": "América do Norte (Canadá - East - Toronto)", - "region_ap-south-mum": "Ásia-Pacífico (Índia - Mumbai)", - "region_labeu-west-1-preprod": "LABEU (França - Croix) (PREPROD)", - "region_labeu-west-1-dev-2": "LABEU (França - Croix) (DEV-2)", - "region_labeu-west-1-dev-1": "LABEU (França - Croix) (DEV-1)", - "region_eu-west-lz-bru": "Europa (Bélgica - Bruxelas) (LZ)", - "region_eu-west-lz-mad": "Europa (Espanha - Madrid) (LZ)", - "region_eu-west-gra-snc": "Europa (França - Gravelines) (SNC)", - "region_us-east-lz-dal": "América do Norte (US - East - Dallas)", - "region_us-west-lz-lax": "América do Norte (US - West - Los Angeles)", - "region_us-east-lz-chi": "América do Norte (US - East - Chicago)", - "region_us-east-lz-nyc": "América do Norte (US - East - Nova Iorque)", - "region_us-east-lz-mia": "América do Norte (US - East - Miami)", - "region_us-west-lz-pao": "América do Norte (US - West - Palo Alto)", - "region_us-west-lz-den": "América do Norte (US - West - Denver)", - "region_us-east-lz-atl": "América do Norte (US - East - Atlanta)", - "region_eu-west-lz-mrs": "Europa (França - Marselha)", - "region_eu-south-mil": "Europa (Itália - Milão)" -} diff --git a/packages/manager-react-components/src/components/region/translations/region/index.ts b/packages/manager-react-components/src/components/region/translations/region/index.ts deleted file mode 100644 index f750fecfbbc1..000000000000 --- a/packages/manager-react-components/src/components/region/translations/region/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'region'); diff --git a/packages/manager-react-components/src/components/tags-list/index.ts b/packages/manager-react-components/src/components/tags-list/index.ts deleted file mode 100644 index 959a58b12b04..000000000000 --- a/packages/manager-react-components/src/components/tags-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tags-list.component'; diff --git a/packages/manager-react-components/src/components/tags-list/tags-list.component.tsx b/packages/manager-react-components/src/components/tags-list/tags-list.component.tsx deleted file mode 100644 index 9c010070fe1e..000000000000 --- a/packages/manager-react-components/src/components/tags-list/tags-list.component.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { OdsBadge, OdsLink } from '@ovhcloud/ods-components/react'; -import { OdsBadgeColor, ODS_ICON_NAME } from '@ovhcloud/ods-components'; -import { calculateAuthorizedTags, truncateTag, filterTags } from './tags-utils'; - -export interface TagsListProps { - tags: { [key: string]: string }; - displayInternalTags?: boolean; - lineNumber?: number; - onClick?: () => void; -} - -export const TagsList: React.FC = ({ - tags, - displayInternalTags = false, - lineNumber, - onClick, -}) => { - const color: OdsBadgeColor = 'neutral'; - const filteredTags = filterTags({ tags, displayInternalTags }); - const containerRef = useRef(null); - const [truncate, setTruncate] = useState(null); - const [visibleCount, setVisibleCount] = useState(filteredTags.length); - const tagRefs = useRef([]); - const initialTagRefs = useRef([]); - - useEffect(() => { - if (filteredTags.length > 0) { - const resizeObserver = new ResizeObserver(() => { - const visibleTags = calculateAuthorizedTags( - initialTagRefs.current, - containerRef.current, - lineNumber, - ); - - setVisibleCount(visibleTags || 1); - - if (visibleTags === 0 && tagRefs.current[0]) { - setTruncate( - truncateTag( - containerRef.current, - tagRefs.current[0], - filteredTags[0], - ), - ); - } - }); - - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => resizeObserver.disconnect(); - } - return undefined; - }, [filteredTags]); - - useEffect(() => { - if (initialTagRefs.current.length === 0) { - initialTagRefs.current = [...tagRefs.current]; - } - }, []); - - return ( - Object.keys(tags).length > 0 && ( -
- {truncate ? ( - - ) : ( - filteredTags.slice(0, visibleCount).map((tag, index) => { - return ( - { - tagRefs.current[index] = el!; - }} - color={color} - label={tag} - /> - ); - }) - )} - - {visibleCount < filteredTags.length && ( - { - if (onClick) onClick(); - e.preventDefault(); - }} - icon={ODS_ICON_NAME.chevronDoubleRight} - /> - )} -
- ) - ); -}; diff --git a/packages/manager-react-components/src/components/tags-list/tags-list.spec.tsx b/packages/manager-react-components/src/components/tags-list/tags-list.spec.tsx deleted file mode 100644 index 0f99b76d02fc..000000000000 --- a/packages/manager-react-components/src/components/tags-list/tags-list.spec.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { render, screen, act } from '@testing-library/react'; -import { describe, it, expect, beforeAll } from 'vitest'; -import { TagsList } from './tags-list.component'; - -describe('TagsList', () => { - const tags = { - tag1: 'value1', - tag2: 'value2', - tag3: 'value3', - tag4: 'value4', - tag5: 'value5', - }; - let resizeCallback: ResizeObserverCallback; - let originalOffsetWidth: any; - let originalOffsetHeight: any; - let originBoundingClientRect: any; - - beforeAll(() => { - originalOffsetWidth = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'offsetWidth', - ); - originalOffsetHeight = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'offsetHeight', - ); - - originBoundingClientRect = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'getBoundingClientRect', - ); - - global.ResizeObserver = class { - constructor(callback: ResizeObserverCallback) { - resizeCallback = callback; - } - - // eslint-disable-next-line class-methods-use-this - observe() {} - - // eslint-disable-next-line class-methods-use-this - unobserve() {} - - // eslint-disable-next-line class-methods-use-this - disconnect() {} - } as any; - }); - - afterEach(() => { - if (originalOffsetWidth) { - Object.defineProperty( - HTMLElement.prototype, - 'offsetWidth', - originalOffsetWidth, - ); - } - if (originalOffsetHeight) { - Object.defineProperty( - HTMLElement.prototype, - 'offsetHeight', - originalOffsetHeight, - ); - } - if (originBoundingClientRect) { - Object.defineProperty( - HTMLElement.prototype, - 'getBoundingClientRect', - originalOffsetHeight, - ); - } - }); - - it('renders all badges when container is large enough (no truncate)', () => { - const { container } = render(); - - act(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 900, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 30, - }); - Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { - configurable: true, - value: () => ({ width: 20 }) as DOMRect, - }); - - resizeCallback([], {} as ResizeObserver); - }); - - const badges = container.querySelectorAll('ods-badge'); - - expect(badges.length).toBe(5); - expect(badges[4].getAttribute('label')).toBe('tag5:value5'); - }); - - it('renders truncated badge when container is too small', () => { - const { container } = render(); - - act(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 10, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 10, - }); - Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { - configurable: true, - value: () => ({ width: 20 }) as DOMRect, - }); - resizeCallback([], {} as ResizeObserver); - }); - - const badges = container.querySelectorAll('ods-badge'); - - expect(badges.length).toBe(1); - expect(badges[0].getAttribute('label')).toBe('...'); - }); - - it('render all badge when have multi-line', () => { - const { container } = render(); - - act(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 30, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 10, - }); - Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { - configurable: true, - value: () => ({ width: 20 }) as DOMRect, - }); - }); - - const badges = container.querySelectorAll('ods-badge'); - - expect(badges.length).toBe(5); - }); -}); diff --git a/packages/manager-react-components/src/components/tags-list/tags-utils.test.ts b/packages/manager-react-components/src/components/tags-list/tags-utils.test.ts deleted file mode 100644 index e08e7fa5e0c3..000000000000 --- a/packages/manager-react-components/src/components/tags-list/tags-utils.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// tags-utils.test.ts -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { calculateAuthorizedTags, truncateTag, filterTags } from './tags-utils'; - -describe('calculateAuthorizedTags', () => { - let container: HTMLDivElement; - let tagElements: HTMLOdsBadgeElement[]; - - beforeEach(() => { - container = document.createElement('div'); - container.style.width = '300px'; - container.style.height = '60px'; - Object.defineProperty(container, 'offsetWidth', { value: 300 }); - Object.defineProperty(container, 'offsetHeight', { value: 60 }); - - tagElements = Array.from({ length: 5 }).map(() => { - const tag = document.createElement( - 'div', - ) as unknown as HTMLOdsBadgeElement; - Object.defineProperty(tag, 'offsetHeight', { value: 20 }); - Object.defineProperty(tag, 'getBoundingClientRect', { - value: () => ({ width: 50 }) as DOMRect, - }); - return tag; - }); - }); - - it('should calculate how many tags can fit', () => { - const result = calculateAuthorizedTags(tagElements, container, 2); - expect(result).toBeGreaterThan(0); - expect(result).toBeLessThanOrEqual(tagElements.length); - }); - - it('should return undefined if container is null', () => { - const result = calculateAuthorizedTags(tagElements, null as any, 2); - expect(result).toBeUndefined(); - }); -}); - -describe('truncateTag', () => { - it('should truncate the text proportionally to the container size', () => { - const container = document.createElement('div'); - const tag = document.createElement('div') as unknown as HTMLOdsBadgeElement; - const fullText = 'LongTagNameForTesting'; - - Object.defineProperty(container, 'offsetWidth', { value: 100 }); - Object.defineProperty(tag, 'getBoundingClientRect', { - value: () => ({ width: 200 }) as DOMRect, - }); - - const result = truncateTag(container, tag, fullText); - expect(result.length).toBeLessThan(fullText.length + 1); - expect(result.endsWith('...')).toBe(true); - }); - - it('should return full text if fits completely', () => { - const container = document.createElement('div'); - const tag = document.createElement('div') as unknown as HTMLOdsBadgeElement; - const shortText = 'Tag'; - - Object.defineProperty(container, 'offsetWidth', { value: 200 }); - Object.defineProperty(tag, 'getBoundingClientRect', { - value: () => ({ width: 100 }) as DOMRect, - }); - - const result = truncateTag(container, tag, shortText); - expect(result).toBe(shortText); - }); -}); - -describe('filterTags', () => { - const tags = { - environment: 'dev', - environment1: 'prod', - 'ovh:internal': 'prod', - }; - - it('should return all tags when displayInternalTags is true', () => { - const result = filterTags({ tags, displayInternalTags: true }); - expect(result).toEqual([ - 'environment:dev', - 'environment1:prod', - 'ovh:internal:prod', - ]); - }); - - it('should filter out internal tags when displayInternalTags is false', () => { - const result = filterTags({ tags, displayInternalTags: false }); - expect(result).toEqual(['environment:dev', 'environment1:prod']); - }); - - it('should return an empty array if tags is empty', () => { - const result = filterTags({ tags: {}, displayInternalTags: true }); - expect(result).toEqual([]); - }); -}); diff --git a/packages/manager-react-components/src/components/tags-list/tags-utils.ts b/packages/manager-react-components/src/components/tags-list/tags-utils.ts deleted file mode 100644 index 5f979e994da6..000000000000 --- a/packages/manager-react-components/src/components/tags-list/tags-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -export const calculateAuthorizedTags = ( - tagRefs: HTMLOdsBadgeElement[], - containerRef: HTMLDivElement, - lineNumber: number, -): number => { - if (!containerRef) return undefined; - const containerWidth = containerRef.offsetWidth; - const containerHeight = containerRef.offsetHeight; - const tagHeight = tagRefs[0] ? tagRefs[0].offsetHeight + 4 : 30; - const maxLine = lineNumber || Math.ceil(containerHeight / tagHeight); - const maxCounter = maxLine * containerWidth; - let usedWidth = 0; - let count = 0; - - tagRefs.forEach((tag) => { - if (!tag) return; - const tagWidth = tag.getBoundingClientRect().width; - if ( - lineNumber && - (tagWidth === 0 || usedWidth + tagWidth > maxCounter - 50) - ) - return; - usedWidth += tagWidth; - count += 1; - }); - return count; -}; - -export const truncateTag = ( - container: HTMLDivElement, - firstTag: HTMLOdsBadgeElement, - tagText: string, -): string => { - const containerWidth = container.offsetWidth; - const availableSpace = Math.ceil( - ((containerWidth - 50) * 100) / firstTag.getBoundingClientRect().width, - ); - const length = Math.floor(tagText.length * (availableSpace / 100)); - return tagText.slice(0, length) + (length < tagText.length ? '...' : ''); -}; - -export const filterTags = ({ - tags, - displayInternalTags, -}: { - tags: { [key: string]: string }; - displayInternalTags: boolean; -}) => { - return Object.keys(tags) - .filter((key) => displayInternalTags || key.indexOf('ovh:') !== 0) - .map((key) => `${key}:${tags[key]}`); -}; diff --git a/packages/manager-react-components/src/components/tags-modal/index.ts b/packages/manager-react-components/src/components/tags-modal/index.ts deleted file mode 100644 index e687b128e91a..000000000000 --- a/packages/manager-react-components/src/components/tags-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tags-modal.component'; diff --git a/packages/manager-react-components/src/components/tags-modal/tags-modal.component.tsx b/packages/manager-react-components/src/components/tags-modal/tags-modal.component.tsx deleted file mode 100644 index 5e025c1df760..000000000000 --- a/packages/manager-react-components/src/components/tags-modal/tags-modal.component.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { - OdsButton, - OdsModal, - OdsText, - OdsInput, -} from '@ovhcloud/ods-components/react'; -import React, { Ref, useState } from 'react'; -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_TEXT_PRESET, - OdsInputChangeEvent, -} from '@ovhcloud/ods-components'; -import { useTranslation } from 'react-i18next'; -import { TagsList } from '../tags-list'; -import './translations'; - -export interface TagsModalProps { - displayName: string; - isOpen: boolean; - tags: { [key: string]: string }; - displayInternalTags?: boolean; - onCancel: () => void; - onEditTags?: () => void; -} - -export const TagsModal = React.forwardRef( - ( - { - isOpen = false, - displayName, - tags, - displayInternalTags = false, - onEditTags, - onCancel, - }, - ref: Ref, - ) => { - const { t } = useTranslation('tags-modal'); - const [search, setSearch] = useState(''); - const [results, setResults] = useState<{ [key: string]: string }>(tags); - - const handleSearch = () => { - setResults( - search - ? Object.fromEntries( - Object.entries(tags).filter( - ([key, value]: [string, string]) => - key.toLowerCase().includes(search.toLowerCase()) || - value.toLowerCase().includes(search.toLowerCase()), - ), - ) - : tags, - ); - }; - - return ( - - - {'Tags:'} {displayName} - -
- { - setSearch(event.detail.value as string); - }} - /> - -
- -
- {results && ( - - )} -
- - {onEditTags && ( - - )} -
- ); - }, -); diff --git a/packages/manager-react-components/src/components/tags-modal/tags-modal.spec.tsx b/packages/manager-react-components/src/components/tags-modal/tags-modal.spec.tsx deleted file mode 100644 index 7576134236d6..000000000000 --- a/packages/manager-react-components/src/components/tags-modal/tags-modal.spec.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { TagsModal } from './tags-modal.component'; - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -describe('TagsModal', () => { - beforeAll(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 900, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 30, - }); - Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { - configurable: true, - value: () => ({ width: 20 }) as DOMRect, - }); - - global.ResizeObserver = class { - // eslint-disable-next-line class-methods-use-this - observe() {} - - // eslint-disable-next-line class-methods-use-this - unobserve() {} - - // eslint-disable-next-line class-methods-use-this - disconnect() {} - } as any; - - vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation( - function () { - const length = this.textContent?.length || 0; - return { - width: Math.max(20, length * 8), - height: 100, - top: 0, - left: 0, - bottom: 100, - right: Math.max(20, length * 8), - x: 0, - y: 0, - toJSON() {}, - }; - }, - ); - }); - - afterAll(() => { - vi.restoreAllMocks(); - }); - - const tags = { - tag1: 'value1', - tag2: 'value2', - }; - - const onCancel = vi.fn(); - const onEditTags = vi.fn(); - - it('renders and uses mocked getBoundingClientRect', () => { - const { container } = render( - , - ); - - const modal = container.querySelector('ods-modal'); - expect(modal.getBoundingClientRect().width).toBeGreaterThan(20); - - const backButton = container.querySelector('ods-button[label="back"]'); - fireEvent.click(backButton); - expect(onCancel).toHaveBeenCalledOnce(); - const editButton = container.querySelector('ods-button[label="edit_tags"]'); - fireEvent.click(editButton); - expect(onEditTags).toHaveBeenCalledTimes(1); - }); - - it('filters tags correctly when searching all value', async () => { - const { container } = render( - {}} - onEditTags={() => {}} - />, - ); - - const input = screen.getByPlaceholderText('search_placeholder'); - fireEvent.change(input, { target: { value: 'tag' } }); - - const searchButton = container.querySelector('ods-button[label="search"]'); - fireEvent.click(searchButton); - await waitFor(() => { - const badges = container.querySelectorAll('ods-badge'); - - expect(badges.length).toBe(4); - expect(badges[3].getAttribute('label')).toBe('tag4:value4'); - }); - }); - - it('filters tags correctly when searching and return one value', async () => { - const { container } = render( - {}} - onEditTags={() => {}} - />, - ); - - const input = screen.getByPlaceholderText('search_placeholder'); - fireEvent.change(input, { target: { value: 'tag1' } }); - - await waitFor(() => { - const searchButton = container.querySelector( - 'ods-button[label="search"]', - ); - fireEvent.click(searchButton); - const badges = container.querySelectorAll('ods-badge'); - - expect(badges.length).toBe(1); - expect(badges[0].getAttribute('label')).toBe('tag1:value1'); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/tags-tile/index.tsx b/packages/manager-react-components/src/components/tags-tile/index.tsx deleted file mode 100644 index 790c94ea4e0d..000000000000 --- a/packages/manager-react-components/src/components/tags-tile/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './tags-tile.component'; diff --git a/packages/manager-react-components/src/components/tags-tile/tags-tile.component.tsx b/packages/manager-react-components/src/components/tags-tile/tags-tile.component.tsx deleted file mode 100644 index c70ef53048d4..000000000000 --- a/packages/manager-react-components/src/components/tags-tile/tags-tile.component.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { OdsLink } from '@ovhcloud/ods-components/react'; -import { ODS_ICON_NAME } from '@ovhcloud/ods-components'; -import { TagsList, TagsListProps } from '../tags-list'; -import './translations'; -import { ManagerTile } from '../content'; - -export interface TagsTileProps extends Omit { - onEditTags?: () => void; -} - -export const TagsTile: React.FC = ({ - tags, - displayInternalTags = false, - lineNumber = 5, - onEditTags, -}) => { - const { t } = useTranslation('tags-tile'); - const isEmptyTags = !tags || Object.keys(tags).length === 0; - - return ( - - {t('tags_tile_title')} - - - {t('assigned_tags')} - -
- {isEmptyTags && {t('tags_tile_empty')}} - {!isEmptyTags && ( - - )} -
- { - onEditTags?.(); - e.preventDefault(); - }} - label={isEmptyTags ? t('tags_tile_add_tag') : t('manage_tags')} - icon={ODS_ICON_NAME.arrowRight} - /> -
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/templates/base/base.component.tsx b/packages/manager-react-components/src/components/templates/base/base.component.tsx deleted file mode 100644 index baa4dce9d0e0..000000000000 --- a/packages/manager-react-components/src/components/templates/base/base.component.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { Headers, HeadersProps } from '../../content'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import { LinkType, Links, Subtitle } from '../../typography'; -import { PageLayout } from '../layout/layout.component'; - -export type BaseLayoutProps = React.PropsWithChildren<{ - breadcrumb?: React.ReactElement; - header?: HeadersProps; - message?: React.ReactElement; - description?: string | React.ReactElement; - subtitle?: string; - subDescription?: string; - backLinkLabel?: string; - hrefPrevious?: string; - tabs?: React.ReactElement; - onClickReturn?: () => void; -}>; - -export const BaseLayout = ({ - backLinkLabel, - hrefPrevious, - onClickReturn, - breadcrumb, - description, - subtitle, - subDescription, - message, - children, - header, - tabs, -}: BaseLayoutProps) => ( - -
{breadcrumb}
- {header && ( -
- -
- )} - {backLinkLabel && (onClickReturn || hrefPrevious) && ( -
- -
- )} - {description && ( - - {description} - - )} - {message &&
{message}
} - {subtitle && {subtitle}} - {subDescription && {subDescription}} - {tabs &&
{tabs}
} - {children} -
-); diff --git a/packages/manager-react-components/src/components/templates/base/base.spec.tsx b/packages/manager-react-components/src/components/templates/base/base.spec.tsx deleted file mode 100644 index d4dacff94afb..000000000000 --- a/packages/manager-react-components/src/components/templates/base/base.spec.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import { vitest } from 'vitest'; -import { - OdsBreadcrumb, - OdsBreadcrumbItem, - OdsTab, - OdsTabs, - OdsTable, - OdsBadge, -} from '@ovhcloud/ods-components/react'; -import { waitFor, screen, fireEvent } from '@testing-library/react'; -import { render } from '../../../utils/test.provider'; -import { BaseLayout } from './base.component'; -import { GuideButton, GuideItem } from '../../navigation'; -import OdsNotification from '../../notifications/ods-notification'; -import { NotificationType } from '../../notifications/useNotifications'; - -const listingTmpltProps = { - breadcrumb: ( - - - - - ), - header: { - title: 'Vrack Services', - headerButton: ( - - ), - }, - description: - 'Description de la listing, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - message: ( - - ), - children: ( - - - - - - - - - - - {[ - { - id: 'user-t', - name: 'user-t', - description: 'Lorem ipsum dolor', - }, - { - id: 'user-n', - name: 'user-n', - description: 'Lorem ipsum dolor', - }, - { - id: 'user-x', - name: 'user-x', - description: 'Lorem ipsum dolor', - }, - { - id: 'user-y', - name: 'user-y', - description: 'Lorem ipsum dolor', - }, - ].map((element) => ( - - - - - - ))} - -
NomDescriptionStatus
{element.name}{element.description} - -
-
- ), - subtitle: '', - backLinkLabel: '', - onClickReturn: () => { - console.log('back link click'); - }, - subdescription: '', -}; - -describe('BaseLayout component', () => { - it('renders base component correctly', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('Vrack Services')).toBeInTheDocument(); - expect( - screen.getByText( - 'Description de la listing, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - ), - ).toBeInTheDocument(); - }); - }); - - it('clicks on back link triggers return fn', async () => { - const backLinkLabel = 'back link'; - const spy = vitest.fn(); - - render( - , - ); - fireEvent.click(screen.getByTestId('manager-back-link')); - await waitFor(() => expect(spy).toHaveBeenCalled()); - }); -}); diff --git a/packages/manager-react-components/src/components/templates/delete-modal/__snapshots__/delete-modal.spec.tsx.snap b/packages/manager-react-components/src/components/templates/delete-modal/__snapshots__/delete-modal.spec.tsx.snap deleted file mode 100644 index c1525a830367..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/__snapshots__/delete-modal.spec.tsx.snap +++ /dev/null @@ -1,44 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Delete Modal component > renders loading modal 1`] = ` - - -
- - Résilier serviceType ? - - - La résiliation de votre service effacera définitivement toutes les données correspondantes. Souhaitez-vous procéder à la résiliation de votre service ? - -
- - -
-
-
-
-`; diff --git a/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.component.tsx b/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.component.tsx deleted file mode 100644 index 1680a2140121..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.component.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { - OdsText, - OdsMessage, - OdsModal, - OdsButton, -} from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_VARIANT, - ODS_BUTTON_COLOR, - ODS_MESSAGE_COLOR, - ODS_MODAL_COLOR, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { handleClick } from '../../../utils/click-utils'; -import './translations/translations'; - -export const defaultDeleteModalTerminateValue = 'TERMINATE'; - -export type DeleteModalProps = { - /** - * @deprecated use serviceTypeName instead. Headline of modal will have a fixed text - */ - headline?: string; - // serviceType: enum; - serviceTypeName?: string; - /** - * Description of modal will have a fixed text - * @deprecated use children instead - */ - description?: string; - /** - * @deprecated input field has been removed - */ - deleteInputLabel?: string; - closeModal: () => void; - isLoading?: boolean; - onConfirmDelete: () => void; - error?: string; - /** - * @deprecated the button label is now fixed - */ - cancelButtonLabel?: string; - /** - * @deprecated the button label is now fixed - */ - confirmButtonLabel?: string; - children?: React.ReactNode; - /** - * @deprecated input field has been removed - */ - terminateValue?: string; - isOpen?: boolean; -}; - -export const DeleteModal: React.FC = ({ - headline, - description, - isOpen = false, - deleteInputLabel, - serviceTypeName, - closeModal, - isLoading, - onConfirmDelete, - error, - children, - cancelButtonLabel, - confirmButtonLabel, - terminateValue = defaultDeleteModalTerminateValue, -}) => { - const { t } = useTranslation('delete-modal'); - - const close = React.useCallback(() => { - closeModal(); - }, []); - - return ( - -
- - {t('deleteModalHeadline', { - serviceType: serviceTypeName || t('deleteModalHeadlineService'), - })} - - {!!error && ( - - {t('deleteModalError', { error })} - - )} - - {t('deleteModalDescription')} - - {children} -
- - { - onConfirmDelete(); - })} - color={ODS_BUTTON_COLOR.critical} - label={t('deleteModalDeleteButton')} - /> -
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.spec.tsx b/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.spec.tsx deleted file mode 100644 index cd9df543a4e2..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/delete-modal.spec.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { vitest } from 'vitest'; -import { waitFor, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from '../../../utils/test.provider'; -import { DeleteModal, DeleteModalProps } from './delete-modal.component'; -import '@testing-library/jest-dom'; - -export const sharedProps: DeleteModalProps = { - closeModal: vitest.fn(), - onConfirmDelete: vitest.fn(), - serviceTypeName: 'serviceType', - isOpen: true, -}; - -// waiting for ODS FIX in (_a = this.modalDialog)?.close in ods-modal2.js -describe('Delete Modal component', () => { - it('waiting for ods fix', async () => { - expect(true).toBeTruthy(); - }); - it('renders correctly', async () => { - render(); - await waitFor(() => { - expect( - screen.getByTestId('manager-delete-modal-description'), - ).toBeInTheDocument(); - expect( - screen.getByTestId('manager-delete-modal-cancel'), - ).toBeInTheDocument(); - expect( - screen.getByTestId('manager-delete-modal-confirm'), - ).toBeInTheDocument(); - }); - }); - it('renders loading modal', async () => { - const { asFragment } = render(); - await waitFor(() => { - expect(asFragment()).toMatchSnapshot(); - }); - }); - it('renders error message in modal', async () => { - const errorMessage = 'Error message'; - render(); - await waitFor(() => { - expect( - screen.getByText(errorMessage, { exact: false }), - ).toBeInTheDocument(); - }); - }); - it('clicking cancel should call closeModal', async () => { - render(); - const button = screen.getByTestId('manager-delete-modal-cancel'); - await fireEvent.click(button); - await waitFor(() => { - expect(sharedProps.closeModal).toHaveBeenCalled(); - }); - }); - it('clicking confirm should call closeModal', async () => { - render(); - const button = screen.getByTestId('manager-delete-modal-confirm'); - - await userEvent.click(button); - await waitFor(() => { - expect(sharedProps.onConfirmDelete).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/templates/delete-modal/delete-service-modal.component.tsx b/packages/manager-react-components/src/components/templates/delete-modal/delete-service-modal.component.tsx deleted file mode 100644 index fdce1e0e077c..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/delete-service-modal.component.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { - useDeleteService, - UseDeleteServiceParams, -} from '../../../hooks/services'; -import { DeleteModal, DeleteModalProps } from './delete-modal.component'; - -export type DeleteServiceModalProps = { - resourceName: string; -} & Omit & - UseDeleteServiceParams; - -export const DeleteServiceModal: React.FC = ({ - onConfirmDelete, - resourceName, - onSuccess, - onError, - mutationKey, - isLoading, - ...props -}) => { - const { terminateService, isPending, error, isError } = useDeleteService({ - onSuccess, - onError, - mutationKey, - }); - - return ( - { - onConfirmDelete?.(); - terminateService({ resourceName }); - }} - /> - ); -}; diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_en_GB.json b/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_en_GB.json deleted file mode 100644 index 46022b6810ce..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_en_GB.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "deleteModalError": "The following error has occurred: {{error}}.", - "deleteModalCancelButton": "No, dismiss", - "deleteModalDeleteButton": "Yes, cancel", - "deleteModalDescription": "Cancelling your service will irretrievably erase all related data. Do you wish to cancel your service?", - "deleteModalHeadline": "Cancel {{serviceType}}?", - "deleteModalHeadlineService": "the service" -} diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/translations.ts b/packages/manager-react-components/src/components/templates/delete-modal/translations/translations.ts deleted file mode 100644 index c6976ed27d6e..000000000000 --- a/packages/manager-react-components/src/components/templates/delete-modal/translations/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'delete-modal'); diff --git a/packages/manager-react-components/src/components/templates/error-boundary/error-boundary.component.tsx b/packages/manager-react-components/src/components/templates/error-boundary/error-boundary.component.tsx deleted file mode 100644 index d8bb779fe9bf..000000000000 --- a/packages/manager-react-components/src/components/templates/error-boundary/error-boundary.component.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useContext, useEffect } from 'react'; -import { ErrorBanner } from '../error/error.component'; -import { useRouteError } from 'react-router-dom'; -import { - ShellContext, - useRouteSynchro, -} from '@ovh-ux/manager-react-shell-client'; - -export interface ResponseAPIError { - message: string; - stack: string; - name: string; - code: string; - response?: { - headers?: { - [key: string]: string; - 'x-ovh-queryid': string; - }; - data?: { - message?: string; - }; - }; -} - -const ShellRoutingSync = () => { - useRouteSynchro(); - return null; -}; - -export interface ErrorBoundaryProps { - /** application name to redirect */ - redirectionApp: string; - /** Trigger the preloader hiding */ - isPreloaderHide?: boolean; - /** Trigger the routes sync beetween shell and the app */ - isRouteShellSync?: boolean; -} - -export const ErrorBoundary = ({ - redirectionApp, - isPreloaderHide = false, - isRouteShellSync = false, -}: ErrorBoundaryProps) => { - const error = useRouteError() as ResponseAPIError; - const shell = useContext(ShellContext)?.shell; - - const navigateToHomePage = () => { - shell?.navigation.navigateTo(redirectionApp, '', {}); - }; - - const reloadPage = () => { - shell?.navigation.reload(); - }; - - useEffect(() => { - if (isPreloaderHide) { - shell?.ux.hidePreloader(); - } - }, [isPreloaderHide]); - - return ( - <> - - {isRouteShellSync && } - - ); -}; diff --git a/packages/manager-react-components/src/components/templates/error/error.component.tsx b/packages/manager-react-components/src/components/templates/error/error.component.tsx deleted file mode 100644 index b8561220a1d3..000000000000 --- a/packages/manager-react-components/src/components/templates/error/error.component.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { OdsText, OdsButton, OdsMessage } from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_VARIANT, - ODS_MESSAGE_COLOR, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { PageType, ShellContext } from '@ovh-ux/manager-react-shell-client'; - -import { ErrorMessage, TRACKING_LABELS, ErrorBannerProps } from './error.types'; -import './translations/translations'; -import ErrorImg from '../../../../public/assets/error-banner-oops.png'; - -function getTrackingTypology(error: ErrorMessage) { - if (error?.status && Math.floor(error.status / 100) === 4) { - return [401, 403].includes(error.status) - ? TRACKING_LABELS.UNAUTHORIZED - : TRACKING_LABELS.SERVICE_NOT_FOUND; - } - return TRACKING_LABELS.PAGE_LOAD; -} - -export const ErrorBanner = ({ - error, - onRedirectHome, - onReloadPage, - labelTracking, - ...rest -}: ErrorBannerProps) => { - const { t } = useTranslation('error'); - const { shell } = React.useContext(ShellContext); - const env = shell?.environment?.getEnvironment(); - - React.useEffect(() => { - env?.then((response) => { - const { applicationName } = response; - const name = `errors::${getTrackingTypology(error)}::${applicationName}`; - shell?.tracking?.trackPage({ - name, - level2: '81', - type: 'navigation', - page_category: PageType.bannerError, - }); - }); - }, []); - - return ( -
- OOPS -
- - {t('manager_error_page_title')} - -
-
- -
-
{t('manager_error_page_default')}
-
- {error?.data?.message && {error.data.message}} -
-
- {error?.headers?.['x-ovh-queryid'] && ( -

- {t('manager_error_page_detail_code')} - {error.headers['x-ovh-queryid']} -

- )} -
-
-
-
-
-
- -
-
- -
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/templates/error/error.scss b/packages/manager-react-components/src/components/templates/error/error.scss deleted file mode 100644 index 27e86b5453ad..000000000000 --- a/packages/manager-react-components/src/components/templates/error/error.scss +++ /dev/null @@ -1,4 +0,0 @@ -.error-template-actions::part(button) { - width: 100%; - margin-bottom: 10px; -} diff --git a/packages/manager-react-components/src/components/templates/error/error.spec.tsx b/packages/manager-react-components/src/components/templates/error/error.spec.tsx deleted file mode 100644 index e6898eb3cca3..000000000000 --- a/packages/manager-react-components/src/components/templates/error/error.spec.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { render } from '../../../utils/test.provider'; -import { ErrorBanner } from './error.component'; -import userEvent from '@testing-library/user-event'; -import tradFr from './translations/Messages_fr_FR.json'; -import { ErrorObject, ErrorBannerProps } from './error.types'; -import { vitest } from 'vitest'; - -export const defaultProps: ErrorBannerProps = { - error: { - headers: { 'x-ovh-queryid': '123456789' }, - data: { message: "Votre requête n'a pas abouti" }, - status: 404, - }, -}; - -const setupSpecTest = async ( - customProps?: Partial, - error?: ErrorObject, -) => - waitFor(() => - render( - , - ), - ); - -describe('specs:error.component', () => { - it('renders without error', async () => { - const screen = await setupSpecTest(); - const img = screen.getByAltText('OOPS'); - const title = screen.queryByText(tradFr.manager_error_page_title); - const errorMessage = screen.queryByText(tradFr.manager_error_page_default); - - expect(img).not.toBeNull(); - expect(title).toBeTruthy(); - expect(errorMessage).toBeTruthy(); - }); - - describe('contents', () => { - it('displays error details if present', async () => { - const customError: ErrorObject = { - status: 500, - data: { message: 'Custom data message' }, - headers: { 'x-ovh-queryid': '123456789' }, - }; - - const screen = await setupSpecTest( - { onRedirectHome: vitest.fn() }, - customError, - ); - - const strongMessage = screen.queryByText('Custom data message'); - - expect(strongMessage).toBeTruthy(); - }); - - it('calls onRedirectHome when home button is clicked', async () => { - const onRedirectHomeMock = vitest.fn(); - const { getByTestId } = await setupSpecTest({ - onRedirectHome: onRedirectHomeMock, - }); - - const homeButton = getByTestId('error-template-action-home'); - await userEvent.click(homeButton); - expect(onRedirectHomeMock).toHaveBeenCalled(); - }); - - it('calls onReloadPage when reload button is clicked', async () => { - const onReloadPageMock = vitest.fn(); - const { getByTestId } = await setupSpecTest({ - onReloadPage: onReloadPageMock, - }); - - const reloadButton = getByTestId('error-template-action-reload'); - await userEvent.click(reloadButton); - expect(onReloadPageMock).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/templates/error/error.types.tsx b/packages/manager-react-components/src/components/templates/error/error.types.tsx deleted file mode 100644 index da984edb61ea..000000000000 --- a/packages/manager-react-components/src/components/templates/error/error.types.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -export interface ErrorObject { - status: number; - data: any; - headers: any; -} -export interface ErrorMessage { - message?: string; - status?: number; - detail?: any; -} -export const TRACKING_LABELS = { - SERVICE_NOT_FOUND: 'service_not_found', - UNAUTHORIZED: 'unauthorized', - PAGE_LOAD: 'error_during_page_loading', -}; -export interface ErrorBannerProps extends React.HTMLProps { - error: { - status?: number; - data?: any; - headers?: any; - }; - onRedirectHome?: () => void; - onReloadPage?: () => void; - labelTracking?: string; -} diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_fr_FR.json b/packages/manager-react-components/src/components/templates/error/translations/Messages_fr_FR.json deleted file mode 100644 index 2c575c63588e..000000000000 --- a/packages/manager-react-components/src/components/templates/error/translations/Messages_fr_FR.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "manager_error_page_title": "Oops …!", - "manager_error_page_button_cancel": "Annuler", - "manager_error_page_detail_code": "Code d'erreur : ", - "manager_error_page_action_reload_label": "Réessayer", - "manager_error_page_action_home_label": "Retour à la page d'accueil", - "manager_error_page_default": "Une erreur est survenue lors du chargement de la page." -} diff --git a/packages/manager-react-components/src/components/templates/error/translations/translations.ts b/packages/manager-react-components/src/components/templates/error/translations/translations.ts deleted file mode 100644 index b4092a871869..000000000000 --- a/packages/manager-react-components/src/components/templates/error/translations/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'error'); diff --git a/packages/manager-react-components/src/components/templates/index.ts b/packages/manager-react-components/src/components/templates/index.ts deleted file mode 100644 index 37709768486e..000000000000 --- a/packages/manager-react-components/src/components/templates/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './base/base.component'; -export * from './error/error.component'; -export * from './error/error.types'; -export * from './error-boundary/error-boundary.component'; -export * from './onboarding/onboarding.component'; -export * from './layout/layout.component'; -export * from './delete-modal/delete-modal.component'; -export * from './delete-modal/delete-service-modal.component'; -export * from './update-name-modal/update-name-modal.component'; -export * from './update-name-modal/update-iam-name-modal.component'; diff --git a/packages/manager-react-components/src/components/templates/layout/layout.component.tsx b/packages/manager-react-components/src/components/templates/layout/layout.component.tsx deleted file mode 100644 index ad5c4f4580f7..000000000000 --- a/packages/manager-react-components/src/components/templates/layout/layout.component.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -/** - * @deprecated Use BaseLayout component instead - */ -export const PageLayout: React.FC = ({ children }) => ( -
{children}
-); - -export const DashboardGridLayout: React.FC = ({ - children, -}) => ( -
-
- {children} -
-
-); diff --git a/packages/manager-react-components/src/components/templates/onboarding/onboarding.component.tsx b/packages/manager-react-components/src/components/templates/onboarding/onboarding.component.tsx deleted file mode 100644 index dfaaac0dbda8..000000000000 --- a/packages/manager-react-components/src/components/templates/onboarding/onboarding.component.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { - ODS_BUTTON_ICON_ALIGNMENT, - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { OdsButton, OdsText } from '@ovhcloud/ods-components/react'; -import { ManagerButton } from '../../ManagerButton/ManagerButton'; -import React, { PropsWithChildren } from 'react'; - -import placeholderSrc from '../../../../public/assets/placeholder.png'; - -type OnboardingLayoutButtonProps = { - orderButtonLabel?: string; - orderHref?: string; - onOrderButtonClick?: () => void; - isActionDisabled?: boolean; - orderIam?: { - urn: string; - iamActions: string[]; - displayTooltip?: boolean; - }; - moreInfoHref?: string; - moreInfoButtonIcon?: ODS_ICON_NAME; - moreInfoButtonLabel?: string; - /** - * @deprecated use onMoreInfoButtonClick - */ - onmoreInfoButtonClick?: () => void; - onMoreInfoButtonClick?: () => void; - isMoreInfoButtonDisabled?: boolean; -}; - -export type OnboardingLayoutProps = OnboardingLayoutButtonProps & - PropsWithChildren<{ - hideHeadingSection?: boolean; - title: string; - description?: React.ReactNode; - img?: React.ComponentProps<'img'>; - }>; - -const OnboardingLayoutButton: React.FC = ({ - orderButtonLabel, - orderHref, - onOrderButtonClick, - isActionDisabled, - orderIam, - moreInfoHref, - moreInfoButtonLabel, - moreInfoButtonIcon = ODS_ICON_NAME.externalLink, - /** - * @deprecated use onMoreInfoButtonClick - */ - onmoreInfoButtonClick, - onMoreInfoButtonClick, - isMoreInfoButtonDisabled, -}) => { - if (!orderButtonLabel && !moreInfoButtonLabel) { - return <>; - } - return ( -
- {orderButtonLabel && (onOrderButtonClick || orderHref) && ( - { - onOrderButtonClick?.(); - if (orderHref) { - window.open(orderHref, '_blank'); - } - }} - label={orderButtonLabel} - isDisabled={isActionDisabled} - {...(orderIam || {})} - /> - )} - {moreInfoButtonLabel && - (onmoreInfoButtonClick || onMoreInfoButtonClick || moreInfoHref) && ( - { - if (!isMoreInfoButtonDisabled) { - // TODO: to delete on next major version - onmoreInfoButtonClick?.(); - onMoreInfoButtonClick?.(); - if (moreInfoHref) { - window.open(moreInfoHref, '_blank'); - } - } - }} - label={moreInfoButtonLabel} - icon={moreInfoButtonIcon} - iconAlignment={ODS_BUTTON_ICON_ALIGNMENT.right} - isDisabled={isMoreInfoButtonDisabled} - /> - )} -
- ); -}; - -export const OnboardingLayout: React.FC = ({ - hideHeadingSection, - title, - description, - orderButtonLabel, - orderHref, - isActionDisabled, - orderIam, - onOrderButtonClick, - moreInfoHref, - moreInfoButtonLabel, - moreInfoButtonIcon, - isMoreInfoButtonDisabled, - /** - * @deprecated use onMoreInfoButtonClick - */ - onmoreInfoButtonClick, - onMoreInfoButtonClick, - img = {}, - children, -}) => { - const { className: imgClassName, alt: altText, ...imgProps } = img; - return ( -
- {!hideHeadingSection && ( -
- {(img?.src || placeholderSrc) && ( -
- {altText -
- )} - - {title} - - {description} - -
- )} - {children && ( - - )} -
- ); -}; diff --git a/packages/manager-react-components/src/components/templates/onboarding/onboarding.spec.tsx b/packages/manager-react-components/src/components/templates/onboarding/onboarding.spec.tsx deleted file mode 100644 index a46c2dfdd7d6..000000000000 --- a/packages/manager-react-components/src/components/templates/onboarding/onboarding.spec.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React from 'react'; -import { vi, vitest } from 'vitest'; -import { waitFor, fireEvent } from '@testing-library/react'; -import { render } from '../../../utils/test.provider'; -import { - OnboardingLayout, - OnboardingLayoutProps, -} from './onboarding.component'; -import { OdsText } from '@ovhcloud/ods-components/react'; -import placeholderSrc from './../../../../public/assets/placeholder.png'; -import { Card } from '../../navigation/card/card.component'; -import { useAuthorizationIam } from '../../../hooks/iam'; -import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; - -const setupSpecTest = async (props: OnboardingLayoutProps) => - waitFor(() => render()); -vitest.mock('../../../hooks/iam'); -const mockedHook = - useAuthorizationIam as unknown as jest.Mock; -const customTitle = 'onboarding title'; -const imgAltText = 'img alt text'; -const descriptionText = 'description text'; -const orderBtnLabel = 'Order Now'; -const infoBtnLabel = 'more info'; -const children = ( - <> - - - - -); - -describe('specs:onboarding', () => { - describe('default content', () => { - it('displays default content', async () => { - const screen = await setupSpecTest({ title: customTitle }); - - expect(screen.getByText(customTitle)).toBeVisible(); - expect(screen.getByAltText('placeholder image')).toBeVisible(); - }); - }); - - describe('additional contents', () => { - it('displays description correctly', async () => { - const screen = await setupSpecTest({ - title: customTitle, - description: ( - - {descriptionText} - - ), - }); - - expect(screen.getByText(descriptionText)).toBeVisible(); - }); - - it('displays img correctly', async () => { - const screen = await setupSpecTest({ - title: customTitle, - img: { src: placeholderSrc, alt: imgAltText }, - }); - - expect(screen.getByAltText(imgAltText)).toBeVisible(); - }); - - it('disable order button with false value for useAuthorizationIam', async () => { - mockedHook.mockReturnValue({ - isAuthorized: false, - isLoading: true, - isFetched: true, - }); - - const screen = await setupSpecTest({ - title: customTitle, - orderHref: 'https://example.com/order', - orderButtonLabel: orderBtnLabel, - orderIam: { - urn: 'urn:v1:eu:resource:vrackServices:vrs-bby-zkm-3a9-tlk', - iamActions: ['vrackServices:apiovh:resource/edit'], - }, - }); - - const orderButton = screen.container.querySelector( - `[label="${orderBtnLabel}"]`, - ); - expect(orderButton).toBeVisible(); - expect(orderButton).toHaveAttribute('is-disabled', 'true'); - }); - - it('displays order button correctly', async () => { - const onOrderButtonClick = vi.fn(); - const screen = await setupSpecTest({ - title: customTitle, - orderHref: 'https://example.com/order', - orderButtonLabel: orderBtnLabel, - onOrderButtonClick, - }); - - const orderButton = screen.container.querySelector( - `[label="${orderBtnLabel}"]`, - ); - expect(orderButton).toBeVisible(); - - await waitFor(() => fireEvent.click(orderButton)); - expect(onOrderButtonClick).toHaveBeenCalledTimes(1); - }); - - it('displays more info button correctly', async () => { - const onMoreInfoButtonClick = vi.fn(); - const screen = await setupSpecTest({ - title: 'title', - moreInfoHref: 'https://example.com/order', - moreInfoButtonLabel: infoBtnLabel, - onMoreInfoButtonClick, - }); - - const moreInfoButton = screen.container.querySelector( - `[label="${infoBtnLabel}"]`, - ); - expect(moreInfoButton).toBeVisible(); - - await waitFor(() => fireEvent.click(moreInfoButton)); - expect(onMoreInfoButtonClick).toHaveBeenCalledTimes(1); - }); - - it('disable buttons', async () => { - const screen = await setupSpecTest({ - title: 'title', - moreInfoHref: 'https://example.com/order', - moreInfoButtonLabel: infoBtnLabel, - orderButtonLabel: orderBtnLabel, - onOrderButtonClick: vi.fn(), - isMoreInfoButtonDisabled: true, - isActionDisabled: true, - }); - - const orderButton = screen.container.querySelector( - `[label="${infoBtnLabel}"]`, - ); - const moreInfoButton = screen.container.querySelector( - `[label="${orderBtnLabel}"]`, - ); - expect(orderButton).toHaveAttribute('is-disabled', 'true'); - expect(moreInfoButton).toHaveAttribute('is-disabled', 'true'); - }); - - it('disable buttons', async () => { - const screen = await setupSpecTest({ - title: 'title', - moreInfoHref: 'https://example.com/order', - moreInfoButtonLabel: infoBtnLabel, - orderButtonLabel: orderBtnLabel, - onOrderButtonClick: vi.fn(), - isMoreInfoButtonDisabled: true, - isActionDisabled: true, - }); - - const orderButton = screen.container.querySelector( - `[label="${infoBtnLabel}"]`, - ); - const moreInfoButton = screen.container.querySelector( - `[label="${orderBtnLabel}"]`, - ); - expect(orderButton).toHaveAttribute('is-disabled', 'true'); - expect(moreInfoButton).toHaveAttribute('is-disabled', 'true'); - }); - - it('displays children correctly', async () => { - const screen = await waitFor(() => - render( - {children}, - ), - ); - - const card = screen.getByText('Test Onboarding 1'); - expect(card.closest('aside')).toHaveClass( - 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 xs:pt-10 sm:pt-20', - ); - }); - }); -}); diff --git a/packages/manager-react-components/src/components/templates/update-name-modal/translations/translations.ts b/packages/manager-react-components/src/components/templates/update-name-modal/translations/translations.ts deleted file mode 100644 index 4dd704e9d8e0..000000000000 --- a/packages/manager-react-components/src/components/templates/update-name-modal/translations/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'update-name-modal'); diff --git a/packages/manager-react-components/src/components/templates/update-name-modal/update-iam-name-modal.component.tsx b/packages/manager-react-components/src/components/templates/update-name-modal/update-iam-name-modal.component.tsx deleted file mode 100644 index 745525341216..000000000000 --- a/packages/manager-react-components/src/components/templates/update-name-modal/update-iam-name-modal.component.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { - UseUpdateServiceDisplayNameParams, - useUpdateServiceDisplayName, -} from '../../../hooks/services'; -import { - UpdateNameModal, - UpdateNameModalProps, -} from './update-name-modal.component'; - -export type UpdateIamNameModalProps = { - resourceName: string; - onConfirm?: () => void; -} & Omit & - UseUpdateServiceDisplayNameParams; - -export const UpdateIamNameModal: React.FC = ({ - onConfirm, - resourceName, - onSuccess, - onError, - mutationKey, - isLoading, - ...props -}) => { - const { - updateDisplayName, - isPending, - error, - isError, - } = useUpdateServiceDisplayName({ onSuccess, onError, mutationKey }); - - return ( - { - onConfirm?.(); - updateDisplayName({ resourceName, displayName }); - }} - /> - ); -}; diff --git a/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.component.tsx b/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.component.tsx deleted file mode 100644 index c95cc6d5d3cf..000000000000 --- a/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.component.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { - OdsText, - OdsInput, - OdsMessage, - OdsModal, - OdsButton, - OdsFormField, -} from '@ovhcloud/ods-components/react'; -import { - ODS_BUTTON_VARIANT, - ODS_INPUT_TYPE, - ODS_MESSAGE_COLOR, - ODS_TEXT_PRESET, -} from '@ovhcloud/ods-components'; -import { handleClick } from '../../../utils/click-utils'; -import './translations/translations'; - -export type UpdateNameModalProps = { - headline: string; - description?: string; - inputLabel: string; - defaultValue?: string; - closeModal: () => void; - updateDisplayName: (newDisplayName: string) => void; - isLoading?: boolean; - error?: string; - cancelButtonLabel?: string; - confirmButtonLabel?: string; - pattern?: string; - patternMessage?: string; - isOpen?: boolean; -}; - -export const UpdateNameModal: React.FC = ({ - headline, - description, - inputLabel, - defaultValue, - closeModal, - isLoading, - updateDisplayName, - error, - cancelButtonLabel, - confirmButtonLabel, - pattern, - patternMessage, - isOpen = false, -}) => { - const { t } = useTranslation('update-name-modal'); - const [displayName, setDisplayName] = React.useState(defaultValue); - const [isPatternError, setIsPatternError] = React.useState(false); - - React.useEffect(() => { - setDisplayName(defaultValue); - }, [defaultValue]); - - React.useEffect(() => { - const regex = new RegExp(pattern); - setIsPatternError(!displayName?.match(regex)); - }, [displayName, pattern]); - - return ( - -
- {headline} - {!!error && ( - - {t('updateModalError', { error })} - - )} - {description && ( - {description} - )} - - - setDisplayName(e.detail.value as string)} - /> - {patternMessage && ( - - {patternMessage} - - )} - -
- - updateDisplayName(displayName))} - label={confirmButtonLabel || t('updateModalConfirmButton')} - /> -
-
-
- ); -}; diff --git a/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.scss b/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.scss deleted file mode 100644 index 743f4ace80fc..000000000000 --- a/packages/manager-react-components/src/components/templates/update-name-modal/update-name-modal.scss +++ /dev/null @@ -1,24 +0,0 @@ -.update-name-headline::part(text) { - margin-top: 0px; - margin-bottom: 0; -} - -.update-name-description::part(text) { - margin-bottom: 0; -} - -.update-name-input-label::part(text) { - margin-top: 5px; - margin-bottom: 10px; -} - -.update-name-modal-pattern-message::part(text) { - margin-top: 0px; - margin-bottom: 10px; -} - -.update-name-modal-pattern-message.error::part(text) { - margin-top: 0px; - margin-bottom: 10px; - color: var(--ods-color-critical-400); -} diff --git a/packages/manager-react-components/src/components/typography/index.ts b/packages/manager-react-components/src/components/typography/index.ts deleted file mode 100644 index 8b26cf95d527..000000000000 --- a/packages/manager-react-components/src/components/typography/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../navigation/card/card.component'; -export * from './title/title.component'; -export * from './links/links.component'; diff --git a/packages/manager-react-components/src/components/typography/links/links.component.tsx b/packages/manager-react-components/src/components/typography/links/links.component.tsx deleted file mode 100644 index 762563050fce..000000000000 --- a/packages/manager-react-components/src/components/typography/links/links.component.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { - ODS_ICON_NAME, - ODS_LINK_COLOR, - ODS_LINK_ICON_ALIGNMENT, -} from '@ovhcloud/ods-components'; -import { OdsLink } from '@ovhcloud/ods-components/react'; - -export enum LinkType { - back = 'back', - next = 'next', - external = 'external', -} -export enum IconLinkAlignmentType { - left = 'left', - right = 'right', -} - -export interface LinksProps { - id?: string; - className?: string; - color?: ODS_LINK_COLOR; - download?: string; - label?: string; - children?: string; - href?: string; - rel?: string; - target?: string; - iconAlignment?: IconLinkAlignmentType; - type?: LinkType; - onClickReturn?: () => void; - isDisabled?: boolean; -} - -export const Links: React.FC = ({ - children, - label, - onClickReturn, - type, - href, - color = ODS_LINK_COLOR.primary, - iconAlignment, - className = '', - ...props -}) => ( - -); - -export default Links; diff --git a/packages/manager-react-components/src/components/typography/links/links.spec.tsx b/packages/manager-react-components/src/components/typography/links/links.spec.tsx deleted file mode 100644 index f879aa4aecf8..000000000000 --- a/packages/manager-react-components/src/components/typography/links/links.spec.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { Links, LinkType } from './links.component'; -import { render } from '../../../utils/test.provider'; - -describe('Links component', () => { - it('renders a back link correctly', () => { - const props = { - label: 'Back to the list', - href: 'https://www.example.com', - type: LinkType.back, - }; - const { container } = render(); - const linkElement = container.querySelector('[label="Back to the list"]'); - expect(linkElement).toBeInTheDocument(); - }); - - it('renders a next link correctly', () => { - const props = { - label: 'Next Page', - href: 'https://www.example.com', - type: LinkType.next, - }; - const { container } = render(); - const linkElement = container.querySelector('[label="Next Page"]'); - expect(linkElement).toBeInTheDocument(); - }); - it('renders a external link correctly with label as prop', () => { - const props = { - href: 'https://www.ovhcloud.com/', - target: '_blank', - label: 'External Page', - type: LinkType.external, - }; - - const { container } = render(); - const linkElement = container.querySelector('[label="External Page"]'); - - expect(linkElement).toBeInTheDocument(); - }); - - it('renders a external link correctly with label as children', () => { - const props = { - href: 'https://www.ovhcloud.com/', - target: '_blank', - type: LinkType.external, - }; - - const { container } = render(External Page); - const linkElement = container.querySelector('[label="External Page"]'); - - expect(linkElement).toBeInTheDocument(); - }); -}); - -it('renders a link correctly with an id', () => { - const props = { - href: 'https://www.ovhcloud.com/', - label: 'tooltip ready link', - id: 'trigger-id', - }; - - const { container } = render(); - const linkElement = container.querySelector('#trigger-id'); - - expect(linkElement).toBeInTheDocument(); -}); diff --git a/packages/manager-react-components/src/components/typography/title/title.component.tsx b/packages/manager-react-components/src/components/typography/title/title.component.tsx deleted file mode 100644 index 4aed5a780f7a..000000000000 --- a/packages/manager-react-components/src/components/typography/title/title.component.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -/** - * @deprecated Use `Text` component from `@ovhcloud/ods-react` with the preset `heading-1` instead. - */ -export const Title: React.FC< - React.PropsWithChildren<{ - className?: string; - }> -> = ({ children, className = '' }) => { - return ( -
- {children} -
- ); -}; - -/** - * @deprecated Use `Text` component from `@ovhcloud/ods-react` with the preset `heading-3` instead. - */ -export const Subtitle: React.FC< - React.PropsWithChildren<{ - className?: string; - }> -> = ({ children, className = '' }) => { - return ( -
- {children} -
- ); -}; - -export default Title; diff --git a/packages/manager-react-components/src/enumTypes/playwright.ts b/packages/manager-react-components/src/enumTypes/playwright.ts deleted file mode 100644 index 2a2a1989655c..000000000000 --- a/packages/manager-react-components/src/enumTypes/playwright.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @deprecated Handler type will be removed in @ovh-ux/manager-react-components v3 - */ -export type Handler = { - url: string; - response?: T; - headers?: HeadersInit; - statusText?: string; - type?: ResponseType; - responseText?: string; - delay?: number; - method?: - | 'get' - | 'post' - | 'put' - | 'delete' - | 'all' - | 'head' - | 'options' - | 'patch'; - status?: number; - api?: 'v2' | 'v6' | 'aapi' | 'ws'; - baseUrl?: string; - disabled?: boolean; - once?: boolean; -}; diff --git a/packages/manager-react-components/src/hooks/date/index.ts b/packages/manager-react-components/src/hooks/date/index.ts deleted file mode 100644 index 106587ff017a..000000000000 --- a/packages/manager-react-components/src/hooks/date/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useDateFnsLocale'; -export * from './useFormatDate'; -export * from './useFormattedDate'; diff --git a/packages/manager-react-components/src/hooks/date/useDateFnsLocale.spec.ts b/packages/manager-react-components/src/hooks/date/useDateFnsLocale.spec.ts deleted file mode 100644 index 6fc31a4dcae8..000000000000 --- a/packages/manager-react-components/src/hooks/date/useDateFnsLocale.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { vi } from 'vitest'; -import { renderHook } from '@testing-library/react'; -import { useTranslation } from 'react-i18next'; -import { getDateFnsLocale } from '@ovh-ux/manager-core-utils'; -import { useDateFnsLocale } from './useDateFnsLocale'; - -vi.mock('@ovh-ux/manager-core-utils', () => ({ - getDateFnsLocale: vi.fn(), -})); - -vi.mock('react-i18next', () => ({ - useTranslation: vi.fn(), -})); - -vi.mock('date-fns/locale', () => ({ - enGB: 'enGB', - fr: 'fr', - frCA: 'frCA', - de: 'de', - es: 'es', - it: 'it', - pl: 'pl', - pt: 'pt', -})); - -const testCases = [ - { languageSource: 'en_GB', localeExpected: 'enGB' }, - { languageSource: 'fr_FR', localeExpected: 'fr' }, - { languageSource: 'fr_CA', localeExpected: 'frCA' }, - { languageSource: 'de_DE', localeExpected: 'de' }, - { languageSource: 'es_ES', localeExpected: 'es' }, - { languageSource: 'it_IT', localeExpected: 'it' }, - { languageSource: 'pl_PL', localeExpected: 'pl' }, - { languageSource: 'pt_PT', localeExpected: 'pt' }, - { languageSource: 'unknown', localeExpected: 'enGB' }, - { languageSource: '', localeExpected: 'enGB' }, - { languageSource: null, localeExpected: 'enGB' }, - { languageSource: undefined, localeExpected: 'enGB' }, -]; - -describe('useDateFnsLocale', () => { - it.each(testCases)( - 'should return the correct locale based on the i18n language', - ({ languageSource, localeExpected }) => { - (useTranslation as jest.Mock).mockReturnValue({ - i18n: { language: languageSource }, - }); - (getDateFnsLocale as jest.Mock).mockReturnValue(localeExpected); - - const { result } = renderHook(() => useDateFnsLocale()); - - expect(result.current).toBe(localeExpected); - }, - ); -}); diff --git a/packages/manager-react-components/src/hooks/date/useDateFnsLocale.ts b/packages/manager-react-components/src/hooks/date/useDateFnsLocale.ts deleted file mode 100644 index 127b325cd5b8..000000000000 --- a/packages/manager-react-components/src/hooks/date/useDateFnsLocale.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getDateFnsLocale } from '@ovh-ux/manager-core-utils'; -import { enGB, fr, frCA, de, es, it, pl, pt } from 'date-fns/locale'; -import { useTranslation } from 'react-i18next'; - -const localeMap = { - enGB, - fr, - frCA, - de, - es, - it, - pl, - pt, -} as const; - -export const useDateFnsLocale = () => { - const { i18n } = useTranslation(); - const language = getDateFnsLocale(i18n?.language); - return localeMap[language] || enGB; -}; diff --git a/packages/manager-react-components/src/hooks/date/useFormatDate.spec.ts b/packages/manager-react-components/src/hooks/date/useFormatDate.spec.ts deleted file mode 100644 index 8eae922edc2a..000000000000 --- a/packages/manager-react-components/src/hooks/date/useFormatDate.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { vitest } from 'vitest'; -import { DEFAULT_UNKNOWN_DATE_LABEL, useFormatDate } from './useFormatDate'; - -vitest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - i18n: { - language: 'fr_FR', - }, - }), -})); - -describe('useFormatDate', () => { - it.each([ - { - case: 'label for no date', - input: 'invalid', - dateString: undefined, - format: undefined, - expected: DEFAULT_UNKNOWN_DATE_LABEL, - }, - { - case: 'label for no date', - input: 'empty', - dateString: '', - format: undefined, - expected: DEFAULT_UNKNOWN_DATE_LABEL, - }, - { - case: 'a valid date', - input: 'null', - dateString: null, - format: undefined, - expected: 'N/A', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: undefined, - expected: '14 sept. 2024', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-10-14T09:21:21.943Z', - format: 'PP', - expected: '14 oct. 2024', - }, - { - case: 'a valid date with non-abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: 'PPP', - expected: '14 septembre 2024', - }, - { - case: 'a valid date with compact format', - input: 'valid and format is compact', - dateString: '2024-06-14T09:21:21.943Z', - format: 'P', - expected: '14/06/2024', - }, - { - case: 'a valid date with compact format and time format (CEST)', - input: 'valid and format is compact with time', - dateString: '2024-06-14T09:21:21.943Z', - format: 'Pp', - expected: '14/06/2024, 09:21', - }, - { - case: 'a valid date with compact format and time format (CET)', - input: 'valid and format is compact with time', - dateString: '2024-01-14T09:21:21.943Z', - format: 'Pp', - expected: '14/01/2024, 09:21', - }, - { - case: 'a valid date with display format and time format (CET)', - input: 'valid and format is compact with time', - dateString: '2024-01-14T09:21:21.943Z', - format: 'PPp', - expected: '14 janv. 2024, 09:21', - }, - { - case: 'a valid date with full display format and time format (CET)', - input: 'valid and format is compact with time', - dateString: '2024-01-14T09:21:21.943Z', - format: 'PPPpp', - expected: '14 janvier 2024 à 09:21:21', - }, - ])( - 'displays %s if the date is %s', - async ({ dateString, format, expected }) => { - const { result } = renderHook(() => useFormatDate()); - expect(result.current({ date: dateString, format })).toBe(expected); - }, - ); -}); diff --git a/packages/manager-react-components/src/hooks/date/useFormatDate.ts b/packages/manager-react-components/src/hooks/date/useFormatDate.ts deleted file mode 100644 index 42ea792a768d..000000000000 --- a/packages/manager-react-components/src/hooks/date/useFormatDate.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useCallback } from 'react'; -import { format as formatDateFns, isValid } from 'date-fns'; -import { useDateFnsLocale } from './useDateFnsLocale'; - -export const DEFAULT_UNKNOWN_DATE_LABEL = 'N/A'; - -/** - * Format options for date strings - * - * @param P - Long localized date (e.g. 04/29/1453) - * @param PP - Long localized date with abbreviated month (e.g. Apr 29, 1453) - * @param PPP - Long localized date with full month (e.g. April 29th, 1453) - * @param PPPP - Full localized date with day of week (e.g. Friday, April 29th, 1453) - * @param p - Long localized time (e.g. 12:00 AM) - * @param pp - Long localized time with seconds (e.g. 12:00:00 AM) - * @param ppp - Long localized time with timezone (e.g. 12:00:00 AM GMT+2) - * @param pppp - Long localized time with full timezone (e.g. 12:00:00 AM GMT+02:00) - * @param Pp - Combined date and time (e.g. 04/29/1453, 12:00 AM) - * @param PPpp - Combined date and time with abbreviated month (e.g. Apr 29, 1453, 12:00:00 AM) - * @param PPPppp - Combined date and time with full month (e.g. April 29th, 1453 at ...) - * @param PPPPpppp - Full combined date and time (e.g. Friday, April 29th, 1453 at ...) - * - * @see https://date-fns.org/v4.1.0/docs/format - */ -export type FormatDateOptions = { - date?: string | Date; - format?: string; // See : https://date-fns.org/v4.1.0/docs/format - invalidDateDisplayLabel?: string; -}; - -export const useFormatDate = ({ - invalidDateDisplayLabel = DEFAULT_UNKNOWN_DATE_LABEL, -}: { invalidDateDisplayLabel?: string } = {}) => { - const locale = useDateFnsLocale(); - - const formatDate = useCallback( - ({ date, format = 'PP' }: FormatDateOptions): string => { - const parsedDate = typeof date === 'string' ? new Date(date) : date; - - if (!parsedDate || !isValid(parsedDate)) { - return invalidDateDisplayLabel; - } - - try { - return formatDateFns(parsedDate, format, { locale }); - } catch (_e) { - return invalidDateDisplayLabel; - } - }, - [locale, invalidDateDisplayLabel], - ); - - return formatDate; -}; diff --git a/packages/manager-react-components/src/hooks/date/useFormattedDate.spec.ts b/packages/manager-react-components/src/hooks/date/useFormattedDate.spec.ts deleted file mode 100644 index 6377dc696c9a..000000000000 --- a/packages/manager-react-components/src/hooks/date/useFormattedDate.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { vitest } from 'vitest'; -import { - useFormattedDate, - defaultUnknownDateLabel, - DateFormat, -} from './useFormattedDate'; - -vitest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - i18n: { - language: 'fr-FR', - }, - }), -})); - -describe('useFormattedDate', () => { - it.each([ - { - case: 'label for no date', - input: 'invalid', - dateString: undefined, - format: undefined, - expected: defaultUnknownDateLabel, - }, - { - case: 'label for no date', - input: 'empty', - dateString: '', - format: undefined, - expected: defaultUnknownDateLabel, - }, - { - case: 'a valid date', - input: 'null', - dateString: null, - format: undefined, - expected: '1 janv. 1970', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: undefined, - expected: '14 sept. 2024', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-10-14T09:21:21.943Z', - format: DateFormat.display, - expected: '14 oct. 2024', - }, - { - case: 'a valid date with non-abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: DateFormat.fullDisplay, - expected: '14 septembre 2024', - }, - { - case: 'a valid date with compact format', - input: 'valid and format is compact', - dateString: '2024-06-14T09:21:21.943Z', - format: DateFormat.compact, - expected: '14/06/2024', - }, - ])( - 'displays %s if the date is %s', - async ({ dateString, format, expected }) => { - const { result } = renderHook(() => - useFormattedDate({ dateString, format }), - ); - expect(result.current).toBe(expected); - }, - ); -}); diff --git a/packages/manager-react-components/src/hooks/date/useFormattedDate.ts b/packages/manager-react-components/src/hooks/date/useFormattedDate.ts deleted file mode 100644 index 580663181452..000000000000 --- a/packages/manager-react-components/src/hooks/date/useFormattedDate.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @deprecated Use useFormatDate with date fns format instead - */ -import { useTranslation } from 'react-i18next'; - -/** - * @deprecated Use useFormatDate with date fns format instead - */ -export const defaultUnknownDateLabel = 'N/A'; - -/** - * @deprecated Use useFormatDate with date fns format instead - */ -export enum DateFormat { - /** - * dd/MM/YYYY - */ - compact = 'compact', - /** - * dd abbreviated month YYYY - */ - display = 'display', - /** - * dd month YYYY - */ - fullDisplay = 'fullDisplay', -} - -export type FormattedDateProps = { - dateString: string; - unknownDateLabel?: string; - defaultLocale?: string; - format?: DateFormat; -}; - -/** - * @deprecated Use useFormatDate with date fns format instead - */ -export const useFormattedDate = ({ - dateString, - defaultLocale = 'FR-fr', - unknownDateLabel = defaultUnknownDateLabel, - format = DateFormat.display, -}: FormattedDateProps) => { - const { i18n } = useTranslation(); - const date = new Date(dateString); - const locale = i18n?.language?.replace('_', '-') || defaultLocale; - - if (date.toString() === 'Invalid Date') { - return unknownDateLabel; - } - - return format === DateFormat.compact - ? date.toLocaleDateString(locale) - : date.toLocaleString(locale, { - day: 'numeric', - month: format === DateFormat.fullDisplay ? 'long' : 'short', - year: 'numeric', - }); -}; diff --git a/packages/manager-react-components/src/hooks/date/useFormattedDateEnglish.spec.ts b/packages/manager-react-components/src/hooks/date/useFormattedDateEnglish.spec.ts deleted file mode 100644 index f0692fce16e5..000000000000 --- a/packages/manager-react-components/src/hooks/date/useFormattedDateEnglish.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { vitest } from 'vitest'; -import { - useFormattedDate, - defaultUnknownDateLabel, - DateFormat, -} from './useFormattedDate'; - -vitest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - i18n: { - language: 'en-GB', - }, - }), -})); - -describe('useFormattedDate', () => { - it.each([ - { - case: 'label for no date', - input: 'invalid', - dateString: undefined, - format: undefined, - expected: defaultUnknownDateLabel, - }, - { - case: 'label for no date', - input: 'empty', - dateString: '', - format: undefined, - expected: defaultUnknownDateLabel, - }, - { - case: 'a valid date', - input: 'null', - dateString: null, - format: undefined, - expected: '1 Jan 1970', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: undefined, - expected: '14 Sept 2024', - }, - { - case: 'a valid date with abbreviated month', - input: 'valid', - dateString: '2024-10-14T09:21:21.943Z', - format: DateFormat.display, - expected: '14 Oct 2024', - }, - { - case: 'a valid date with non-abbreviated month', - input: 'valid', - dateString: '2024-09-14T09:21:21.943Z', - format: DateFormat.fullDisplay, - expected: '14 September 2024', - }, - { - case: 'a valid date with compact format', - input: 'valid and format is compact', - dateString: '2024-06-14T09:21:21.943Z', - format: DateFormat.compact, - expected: '14/06/2024', - }, - ])( - 'displays %s if the date is %s', - async ({ dateString, format, expected }) => { - const { result } = renderHook(() => - useFormattedDate({ dateString, format }), - ); - expect(result.current).toBe(expected); - }, - ); -}); diff --git a/packages/manager-react-components/src/hooks/feature-availability/index.ts b/packages/manager-react-components/src/hooks/feature-availability/index.ts deleted file mode 100644 index f0c6a79a47c8..000000000000 --- a/packages/manager-react-components/src/hooks/feature-availability/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useFeatureAvailability'; -export * from './mocks/feature-availability.mock'; diff --git a/packages/manager-react-components/src/hooks/feature-availability/mocks/feature-availability.mock.ts b/packages/manager-react-components/src/hooks/feature-availability/mocks/feature-availability.mock.ts deleted file mode 100644 index 15082f065a10..000000000000 --- a/packages/manager-react-components/src/hooks/feature-availability/mocks/feature-availability.mock.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type GetFeatureAvailabilityMocksParams = { - isFeatureAvailabilityServiceKo?: boolean; - featureAvailabilityResponse?: Record; -}; - -export const featureAvailabilityError = 'Feature availability service error'; - -export const getFeatureAvailabilityMocks = ({ - isFeatureAvailabilityServiceKo, - featureAvailabilityResponse, -}: GetFeatureAvailabilityMocksParams): any[] => [ - { - url: `/feature/${Object.keys(featureAvailabilityResponse).join( - ',', - )}/availability`, - response: () => - isFeatureAvailabilityServiceKo - ? { - message: featureAvailabilityError, - } - : featureAvailabilityResponse, - status: isFeatureAvailabilityServiceKo ? 500 : 200, - method: 'get', - api: 'aapi', - }, -]; diff --git a/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.spec.tsx b/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.spec.tsx deleted file mode 100644 index e1d22b601a87..000000000000 --- a/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.spec.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { vi } from 'vitest'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { useFeatureAvailability } from './useFeatureAvailability'; - -const wrapper = ({ children }) => { - const queryClient = new QueryClient(); - return ( - {children} - ); -}; - -vi.mock('@ovh-ux/manager-core-api', () => ({ - apiClient: { - aapi: { - get: vi.fn(), - }, - }, -})); - -describe('useFeatureAvailability', () => { - it('should fetch data if no options is given', () => { - const { result } = renderHook(() => useFeatureAvailability(['test']), { - wrapper, - }); - expect(result.current?.isFetching).toBe(true); - }); - - it('should not fetch data if enabled option is false', () => { - const { result } = renderHook( - () => useFeatureAvailability(['test'], { enabled: false }), - { - wrapper, - }, - ); - expect(result.current?.isFetching).toBe(false); - }); -}); diff --git a/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.ts b/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.ts deleted file mode 100644 index 3aeb66bf76ae..000000000000 --- a/packages/manager-react-components/src/hooks/feature-availability/useFeatureAvailability.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, use useFeatureAvailability from @ovh-ux/manager-module-common-api' - */ - -import { apiClient, ApiError } from '@ovh-ux/manager-core-api'; -import { - DefinedInitialDataOptions, - useQuery, - UseQueryResult, -} from '@tanstack/react-query'; - -export type UseFeatureAvailabilityResult> = - UseQueryResult; - -export const fetchFeatureAvailabilityData = async ( - featureList: [...T], -) => { - const result = await apiClient.aapi.get( - `/feature/${featureList.join(',')}/availability`, - ); - - const features = {} as Record<(typeof featureList)[number], boolean>; - featureList.forEach((feature) => { - features[feature] = feature in result.data ? result.data[feature] : false; - }); - - return features; -}; - -export const getFeatureAvailabilityQueryKey = ( - featureList: [...T], -) => [`feature-availability-${featureList.join('-')}`]; - -/** - * @examples - * const featureList = ['billing', 'webooo', 'web:microsoft']; - * - * const { data, error, isLoading } = useFeatureAvailability(featureList); - * const isBillingAvailable = data?.billing; - * const isWebooooAvailable = data?.webooo; - * const isMicrosoftAvailable = data.['web:microsoft']; - */ -export const useFeatureAvailability = ( - featureList: [...T], - options: Partial< - DefinedInitialDataOptions> - > = {}, -): UseFeatureAvailabilityResult< - Record<(typeof featureList)[number], boolean> -> => - useQuery, ApiError>({ - ...options, - queryKey: getFeatureAvailabilityQueryKey(featureList), - queryFn: () => fetchFeatureAvailabilityData(featureList), - }); diff --git a/packages/manager-react-components/src/hooks/index.ts b/packages/manager-react-components/src/hooks/index.ts deleted file mode 100644 index 3ca8d684d9da..000000000000 --- a/packages/manager-react-components/src/hooks/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TRegion as Region } from './useProjectRegions'; - -export * from './pci-project-provider'; -export * from './breadcrumb/useBreadcrumb'; -export * from './useCatalogPrice'; -export * from './useMe'; -export * from './useProjectRegions'; -export * from './useProjectUrl'; -export * from './feature-availability'; -export * from './datagrid/useIcebergV2'; -export * from './datagrid/useIcebergV6'; -export * from './datagrid/useResourcesV6'; -export * from './services'; -export * from './tasks'; -export * from './date'; -export * from './iam'; -export * from './bytes/useBytes'; -export { useProductMaintenance } from './pci/useMaintenance'; -export { - getMacroRegion, - useTranslatedMicroRegions, - isLocalZone, -} from './region/useTranslatedMicroRegions'; - -export type TRegion = Region; diff --git a/packages/manager-react-components/src/hooks/pci-project-provider/constants.ts b/packages/manager-react-components/src/hooks/pci-project-provider/constants.ts deleted file mode 100644 index 7a62f67a18a2..000000000000 --- a/packages/manager-react-components/src/hooks/pci-project-provider/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The constants will be available in the `@ovh-ux/manager-pci-common` package. - */ - -/** - * @deprecated This constant is deprecated and will be removed in MRC V3. - */ -export const DISCOVERY_PROJECT_PLANCODE = 'project.discovery'; diff --git a/packages/manager-react-components/src/hooks/pci-project-provider/index.ts b/packages/manager-react-components/src/hooks/pci-project-provider/index.ts deleted file mode 100644 index eb94656e0f54..000000000000 --- a/packages/manager-react-components/src/hooks/pci-project-provider/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { useProjectQuota } from './useProject'; - -/** - * @deprecated This export is deprecated and will be removed in MRC V3. - */ -export { useProjectQuota }; diff --git a/packages/manager-react-components/src/hooks/pci-project-provider/publicCloudProject.interface.ts b/packages/manager-react-components/src/hooks/pci-project-provider/publicCloudProject.interface.ts deleted file mode 100644 index b767c100ae33..000000000000 --- a/packages/manager-react-components/src/hooks/pci-project-provider/publicCloudProject.interface.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type TProjectStatus = - | 'creating' - | 'deleted' - | 'deleting' - | 'ok' - | 'suspended'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type PublicCloudProject = { - access: 'full' | 'restricted'; - creationDate: string; - description?: string; - expiration?: string | null; - iam: { - displayName?: string; - id: string; - tags?: Record; - urn: string; - }; - manualQuota: boolean; - orderId: number | null; - planCode: string; - projectName: string | null; - project_id: string; - status: TProjectStatus; - unleash: boolean; -}; diff --git a/packages/manager-react-components/src/hooks/pci-project-provider/useProject.ts b/packages/manager-react-components/src/hooks/pci-project-provider/useProject.ts deleted file mode 100644 index 118b990f27a7..000000000000 --- a/packages/manager-react-components/src/hooks/pci-project-provider/useProject.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { useQuery } from '@tanstack/react-query'; - -import { v6 } from '@ovh-ux/manager-core-api'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type TProjectQuota = { - region: string; - instance: { - maxCores: number; - maxInstances: number; - maxRam: number; - usedCores: number; - usedInstances: number; - usedRAM: number; - }; - keypair: { - maxCount: number; - }; - volume: { - maxGigabytes: number; - usedGigabytes: number; - volumeCount: number; - maxVolumeCount: number; - maxBackupGigabytes: number; - usedBackupGigabytes: number; - volumeBackupCount: number; - maxVolumeBackupCount: number; - }; - network: { - maxNetworks: number; - usedNetworks: number; - maxSubnets: number; - usedSubnets: number; - maxFloatingIPs: number; - usedFloatingIPs: number; - maxGateways: number; - usedGateways: number; - }; - loadbalancer: { - maxLoadbalancers: number; - usedLoadbalancers: number; - } | null; - keymanager: { - maxSecrets: number; - usedSecrets: number; - } | null; - share: any | null; -}; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getProjectQuota = async (projectId: string) => { - const { data } = await v6.get( - `/cloud/project/${projectId}/quota`, - ); - - return data; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectQuota = (projectId: string) => - useQuery({ - queryKey: ['project', projectId, 'quota'], - queryFn: () => getProjectQuota(projectId), - }); diff --git a/packages/manager-react-components/src/hooks/pci/useAggregatedPrivateNetworks.tsx b/packages/manager-react-components/src/hooks/pci/useAggregatedPrivateNetworks.tsx deleted file mode 100644 index 226177c60bfe..000000000000 --- a/packages/manager-react-components/src/hooks/pci/useAggregatedPrivateNetworks.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { useMemo } from 'react'; -import { v6 } from '@ovh-ux/manager-core-api'; -import { useQuery } from '@tanstack/react-query'; -import { Region } from './useProjectRegions'; - -/** - * @deprecated The interface is deprecated and will be removed in MRC V3. - */ -export interface Subnet { - region: string; - id: string; -} - -/** - * @deprecated The interface is deprecated and will be removed in MRC V3. - */ -interface Network { - id: string; - name: string; - region: string; - visibility: string; - vlanId: number; -} - -/** - * @deprecated The interface is deprecated and will be removed in MRC V3. - */ -export interface AggregatedNetwork { - resources: Network[]; -} - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getAggregatedNetwork = async ( - projectId: string, -): Promise => { - const { data } = await v6.get( - `/cloud/project/${projectId}/aggregated/network`, - ); - return data; -}; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getAggregatedPrivateNetworksQueryKey = (projectId: string) => { - return ['aggregated-network', projectId]; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useAggregatedPrivateNetworks = ( - projectId: string, - customerRegions: Region[], -) => { - return useQuery({ - queryKey: getAggregatedPrivateNetworksQueryKey(projectId), - queryFn: () => getAggregatedNetwork(projectId), - enabled: customerRegions?.length > 0, - select: (data) => { - const privateNetworks = {} as Record; - const localZones = - customerRegions?.filter(({ type }) => type.includes('localzone')) || []; - data.resources?.forEach((network) => { - if ( - network.visibility === 'private' && - !localZones?.some((region) => region.name === network.region) - ) { - if (!privateNetworks[network.vlanId]) { - const { id, region, ...rest } = network; - privateNetworks[network.vlanId] = { - ...rest, - region, - subnets: [{ region, networkId: id }], - }; - } else { - const { id, region } = network; - privateNetworks[network.vlanId].subnets.push({ - region, - networkId: id, - }); - } - } - }); - return Object.values(privateNetworks); - }, - }); -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useAggregatedPrivateNetworksRegions = ( - projectId: string, - customerRegions: Region[], -) => { - const privateNetworksQuery = useAggregatedPrivateNetworks( - projectId, - customerRegions, - ); - const { data } = privateNetworksQuery; - - return { - ...privateNetworksQuery, - data: useMemo( - () => - Array.from( - new Set( - data?.reduce( - (result: string[], network: { subnets: Subnet[] }) => - result.concat(network.subnets.map(({ region }) => region)), - [], - ), - ), - ), - [data], - ), - }; -}; diff --git a/packages/manager-react-components/src/hooks/pci/useMaintenance.spec.tsx b/packages/manager-react-components/src/hooks/pci/useMaintenance.spec.tsx deleted file mode 100644 index 50c4f9be057a..000000000000 --- a/packages/manager-react-components/src/hooks/pci/useMaintenance.spec.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { - QueryClient, - QueryClientProvider, - UseQueryResult, -} from '@tanstack/react-query'; -import { vitest } from 'vitest'; -import React from 'react'; -import { renderHook } from '@testing-library/react'; -import { useProductMaintenance } from './useMaintenance'; -import * as useMigrationSteins from './useMigrationSteins'; -import * as useAggregatedPrivateNetworks from './useAggregatedPrivateNetworks'; -import * as useProjectRegions from './useProjectRegions'; -import { Stein } from './useMigrationSteins'; -import { Region } from './useProjectRegions'; -import '@testing-library/jest-dom'; - -const renderUseMaintenanceHook = () => { - const queryClient = new QueryClient(); - - const wrapper = ({ children }: React.PropsWithChildren) => ( - {children} - ); - - const { result } = renderHook(() => useProductMaintenance('projectId'), { - wrapper, - }); - - return result; -}; - -describe('useMaintenance', () => { - it('should return hasMaintenance with false value when useMigrationSteins return empty array', async () => { - vitest.spyOn(useMigrationSteins, 'useMigrationSteins').mockReturnValue({ - data: [], - } as UseQueryResult); - vitest.spyOn(useProjectRegions, 'useProjectRegions').mockReturnValue({ - data: [], - } as UseQueryResult); - vitest - .spyOn( - useAggregatedPrivateNetworks, - 'useAggregatedPrivateNetworksRegions', - ) - .mockReturnValue({ - data: [], - } as UseQueryResult); - - const result = renderUseMaintenanceHook(); - - expect(result.current.hasMaintenance).toEqual(false); - }); - - it('should return hasMaintenance with false value when useAggregatedPrivateNetworksRegions return empty array', async () => { - const mockSteins: Stein[] = [ - { - date: 'date-1', - zone: 'REGION-1', - travaux: 'travaux-1', - }, - { - date: 'date-2', - zone: 'REGION-2', - travaux: 'travaux-2', - }, - ]; - - vitest.spyOn(useMigrationSteins, 'useMigrationSteins').mockReturnValue({ - data: mockSteins, - } as UseQueryResult); - - vitest - .spyOn( - useAggregatedPrivateNetworks, - 'useAggregatedPrivateNetworksRegions', - ) - .mockReturnValue({ - data: [], - } as UseQueryResult); - - const result = renderUseMaintenanceHook(); - - expect(result.current.hasMaintenance).toEqual(false); - }); - - it('should return hasMaintenance true.', async () => { - const mockedCustomRegions: Partial[] = [ - { - name: 'RN', - type: 'RT', - status: 'RS', - continentCode: 'CC', - datacenterLocation: 'DCL', - }, - ]; - - vitest.spyOn(useProjectRegions, 'useProjectRegions').mockReturnValue({ - data: mockedCustomRegions, - } as UseQueryResult); - - const mockSteins: Stein[] = [ - { - date: 'date-1', - zone: 'REGION-1', - travaux: 'travaux-1', - }, - { - date: 'date-2', - zone: 'REGION-2', - travaux: 'travaux-2', - }, - ]; - - vitest.spyOn(useMigrationSteins, 'useMigrationSteins').mockReturnValue({ - data: mockSteins, - } as UseQueryResult); - - const mockedProjectRegions = ['REGION-1', 'REGION-2']; - - vitest - .spyOn( - useAggregatedPrivateNetworks, - 'useAggregatedPrivateNetworksRegions', - ) - .mockReturnValue({ - data: mockedProjectRegions, - } as UseQueryResult); - - const result = renderUseMaintenanceHook(); - - expect(result.current.hasMaintenance).toEqual(true); - }); -}); diff --git a/packages/manager-react-components/src/hooks/pci/useMaintenance.tsx b/packages/manager-react-components/src/hooks/pci/useMaintenance.tsx deleted file mode 100644 index c1cc829fff13..000000000000 --- a/packages/manager-react-components/src/hooks/pci/useMaintenance.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { useAggregatedPrivateNetworksRegions } from './useAggregatedPrivateNetworks'; -import { useMigrationSteins } from './useMigrationSteins'; -import { useProjectRegions } from './useProjectRegions'; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export function useProductMaintenance(projectId: string) { - const { data: customerRegions } = useProjectRegions(projectId); - const { data: steins } = useMigrationSteins(); - const { data: productRegions } = useAggregatedPrivateNetworksRegions( - projectId, - customerRegions, - ); - - const regionsMaintenance = steins?.map(({ zone }) => zone) || []; - - const isProductConcernedByMaintenance = (productRegions || []).some( - (region) => regionsMaintenance.includes(region), - ); - - return { - hasMaintenance: isProductConcernedByMaintenance, - maintenanceURL: steins?.[0]?.travaux, - }; -} diff --git a/packages/manager-react-components/src/hooks/pci/useMigrationSteins.tsx b/packages/manager-react-components/src/hooks/pci/useMigrationSteins.tsx deleted file mode 100644 index d72df6af9f77..000000000000 --- a/packages/manager-react-components/src/hooks/pci/useMigrationSteins.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { v6 } from '@ovh-ux/manager-core-api'; -import { useQuery } from '@tanstack/react-query'; - -/** - * @deprecated The interface is deprecated and will be removed in MRC V3. - */ -export interface Stein { - date: string; - zone: string; - travaux: string; -} - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getMigrationSteins = async (): Promise => { - const { data } = await v6.get('/cloud/migrationStein'); - return data; -}; - -/** - * @deprecated This constant is deprecated and will be removed in MRC V3. - */ -export const migrationSteinsQueryKey = ['migrationSteins']; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useMigrationSteins = () => - useQuery({ - queryKey: migrationSteinsQueryKey, - queryFn: () => getMigrationSteins(), - select: (data) => - [...data].sort( - (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), - ), - }); diff --git a/packages/manager-react-components/src/hooks/pci/useProjectRegions.tsx b/packages/manager-react-components/src/hooks/pci/useProjectRegions.tsx deleted file mode 100644 index af64bbee10cc..000000000000 --- a/packages/manager-react-components/src/hooks/pci/useProjectRegions.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * The hooks will be available in the `@ovh-ux/manager-pci-common` package. - */ -import { useQuery } from '@tanstack/react-query'; -import { fetchIcebergV6 } from '@ovh-ux/manager-core-api'; - -/** - * @deprecated The interface is deprecated and will be removed in MRC V3. - */ -export interface Region { - continentCode: string; - datacenterLocation: string; - name: string; - status: string; - type: string; -} - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getProjectRegionsQueryKey = (projectId: string) => [ - 'project', - projectId, - 'regions', -]; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getProjectRegions = async ( - projectId: string, -): Promise => { - const { data } = await fetchIcebergV6({ - route: `/cloud/project/${projectId}/region`, - }); - return data; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectRegions = (projectId: string) => - useQuery({ - queryKey: getProjectRegionsQueryKey(projectId), - queryFn: () => getProjectRegions(projectId), - }); diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_de_DE.json b/packages/manager-react-components/src/hooks/region/translations/Messages_de_DE.json deleted file mode 100644 index 66e9e84ccc34..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_de_DE.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Straßburg (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Straßburg", - "manager_components_region_SBG_micro": "Straßburg ({{ micro }})", - "manager_components_region_MRS": "Marseille", - "manager_components_region_MRS_micro": "Marseille ({{ micro }})", - "manager_components_region_PAR": "Paris", - "manager_components_region_PAR_micro": "Paris ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "London", - "manager_components_region_ERI_micro": "London ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburg", - "manager_components_region_LIM_micro": "Limburg ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Warschau", - "manager_components_region_WAW_micro": "Warschau ({{ micro }})", - "manager_components_region_DE": "Frankfurt", - "manager_components_region_DE_micro": "Frankfurt ({{ micro }})", - "manager_components_region_UK": "London", - "manager_components_region_UK_micro": "London ({{ micro }})", - "manager_components_region_SGP": "Singapur", - "manager_components_region_SGP_micro": "Singapur ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "Nordamerika", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "Nordamerika", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "Nordamerika", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virginia", - "manager_components_region_VA_micro": "Virginia ({{ micro }})", - "manager_components_region_continent_VA": "Nordamerika", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "Nordamerika", - "manager_components_region_US": "USA", - "manager_components_region_US_micro": "Vereinigte Staaten ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Brüssel", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Nordamerika", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Brüssel ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Prag", - "manager_components_region_PRG_micro": "Prag ({{ micro }})", - "manager_components_region_continent_PRG": "Mitteleuropa", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Westeuropa", - "manager_components_region_MIL": "Mailand", - "manager_components_region_MIL_micro": "Mailand ({{ micro }})", - "manager_components_region_continent_MIL": "Südeuropa", - "manager_components_region_ZRH": "Zürich", - "manager_components_region_ZRH_micro": "Zürich ({{ micro }})", - "manager_components_region_continent_ZRH": "Westeuropa", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "Nordamerika", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "Nordamerika", - "manager_components_region_NYC": "New York", - "manager_components_region_NYC_micro": "New York ({{ micro }})", - "manager_components_region_continent_NYC": "Nordamerika", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "Nordamerika", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "Nordamerika", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "Nordamerika", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "Nordamerika", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "Afrika", - "manager_components_region_TYO": "Tokio", - "manager_components_region_TYO_micro": "Tokio ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Naher Osten", - "manager_components_region_JKT": "Jakarta", - "manager_components_region_JKT_micro": "Jakarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxemburg", - "manager_components_region_LUX_micro": "Luxemburg ({{ micro }})", - "manager_components_region_continent_LUX": "Westeuropa", - "manager_components_region_MEX": "Mexiko", - "manager_components_region_MEX_micro": "Mexiko ({{ micro }})", - "manager_components_region_continent_MEX": "Mittelamerika", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "Südamerika", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{ micro }})", - "manager_components_region_continent_TUN": "Nordafrika", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "Nordamerika", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "Nordamerika", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "Nordamerika", - "manager_components_region_BUH": "Bukarest", - "manager_components_region_BUH_micro": "Bukarest ({{ micro }})", - "manager_components_region_continent_BUH": "Mitteleuropa", - "manager_components_region_PHL": "Philadelphia", - "manager_components_region_PHL_micro": "Philadelphia ({{ micro }})", - "manager_components_region_continent_PHL": "Nordamerika", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "Südamerika", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Nordeuropa", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "Nordamerika", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{ micro }})", - "manager_components_region_continent_SOF": "Mitteleuropa", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Nordeuropa", - "manager_components_region_STO": "Stockholm", - "manager_components_region_STO_micro": "Stockholm ({{ micro }})", - "manager_components_region_continent_STO": "Nordeuropa", - "manager_components_region_TPE": "Taipeh", - "manager_components_region_TPE_micro": "Taipeh ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "Südamerika", - "manager_components_region_SEL": "Seoul", - "manager_components_region_SEL_micro": "Seoul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogotá", - "manager_components_region_BOG_micro": "Bogotá ({{ micro }})", - "manager_components_region_continent_BOG": "Südamerika", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "Afrika", - "manager_components_region_CPT": "Kapstadt", - "manager_components_region_CPT_micro": "Kapstadt ({{ micro }})", - "manager_components_region_continent_CPT": "Afrika", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "Afrika", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "Afrika", - "manager_components_region_CAI": "Kairo", - "manager_components_region_CAI_micro": "Kairo ({{ micro }})", - "manager_components_region_continent_CAI": "Afrika", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Naher Osten", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausanne ({{ micro }})", - "manager_components_region_continent_LAU": "Westeuropa", - "manager_components_region_CPH": "Kopenhagen", - "manager_components_region_CPH_micro": "Kopenhagen ({{ micro }})", - "manager_components_region_continent_CPH": "Nordeuropa", - "manager_components_region_LIS": "Lissabon", - "manager_components_region_LIS_micro": "Lissabon ({{ micro }})", - "manager_components_region_continent_LIS": "Südeuropa", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "Neu-Delhi", - "manager_components_region_ICD_micro": "Neu-Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho-Chi-Minh-Stadt", - "manager_components_region_SGN_micro": "Ho-Chi-Minh-Stadt ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Wien", - "manager_components_region_VIE_micro": "Wien ({{ micro }})", - "manager_components_region_continent_VIE": "Westeuropa", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{ micro }})", - "manager_components_region_continent_DLN": "Westeuropa", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "Nordamerika", - "manager_components_region_TLV": "Tel Aviv-Jaffa", - "manager_components_region_TLV_micro": "Tel Aviv-Jaffa ({{ micro }})", - "manager_components_region_continent_TLV": "Naher Osten", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Westeuropa", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "Nordamerika", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "Nordamerika", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "Nordamerika", - "manager_components_region_STL": "Saint-Louis", - "manager_components_region_STL_micro": "Saint-Louis ({{ micro }})", - "manager_components_region_continent_STL": "Nordamerika", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "Nordamerika", - "manager_components_region_IST": "Istanbul", - "manager_components_region_IST_micro": "Istanbul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "Nordamerika", - "manager_components_region_continent_IST": "Naher Osten", - "manager_components_region_localize": "Lokalisieren", - "manager_components_region_location_SBG": "Mitteleuropa (Frankreich)", - "manager_components_region_location_WAW": "Mitteleuropa (Polen)", - "manager_components_region_location_BHS": "Nordamerika (Kanada)", - "manager_components_region_location_ERI": "Westeuropa (Vereinigtes Königreich)", - "manager_components_region_location_GRA": "Westeuropa (Frankreich)", - "manager_components_region_location_PAR": "Westeuropa (Frankreich)", - "manager_components_region_location_GS": "Westeuropa", - "manager_components_region_location_MAD": "Westeuropa", - "manager_components_region_location_BRU": "Westeuropa", - "manager_components_region_location_LIM": "Mitteleuropa (Deutschland)", - "manager_components_region_location_RBX": "Westeuropa (Frankreich)", - "manager_components_region_location_DE": "Mitteleuropa (Deutschland)", - "manager_components_region_location_UK": "Westeuropa (Vereinigtes Königreich)", - "manager_components_region_location_SGP": "Asiatisch-pazifischer Raum (Singapur)", - "manager_components_region_location_MUM": "Asiatisch-pazifischer Raum (Mumbai)", - "manager_components_region_location_SYD": "Ozeanien (Australien)", - "manager_components_region_location_US": "USA", - "manager_components_region_location_DAL": "USA", - "manager_components_region_continent_SBG": "Mitteleuropa", - "manager_components_region_continent_WAW": "Mitteleuropa", - "manager_components_region_continent_BHS": "Nordamerika", - "manager_components_region_continent_GRA": "Westeuropa", - "manager_components_region_continent_RBX": "Westeuropa", - "manager_components_region_continent_GS": "Westeuropa", - "manager_components_region_continent_MAD": "Südeuropa", - "manager_components_region_continent_BRU": "Westeuropa", - "manager_components_region_continent_DE": "Mitteleuropa", - "manager_components_region_continent_UK": "Westeuropa", - "manager_components_region_continent_SGP": "Asiatisch-pazifischer Raum", - "manager_components_region_continent_MUM": "Asiatisch-pazifischer Raum", - "manager_components_region_continent_SYD": "Ozeanien", - "manager_components_region_continent_SHA": "Westeuropa", - "manager_components_region_continent_CA": "Nordamerika", - "manager_components_region_continent_MRS": "Westeuropa", - "manager_components_region_continent_PAR": "Westeuropa" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_en_GB.json b/packages/manager-react-components/src/hooks/region/translations/Messages_en_GB.json deleted file mode 100644 index 31d1acda0d82..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_en_GB.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Strasbourg (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Strasbourg", - "manager_components_region_SBG_micro": "Strasbourg ({{ micro }})", - "manager_components_region_MRS": "Marseille", - "manager_components_region_MRS_micro": "Marseille ({{ micro }})", - "manager_components_region_PAR": "Paris", - "manager_components_region_PAR_micro": "Paris ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "London", - "manager_components_region_ERI_micro": "London ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburg", - "manager_components_region_LIM_micro": "Limburg ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Warsaw", - "manager_components_region_WAW_micro": "Warsaw ({{ micro }})", - "manager_components_region_DE": "Frankfurt", - "manager_components_region_DE_micro": "Frankfurt ({{ micro }})", - "manager_components_region_UK": "London", - "manager_components_region_UK_micro": "London ({{ micro }})", - "manager_components_region_SGP": "Singapore", - "manager_components_region_SGP_micro": "Singapore ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "North America", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "North America", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "North America", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virginia", - "manager_components_region_VA_micro": "Virginia ({{ micro }})", - "manager_components_region_continent_VA": "North America", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "North America", - "manager_components_region_US": "United States of America", - "manager_components_region_US_micro": "United States ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Brussels", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "North America", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Brussels ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Prague", - "manager_components_region_PRG_micro": "Prague ({{ micro }})", - "manager_components_region_continent_PRG": "Central Europe", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Western Europe", - "manager_components_region_MIL": "Milan", - "manager_components_region_MIL_micro": "Milan ({{ micro }})", - "manager_components_region_continent_MIL": "Southern Europe", - "manager_components_region_ZRH": "Zurich", - "manager_components_region_ZRH_micro": "Zurich ({{ micro }})", - "manager_components_region_continent_ZRH": "Western Europe", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "North America", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "North America", - "manager_components_region_NYC": "New York", - "manager_components_region_NYC_micro": "New York ({{ micro }})", - "manager_components_region_continent_NYC": "North America", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "North America", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "North America", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "North America", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "North America", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "Africa", - "manager_components_region_TYO": "Tokyo", - "manager_components_region_TYO_micro": "Tokyo ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Middle East", - "manager_components_region_JKT": "Jakarta", - "manager_components_region_JKT_micro": "Jakarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxembourg", - "manager_components_region_LUX_micro": "Luxembourg ({{ micro }})", - "manager_components_region_continent_LUX": "Western Europe", - "manager_components_region_MEX": "Mexico", - "manager_components_region_MEX_micro": "Mexico City ({{ micro }})", - "manager_components_region_continent_MEX": "Central America", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "South America", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{ micro }})", - "manager_components_region_continent_TUN": "North Africa", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "North America", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "North America", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "North America", - "manager_components_region_BUH": "Bucharest", - "manager_components_region_BUH_micro": "Bucharest ({{ micro }})", - "manager_components_region_continent_BUH": "Central Europe", - "manager_components_region_PHL": "Philadelphia", - "manager_components_region_PHL_micro": "Philadelphia ({{ micro }})", - "manager_components_region_continent_PHL": "North America", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "South America", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Northern Europe", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "North America", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{ micro }})", - "manager_components_region_continent_SOF": "Central Europe", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Northern Europe", - "manager_components_region_STO": "Stockholm", - "manager_components_region_STO_micro": "Stockholm ({{ micro }})", - "manager_components_region_continent_STO": "Northern Europe", - "manager_components_region_TPE": "Taipei", - "manager_components_region_TPE_micro": "Taipei ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "South America", - "manager_components_region_SEL": "Seoul", - "manager_components_region_SEL_micro": "Seoul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogota", - "manager_components_region_BOG_micro": "Bogota ({{ micro }})", - "manager_components_region_continent_BOG": "South America", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "Africa", - "manager_components_region_CPT": "Cape Town", - "manager_components_region_CPT_micro": "Cape Town ({{ micro }})", - "manager_components_region_continent_CPT": "Africa", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "Africa", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "Africa", - "manager_components_region_CAI": "Cairo", - "manager_components_region_CAI_micro": "Cairo ({{ micro }})", - "manager_components_region_continent_CAI": "Africa", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Middle East", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausanne ({{ micro }})", - "manager_components_region_continent_LAU": "Western Europe", - "manager_components_region_CPH": "Copenhagen", - "manager_components_region_CPH_micro": "Copenhagen ({{ micro }})", - "manager_components_region_continent_CPH": "Northern Europe", - "manager_components_region_LIS": "Lisbon", - "manager_components_region_LIS_micro": "Lisbon ({{ micro }})", - "manager_components_region_continent_LIS": "Southern Europe", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "New Delhi", - "manager_components_region_ICD_micro": "New Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Vienna", - "manager_components_region_VIE_micro": "Vienna ({{ micro }})", - "manager_components_region_continent_VIE": "Western Europe", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{ micro }})", - "manager_components_region_continent_DLN": "Western Europe", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "North America", - "manager_components_region_TLV": "Tel Aviv-Yafo", - "manager_components_region_TLV_micro": "Tel Aviv-yafo ({{ micro }})", - "manager_components_region_continent_TLV": "Middle East", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Western Europe", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "North America", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "North America", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "North America", - "manager_components_region_STL": "Saint Louis", - "manager_components_region_STL_micro": "Saint Louis ({{ micro }})", - "manager_components_region_continent_STL": "North America", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "North America", - "manager_components_region_IST": "Istanbul", - "manager_components_region_IST_micro": "Istanbul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "North America", - "manager_components_region_continent_IST": "Middle East", - "manager_components_region_localize": "Locate", - "manager_components_region_location_SBG": "Central Europe (France)", - "manager_components_region_location_WAW": "Central Europe (Poland)", - "manager_components_region_location_BHS": "North America (Canada)", - "manager_components_region_location_ERI": "Western Europe (United Kingdom)", - "manager_components_region_location_GRA": "Western Europe (France)", - "manager_components_region_location_PAR": "Western Europe (France)", - "manager_components_region_location_GS": "Western Europe", - "manager_components_region_location_MAD": "Western Europe", - "manager_components_region_location_BRU": "Western Europe", - "manager_components_region_location_LIM": "Central Europe (Germany)", - "manager_components_region_location_RBX": "Western Europe (France)", - "manager_components_region_location_DE": "Central Europe (Germany)", - "manager_components_region_location_UK": "Western Europe (United Kingdom)", - "manager_components_region_location_SGP": "Asia Pacific (Singapore)", - "manager_components_region_location_MUM": "Asia Pacific (Mumbai)", - "manager_components_region_location_SYD": "Oceania (Australia)", - "manager_components_region_location_US": "United States of America", - "manager_components_region_location_DAL": "United States of America", - "manager_components_region_continent_SBG": "Central Europe", - "manager_components_region_continent_WAW": "Central Europe", - "manager_components_region_continent_BHS": "North America", - "manager_components_region_continent_GRA": "Western Europe", - "manager_components_region_continent_RBX": "Western Europe", - "manager_components_region_continent_GS": "Western Europe", - "manager_components_region_continent_MAD": "Southern Europe", - "manager_components_region_continent_BRU": "Western Europe", - "manager_components_region_continent_DE": "Central Europe", - "manager_components_region_continent_UK": "Western Europe", - "manager_components_region_continent_SGP": "Asia Pacific", - "manager_components_region_continent_MUM": "Asia Pacific", - "manager_components_region_continent_SYD": "Oceania", - "manager_components_region_continent_SHA": "Western Europe", - "manager_components_region_continent_CA": "North America", - "manager_components_region_continent_MRS": "Western Europe", - "manager_components_region_continent_PAR": "Western Europe" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_es_ES.json b/packages/manager-react-components/src/hooks/region/translations/Messages_es_ES.json deleted file mode 100644 index 5f8dc3268cbb..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_es_ES.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Estrasburgo (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Estrasburgo", - "manager_components_region_SBG_micro": "Estrasburgo ({{ micro }})", - "manager_components_region_MRS": "Marsella", - "manager_components_region_MRS_micro": "Marsella ({{ micro }})", - "manager_components_region_PAR": "París", - "manager_components_region_PAR_micro": "París ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "Londres", - "manager_components_region_ERI_micro": "Londres ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburgo", - "manager_components_region_LIM_micro": "Limburgo ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Varsovia", - "manager_components_region_WAW_micro": "Varsovia ({{ micro }})", - "manager_components_region_DE": "Fráncfort", - "manager_components_region_DE_micro": "Fráncfort ({{ micro }})", - "manager_components_region_UK": "Londres", - "manager_components_region_UK_micro": "Londres ({{ micro }})", - "manager_components_region_SGP": "Singapur", - "manager_components_region_SGP_micro": "Singapur ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "Norteamérica", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "Norteamérica", - "manager_components_region_OR": "Oregón", - "manager_components_region_continent_OR": "Norteamérica", - "manager_components_region_OR_micro": "Oregón ({{ micro }})", - "manager_components_region_VA": "Virginia", - "manager_components_region_VA_micro": "Virginia ({{ micro }})", - "manager_components_region_continent_VA": "Norteamérica", - "manager_components_region_SYD": "Sídney", - "manager_components_region_SYD_micro": "Sídney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "Norteamérica", - "manager_components_region_US": "Estados Unidos", - "manager_components_region_US_micro": "Estados Unidos ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Bruselas", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Norteamérica", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Bruselas ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Praga", - "manager_components_region_PRG_micro": "Praga ({{ micro }})", - "manager_components_region_continent_PRG": "Europa Central", - "manager_components_region_AMS": "Ámsterdam", - "manager_components_region_AMS_micro": "Ámsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Europa Occidental", - "manager_components_region_MIL": "Milán", - "manager_components_region_MIL_micro": "Milán ({{ micro }})", - "manager_components_region_continent_MIL": "Europa Meridional", - "manager_components_region_ZRH": "Zúrich", - "manager_components_region_ZRH_micro": "Zúrich ({{ micro }})", - "manager_components_region_continent_ZRH": "Europa Occidental", - "manager_components_region_LAX": "Los Ángeles", - "manager_components_region_LAX_micro": "Los Ángeles ({{ micro }})", - "manager_components_region_continent_LAX": "Norteamérica", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "Norteamérica", - "manager_components_region_NYC": "Nueva York", - "manager_components_region_NYC_micro": "Nueva York ({{ micro }})", - "manager_components_region_continent_NYC": "Norteamérica", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "Norteamérica", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "Norteamérica", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "Norteamérica", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "Norteamérica", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "África", - "manager_components_region_TYO": "Tokio", - "manager_components_region_TYO_micro": "Tokio ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubái", - "manager_components_region_DXB_micro": "Dubái ({{ micro }})", - "manager_components_region_continent_DXB": "Oriente Medio", - "manager_components_region_JKT": "Yakarta", - "manager_components_region_JKT_micro": "Yakarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxemburgo", - "manager_components_region_LUX_micro": "Luxemburgo ({{ micro }})", - "manager_components_region_continent_LUX": "Europa Occidental", - "manager_components_region_MEX": "México", - "manager_components_region_MEX_micro": "México ({{ micro }})", - "manager_components_region_continent_MEX": "Centroamérica", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "Sudamérica", - "manager_components_region_TUN": "Túnez", - "manager_components_region_TUN_micro": "Túnez ({{ micro }})", - "manager_components_region_continent_TUN": "Norte de África", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "Norteamérica", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "Norteamérica", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "Norteamérica", - "manager_components_region_BUH": "Bucarest", - "manager_components_region_BUH_micro": "Bucarest ({{ micro }})", - "manager_components_region_continent_BUH": "Europa Central", - "manager_components_region_PHL": "Filadelfia", - "manager_components_region_PHL_micro": "Filadelfia ({{ micro }})", - "manager_components_region_continent_PHL": "Norteamérica", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "Sudamérica", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Norte de Europa", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "Norteamérica", - "manager_components_region_SOF": "Sofía", - "manager_components_region_SOF_micro": "Sofía ({{ micro }})", - "manager_components_region_continent_SOF": "Europa Central", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Norte de Europa", - "manager_components_region_STO": "Estocolmo", - "manager_components_region_STO_micro": "Estocolmo ({{ micro }})", - "manager_components_region_continent_STO": "Norte de Europa", - "manager_components_region_TPE": "Taipei", - "manager_components_region_TPE_micro": "Taipei ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "Sudamérica", - "manager_components_region_SEL": "Seúl", - "manager_components_region_SEL_micro": "Seúl ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogotá", - "manager_components_region_BOG_micro": "Bogotá ({{ micro }})", - "manager_components_region_continent_BOG": "Sudamérica", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "África", - "manager_components_region_CPT": "Ciudad del Cabo", - "manager_components_region_CPT_micro": "Ciudad del Cabo ({{ micro }})", - "manager_components_region_continent_CPT": "África", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "África", - "manager_components_region_ABJ": "Abiyán", - "manager_components_region_ABJ_micro": "Abiyán ({{ micro }})", - "manager_components_region_continent_ABJ": "África", - "manager_components_region_CAI": "El Cairo", - "manager_components_region_CAI_micro": "El Cairo ({{ micro }})", - "manager_components_region_continent_CAI": "África", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Oriente Medio", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausana ({{ micro }})", - "manager_components_region_continent_LAU": "Europa Occidental", - "manager_components_region_CPH": "Copenhague", - "manager_components_region_CPH_micro": "Copenhague ({{ micro }})", - "manager_components_region_continent_CPH": "Norte de Europa", - "manager_components_region_LIS": "Lisboa", - "manager_components_region_LIS_micro": "Lisboa ({{ micro }})", - "manager_components_region_continent_LIS": "Europa del Sur", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "Nueva Delhi", - "manager_components_region_ICD_micro": "Nueva Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Viena", - "manager_components_region_VIE_micro": "Viena ({{ micro }})", - "manager_components_region_continent_VIE": "Europa Occidental", - "manager_components_region_DLN": "Dublín", - "manager_components_region_DLN_micro": "Dublín ({{ micro }})", - "manager_components_region_continent_DLN": "Europa Occidental", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "Norteamérica", - "manager_components_region_TLV": "Tel Aviv-Yafo", - "manager_components_region_TLV_micro": "Tel Aviv-Yafo ({{ micro }})", - "manager_components_region_continent_TLV": "Oriente Medio", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Mánchester ({{ micro }})", - "manager_components_region_continent_MNC": "Europa Occidental", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "Norteamérica", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "Norteamérica", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "Norteamérica", - "manager_components_region_STL": "San Luis", - "manager_components_region_STL_micro": "San Luis ({{ micro }})", - "manager_components_region_continent_STL": "Norteamérica", - "manager_components_region_IND": "Indianápolis", - "manager_components_region_IND_micro": "Indianápolis ({{ micro }})", - "manager_components_region_continent_IND": "Norteamérica", - "manager_components_region_IST": "Estambul", - "manager_components_region_IST_micro": "Estambul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "Norteamérica", - "manager_components_region_continent_IST": "Oriente Medio", - "manager_components_region_localize": "Localizar", - "manager_components_region_location_SBG": "Europa Central (Francia)", - "manager_components_region_location_WAW": "Europa Central (Polonia)", - "manager_components_region_location_BHS": "Norteamérica (Canadá)", - "manager_components_region_location_ERI": "Europa Occidental (Reino Unido)", - "manager_components_region_location_GRA": "Europa Occidental (Francia)", - "manager_components_region_location_PAR": "Europa occidental (Francia)", - "manager_components_region_location_GS": "Europa Occidental", - "manager_components_region_location_MAD": "Europa Occidental", - "manager_components_region_location_BRU": "Europa Occidental", - "manager_components_region_location_LIM": "Europa Central (Alemania)", - "manager_components_region_location_RBX": "Europa Occidental (Francia)", - "manager_components_region_location_DE": "Europa Central (Alemania)", - "manager_components_region_location_UK": "Europa Occidental (Reino Unido)", - "manager_components_region_location_SGP": "Asia-Pacífico (Singapur)", - "manager_components_region_location_MUM": "Asia-Pacífico (Mumbai)", - "manager_components_region_location_SYD": "Oceanía (Australia)", - "manager_components_region_location_US": "Estados Unidos", - "manager_components_region_location_DAL": "Estados Unidos", - "manager_components_region_continent_SBG": "Europa Central", - "manager_components_region_continent_WAW": "Europa Central", - "manager_components_region_continent_BHS": "Norteamérica", - "manager_components_region_continent_GRA": "Europa Occidental", - "manager_components_region_continent_RBX": "Europa Occidental", - "manager_components_region_continent_GS": "Europa Occidental", - "manager_components_region_continent_MAD": "Europa Meridional", - "manager_components_region_continent_BRU": "Europa Occidental", - "manager_components_region_continent_DE": "Europa Central", - "manager_components_region_continent_UK": "Europa Occidental", - "manager_components_region_continent_SGP": "Asia-Pacífico", - "manager_components_region_continent_MUM": "Asia-Pacífico", - "manager_components_region_continent_SYD": "Oceanía", - "manager_components_region_continent_SHA": "Europa Occidental", - "manager_components_region_continent_CA": "Norteamérica", - "manager_components_region_continent_MRS": "Europa Occidental", - "manager_components_region_continent_PAR": "Europa occidental" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_fr_CA.json b/packages/manager-react-components/src/hooks/region/translations/Messages_fr_CA.json deleted file mode 100644 index 9c0af15879df..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_fr_CA.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Strasbourg (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Strasbourg", - "manager_components_region_SBG_micro": "Strasbourg ({{ micro }})", - "manager_components_region_MRS": "Marseille", - "manager_components_region_MRS_micro": "Marseille ({{ micro }})", - "manager_components_region_PAR": "Paris", - "manager_components_region_PAR_micro": "Paris ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "Londres", - "manager_components_region_ERI_micro": "Londres ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburg", - "manager_components_region_LIM_micro": "Limburg ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Varsovie", - "manager_components_region_WAW_micro": "Varsovie ({{ micro }})", - "manager_components_region_DE": "Francfort", - "manager_components_region_DE_micro": "Francfort ({{ micro }})", - "manager_components_region_UK": "Londres", - "manager_components_region_UK_micro": "Londres ({{ micro }})", - "manager_components_region_SGP": "Singapour", - "manager_components_region_SGP_micro": "Singapour ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "Amérique du Nord", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "Amérique du Nord", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "Amérique du Nord", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virginie", - "manager_components_region_VA_micro": "Virginie ({{ micro }})", - "manager_components_region_continent_VA": "Amérique du Nord", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "Amérique du Nord", - "manager_components_region_US": "États-Unis", - "manager_components_region_US_micro": "États-Unis ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Bruxelles", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Amérique du Nord", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Bruxelles ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Prague", - "manager_components_region_PRG_micro": "Prague ({{ micro }})", - "manager_components_region_continent_PRG": "Europe centrale", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Europe de l'Ouest", - "manager_components_region_MIL": "Milan", - "manager_components_region_MIL_micro": "Milan ({{ micro }})", - "manager_components_region_continent_MIL": "Europe du Sud", - "manager_components_region_ZRH": "Zurich", - "manager_components_region_ZRH_micro": "Zurich ({{ micro }})", - "manager_components_region_continent_ZRH": "Europe de l'Ouest", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "Amérique du Nord", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "Amérique du Nord", - "manager_components_region_NYC": "New York", - "manager_components_region_NYC_micro": "New York ({{ micro }})", - "manager_components_region_continent_NYC": "Amérique du Nord", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "Amérique du Nord", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "Amérique du Nord", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "Amérique du Nord", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "Amérique du Nord", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "Afrique", - "manager_components_region_TYO": "Tokyo", - "manager_components_region_TYO_micro": "Tokyo ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Moyen Orient", - "manager_components_region_JKT": "Djakarta", - "manager_components_region_JKT_micro": "Djakarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxembourg", - "manager_components_region_LUX_micro": "Luxembourg ({{ micro }})", - "manager_components_region_continent_LUX": "Europe de l'Ouest", - "manager_components_region_MEX": "Mexico", - "manager_components_region_MEX_micro": "Mexico ({{ micro }})", - "manager_components_region_continent_MEX": "Amérique Centrale", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "Amérique du Sud", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{ micro }})", - "manager_components_region_continent_TUN": "Afrique du Nord", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "Amérique du Nord", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "Amérique du Nord", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "Amérique du Nord", - "manager_components_region_BUH": "Bucarest", - "manager_components_region_BUH_micro": "Bucarest ({{ micro }})", - "manager_components_region_continent_BUH": "Europe centrale", - "manager_components_region_PHL": "Philadelphie", - "manager_components_region_PHL_micro": "Philadelphie ({{ micro }})", - "manager_components_region_continent_PHL": "Amérique du Nord", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "Amérique du Sud", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Europe du Nord", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "Amérique du Nord", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{ micro }})", - "manager_components_region_continent_SOF": "Europe centrale", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Europe du Nord", - "manager_components_region_STO": "Stockholm", - "manager_components_region_STO_micro": "Stockholm ({{ micro }})", - "manager_components_region_continent_STO": "Europe du Nord", - "manager_components_region_TPE": "Taipei", - "manager_components_region_TPE_micro": "Taipei ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "Amérique du Sud", - "manager_components_region_SEL": "Seoul", - "manager_components_region_SEL_micro": "Seoul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogota", - "manager_components_region_BOG_micro": "Bogota ({{ micro }})", - "manager_components_region_continent_BOG": "Amérique du Sud", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "Afrique", - "manager_components_region_CPT": "Le Cap", - "manager_components_region_CPT_micro": "Le Cap ({{ micro }})", - "manager_components_region_continent_CPT": "Afrique", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "Afrique", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "Afrique", - "manager_components_region_CAI": "Le Caire", - "manager_components_region_CAI_micro": "Le Caire ({{ micro }})", - "manager_components_region_continent_CAI": "Afrique", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Moyen Orient", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausanne ({{ micro }})", - "manager_components_region_continent_LAU": "Europe de l'Ouest", - "manager_components_region_CPH": "Copenhague", - "manager_components_region_CPH_micro": "Copenhague ({{ micro }})", - "manager_components_region_continent_CPH": "Europe du Nord", - "manager_components_region_LIS": "Lisbonne", - "manager_components_region_LIS_micro": "Lisbonne ({{ micro }})", - "manager_components_region_continent_LIS": "Europe du Sud", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "New Delhi", - "manager_components_region_ICD_micro": "New Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manille", - "manager_components_region_MNL_micro": "Manille ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Vienne", - "manager_components_region_VIE_micro": "Vienne ({{ micro }})", - "manager_components_region_continent_VIE": "Europe de l'Ouest", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{ micro }})", - "manager_components_region_continent_DLN": "Europe de l'Ouest", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "Amérique du Nord", - "manager_components_region_TLV": "Tel Aviv-yafo", - "manager_components_region_TLV_micro": "Tel Aviv-yafo ({{ micro }})", - "manager_components_region_continent_TLV": "Moyen Orient", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Europe de l'Ouest", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "Amérique du Nord", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "Amérique du Nord", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "Amérique du Nord", - "manager_components_region_STL": "Saint-Louis", - "manager_components_region_STL_micro": "Saint-Louis ({{ micro }})", - "manager_components_region_continent_STL": "Amérique du Nord", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "Amérique du Nord", - "manager_components_region_IST": "Istanbul", - "manager_components_region_IST_micro": "Istanbul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "Amérique du Nord", - "manager_components_region_continent_IST": "Moyen Orient", - "manager_components_region_localize": "Localiser", - "manager_components_region_location_SBG": "Europe centrale (France)", - "manager_components_region_location_WAW": "Europe centrale (Pologne)", - "manager_components_region_location_BHS": "Amérique du Nord (Canada)", - "manager_components_region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)", - "manager_components_region_location_GRA": "Europe de l'Ouest (France)", - "manager_components_region_location_PAR": "Europe de l'Ouest (France)", - "manager_components_region_location_GS": "Western Europe", - "manager_components_region_location_MAD": "Western Europe", - "manager_components_region_location_BRU": "Western Europe", - "manager_components_region_location_LIM": "Europe centrale (Allemagne)", - "manager_components_region_location_RBX": "Europe de l'Ouest (France)", - "manager_components_region_location_DE": "Europe centrale (Allemagne)", - "manager_components_region_location_UK": "Europe de l'Ouest (Grande-Bretagne)", - "manager_components_region_location_SGP": "Asie Pacifique (Singapour)", - "manager_components_region_location_MUM": "Asie Pacifique (Mumbai)", - "manager_components_region_location_SYD": "Océanie (Australie)", - "manager_components_region_location_US": "États-Unis", - "manager_components_region_location_DAL": "États-Unis", - "manager_components_region_continent_SBG": "Europe centrale", - "manager_components_region_continent_WAW": "Europe centrale", - "manager_components_region_continent_BHS": "Amérique du Nord", - "manager_components_region_continent_GRA": "Europe de l'Ouest", - "manager_components_region_continent_RBX": "Europe de l'Ouest", - "manager_components_region_continent_GS": "Europe de l'Ouest", - "manager_components_region_continent_MAD": "Europe du Sud", - "manager_components_region_continent_BRU": "Europe de l'Ouest", - "manager_components_region_continent_DE": "Europe centrale", - "manager_components_region_continent_UK": "Europe de l'Ouest", - "manager_components_region_continent_SGP": "Asie Pacifique", - "manager_components_region_continent_MUM": "Asie Pacifique", - "manager_components_region_continent_SYD": "Océanie", - "manager_components_region_continent_SHA": "Europe de l'Ouest", - "manager_components_region_continent_CA": "Amérique du Nord", - "manager_components_region_continent_MRS": "Europe de l'Ouest", - "manager_components_region_continent_PAR": "Europe de l'Ouest" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_fr_FR.json b/packages/manager-react-components/src/hooks/region/translations/Messages_fr_FR.json deleted file mode 100644 index 9c0af15879df..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_fr_FR.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Strasbourg (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Strasbourg", - "manager_components_region_SBG_micro": "Strasbourg ({{ micro }})", - "manager_components_region_MRS": "Marseille", - "manager_components_region_MRS_micro": "Marseille ({{ micro }})", - "manager_components_region_PAR": "Paris", - "manager_components_region_PAR_micro": "Paris ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "Londres", - "manager_components_region_ERI_micro": "Londres ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburg", - "manager_components_region_LIM_micro": "Limburg ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Varsovie", - "manager_components_region_WAW_micro": "Varsovie ({{ micro }})", - "manager_components_region_DE": "Francfort", - "manager_components_region_DE_micro": "Francfort ({{ micro }})", - "manager_components_region_UK": "Londres", - "manager_components_region_UK_micro": "Londres ({{ micro }})", - "manager_components_region_SGP": "Singapour", - "manager_components_region_SGP_micro": "Singapour ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "Amérique du Nord", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "Amérique du Nord", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "Amérique du Nord", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virginie", - "manager_components_region_VA_micro": "Virginie ({{ micro }})", - "manager_components_region_continent_VA": "Amérique du Nord", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "Amérique du Nord", - "manager_components_region_US": "États-Unis", - "manager_components_region_US_micro": "États-Unis ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Bruxelles", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Amérique du Nord", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Bruxelles ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Prague", - "manager_components_region_PRG_micro": "Prague ({{ micro }})", - "manager_components_region_continent_PRG": "Europe centrale", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Europe de l'Ouest", - "manager_components_region_MIL": "Milan", - "manager_components_region_MIL_micro": "Milan ({{ micro }})", - "manager_components_region_continent_MIL": "Europe du Sud", - "manager_components_region_ZRH": "Zurich", - "manager_components_region_ZRH_micro": "Zurich ({{ micro }})", - "manager_components_region_continent_ZRH": "Europe de l'Ouest", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "Amérique du Nord", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "Amérique du Nord", - "manager_components_region_NYC": "New York", - "manager_components_region_NYC_micro": "New York ({{ micro }})", - "manager_components_region_continent_NYC": "Amérique du Nord", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "Amérique du Nord", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "Amérique du Nord", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "Amérique du Nord", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "Amérique du Nord", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "Afrique", - "manager_components_region_TYO": "Tokyo", - "manager_components_region_TYO_micro": "Tokyo ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Moyen Orient", - "manager_components_region_JKT": "Djakarta", - "manager_components_region_JKT_micro": "Djakarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxembourg", - "manager_components_region_LUX_micro": "Luxembourg ({{ micro }})", - "manager_components_region_continent_LUX": "Europe de l'Ouest", - "manager_components_region_MEX": "Mexico", - "manager_components_region_MEX_micro": "Mexico ({{ micro }})", - "manager_components_region_continent_MEX": "Amérique Centrale", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "Amérique du Sud", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{ micro }})", - "manager_components_region_continent_TUN": "Afrique du Nord", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "Amérique du Nord", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "Amérique du Nord", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "Amérique du Nord", - "manager_components_region_BUH": "Bucarest", - "manager_components_region_BUH_micro": "Bucarest ({{ micro }})", - "manager_components_region_continent_BUH": "Europe centrale", - "manager_components_region_PHL": "Philadelphie", - "manager_components_region_PHL_micro": "Philadelphie ({{ micro }})", - "manager_components_region_continent_PHL": "Amérique du Nord", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "Amérique du Sud", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Europe du Nord", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "Amérique du Nord", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{ micro }})", - "manager_components_region_continent_SOF": "Europe centrale", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Europe du Nord", - "manager_components_region_STO": "Stockholm", - "manager_components_region_STO_micro": "Stockholm ({{ micro }})", - "manager_components_region_continent_STO": "Europe du Nord", - "manager_components_region_TPE": "Taipei", - "manager_components_region_TPE_micro": "Taipei ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "Amérique du Sud", - "manager_components_region_SEL": "Seoul", - "manager_components_region_SEL_micro": "Seoul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogota", - "manager_components_region_BOG_micro": "Bogota ({{ micro }})", - "manager_components_region_continent_BOG": "Amérique du Sud", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "Afrique", - "manager_components_region_CPT": "Le Cap", - "manager_components_region_CPT_micro": "Le Cap ({{ micro }})", - "manager_components_region_continent_CPT": "Afrique", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "Afrique", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "Afrique", - "manager_components_region_CAI": "Le Caire", - "manager_components_region_CAI_micro": "Le Caire ({{ micro }})", - "manager_components_region_continent_CAI": "Afrique", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Moyen Orient", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausanne ({{ micro }})", - "manager_components_region_continent_LAU": "Europe de l'Ouest", - "manager_components_region_CPH": "Copenhague", - "manager_components_region_CPH_micro": "Copenhague ({{ micro }})", - "manager_components_region_continent_CPH": "Europe du Nord", - "manager_components_region_LIS": "Lisbonne", - "manager_components_region_LIS_micro": "Lisbonne ({{ micro }})", - "manager_components_region_continent_LIS": "Europe du Sud", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "New Delhi", - "manager_components_region_ICD_micro": "New Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manille", - "manager_components_region_MNL_micro": "Manille ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Vienne", - "manager_components_region_VIE_micro": "Vienne ({{ micro }})", - "manager_components_region_continent_VIE": "Europe de l'Ouest", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{ micro }})", - "manager_components_region_continent_DLN": "Europe de l'Ouest", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "Amérique du Nord", - "manager_components_region_TLV": "Tel Aviv-yafo", - "manager_components_region_TLV_micro": "Tel Aviv-yafo ({{ micro }})", - "manager_components_region_continent_TLV": "Moyen Orient", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Europe de l'Ouest", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "Amérique du Nord", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "Amérique du Nord", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "Amérique du Nord", - "manager_components_region_STL": "Saint-Louis", - "manager_components_region_STL_micro": "Saint-Louis ({{ micro }})", - "manager_components_region_continent_STL": "Amérique du Nord", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "Amérique du Nord", - "manager_components_region_IST": "Istanbul", - "manager_components_region_IST_micro": "Istanbul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "Amérique du Nord", - "manager_components_region_continent_IST": "Moyen Orient", - "manager_components_region_localize": "Localiser", - "manager_components_region_location_SBG": "Europe centrale (France)", - "manager_components_region_location_WAW": "Europe centrale (Pologne)", - "manager_components_region_location_BHS": "Amérique du Nord (Canada)", - "manager_components_region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)", - "manager_components_region_location_GRA": "Europe de l'Ouest (France)", - "manager_components_region_location_PAR": "Europe de l'Ouest (France)", - "manager_components_region_location_GS": "Western Europe", - "manager_components_region_location_MAD": "Western Europe", - "manager_components_region_location_BRU": "Western Europe", - "manager_components_region_location_LIM": "Europe centrale (Allemagne)", - "manager_components_region_location_RBX": "Europe de l'Ouest (France)", - "manager_components_region_location_DE": "Europe centrale (Allemagne)", - "manager_components_region_location_UK": "Europe de l'Ouest (Grande-Bretagne)", - "manager_components_region_location_SGP": "Asie Pacifique (Singapour)", - "manager_components_region_location_MUM": "Asie Pacifique (Mumbai)", - "manager_components_region_location_SYD": "Océanie (Australie)", - "manager_components_region_location_US": "États-Unis", - "manager_components_region_location_DAL": "États-Unis", - "manager_components_region_continent_SBG": "Europe centrale", - "manager_components_region_continent_WAW": "Europe centrale", - "manager_components_region_continent_BHS": "Amérique du Nord", - "manager_components_region_continent_GRA": "Europe de l'Ouest", - "manager_components_region_continent_RBX": "Europe de l'Ouest", - "manager_components_region_continent_GS": "Europe de l'Ouest", - "manager_components_region_continent_MAD": "Europe du Sud", - "manager_components_region_continent_BRU": "Europe de l'Ouest", - "manager_components_region_continent_DE": "Europe centrale", - "manager_components_region_continent_UK": "Europe de l'Ouest", - "manager_components_region_continent_SGP": "Asie Pacifique", - "manager_components_region_continent_MUM": "Asie Pacifique", - "manager_components_region_continent_SYD": "Océanie", - "manager_components_region_continent_SHA": "Europe de l'Ouest", - "manager_components_region_continent_CA": "Amérique du Nord", - "manager_components_region_continent_MRS": "Europe de l'Ouest", - "manager_components_region_continent_PAR": "Europe de l'Ouest" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_it_IT.json b/packages/manager-react-components/src/hooks/region/translations/Messages_it_IT.json deleted file mode 100644 index a977abd30527..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_it_IT.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Strasburgo (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Strasburgo", - "manager_components_region_SBG_micro": "Strasburgo ({{ micro }})", - "manager_components_region_MRS": "Marsiglia", - "manager_components_region_MRS_micro": "Marsiglia ({{ micro }})", - "manager_components_region_PAR": "Parigi", - "manager_components_region_PAR_micro": "Parigi ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "Londra", - "manager_components_region_ERI_micro": "Londra ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburgo", - "manager_components_region_LIM_micro": "Limburgo ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Varsavia", - "manager_components_region_WAW_micro": "Varsavia ({{ micro }})", - "manager_components_region_DE": "Francoforte", - "manager_components_region_DE_micro": "Francoforte ({{ micro }})", - "manager_components_region_UK": "Londra", - "manager_components_region_UK_micro": "Londra ({{ micro }})", - "manager_components_region_SGP": "Singapore", - "manager_components_region_SGP_micro": "Singapore ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "Nord America ", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "Nord America ", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "Nord America ", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virginia", - "manager_components_region_VA_micro": "Virginia ({{ micro }})", - "manager_components_region_continent_VA": "Nord America ", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "Nord America ", - "manager_components_region_US": "Stati Uniti", - "manager_components_region_US_micro": "Stati Uniti ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Bruxelles", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Nord America ", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Bruxelles ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Praga", - "manager_components_region_PRG_micro": "Praga ({{ micro }})", - "manager_components_region_continent_PRG": "Europa centrale", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{ micro }})", - "manager_components_region_continent_AMS": "Europa Occidentale", - "manager_components_region_MIL": "Milano", - "manager_components_region_MIL_micro": "Milano ({{ micro }})", - "manager_components_region_continent_MIL": "Sud Europa", - "manager_components_region_ZRH": "Zurigo", - "manager_components_region_ZRH_micro": "Zurigo ({{ micro }})", - "manager_components_region_continent_ZRH": "Europa Occidentale", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "Nord America ", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "Nord America ", - "manager_components_region_NYC": "New York", - "manager_components_region_NYC_micro": "New York ({{ micro }})", - "manager_components_region_continent_NYC": "Nord America ", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "Nord America ", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "Nord America ", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "Nord America ", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "Nord America ", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "Africa", - "manager_components_region_TYO": "Tokyo", - "manager_components_region_TYO_micro": "Tokyo ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Medio Oriente", - "manager_components_region_JKT": "Giacarta", - "manager_components_region_JKT_micro": "Giacarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Lussemburgo", - "manager_components_region_LUX_micro": "Lussemburgo ({{ micro }})", - "manager_components_region_continent_LUX": "Europa Occidentale", - "manager_components_region_MEX": "Messico", - "manager_components_region_MEX_micro": "Messico ({{ micro }})", - "manager_components_region_continent_MEX": "Centro America ", - "manager_components_region_SAO": "San Paolo", - "manager_components_region_SAO_micro": "San Paolo ({{ micro }})", - "manager_components_region_continent_SAO": "Sud America", - "manager_components_region_TUN": "Tunisi", - "manager_components_region_TUN_micro": "Tunisi ({{ micro }})", - "manager_components_region_continent_TUN": "Nord Africa", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "Nord America ", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "Nord America ", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "Nord America ", - "manager_components_region_BUH": "Bucarest", - "manager_components_region_BUH_micro": "Bucarest ({{ micro }})", - "manager_components_region_continent_BUH": "Europa centrale", - "manager_components_region_PHL": "Filadelfia", - "manager_components_region_PHL_micro": "Filadelfia ({{ micro }})", - "manager_components_region_continent_PHL": "Nord America ", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "Sud America", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{ micro }})", - "manager_components_region_continent_HEL": "Nord Europa", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "Nord America ", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{ micro }})", - "manager_components_region_continent_SOF": "Europa centrale", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Nord Europa", - "manager_components_region_STO": "Stoccolma", - "manager_components_region_STO_micro": "Stoccolma ({{ micro }})", - "manager_components_region_continent_STO": "Nord Europa", - "manager_components_region_TPE": "Taipei", - "manager_components_region_TPE_micro": "Taipei ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "Sud America", - "manager_components_region_SEL": "Seul", - "manager_components_region_SEL_micro": "Seul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogotà", - "manager_components_region_BOG_micro": "Bogotà ({{ micro }})", - "manager_components_region_continent_BOG": "Sud America", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "Africa", - "manager_components_region_CPT": "Città del Capo", - "manager_components_region_CPT_micro": "Città del Capo ({{ micro }})", - "manager_components_region_continent_CPT": "Africa", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "Africa", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "Africa", - "manager_components_region_CAI": "Il Cairo", - "manager_components_region_CAI_micro": "Il Cairo ({{ micro }})", - "manager_components_region_continent_CAI": "Africa", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Medio Oriente", - "manager_components_region_LAU": "Losanna", - "manager_components_region_LAU_micro": "Losanna ({{ micro }})", - "manager_components_region_continent_LAU": "Europa Occidentale", - "manager_components_region_CPH": "Copenaghen", - "manager_components_region_CPH_micro": "Copenaghen ({{ micro }})", - "manager_components_region_continent_CPH": "Nord Europa", - "manager_components_region_LIS": "Lisbona", - "manager_components_region_LIS_micro": "Lisbona ({{ micro }})", - "manager_components_region_continent_LIS": "Sud Europa", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "Nuova Delhi", - "manager_components_region_ICD_micro": "Nuova Delhi ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Vienna", - "manager_components_region_VIE_micro": "Vienna ({{ micro }})", - "manager_components_region_continent_VIE": "Europa Occidentale", - "manager_components_region_DLN": "Dublino", - "manager_components_region_DLN_micro": "Dublino ({{ micro }})", - "manager_components_region_continent_DLN": "Europa Occidentale", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "Nord America ", - "manager_components_region_TLV": "Tel Aviv", - "manager_components_region_TLV_micro": "Tel Aviv ({{ micro }})", - "manager_components_region_continent_TLV": "Medio Oriente", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Europa Occidentale", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "Nord America ", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "Nord America ", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "Nord America ", - "manager_components_region_STL": "Saint Louis", - "manager_components_region_STL_micro": "Saint Louis ({{ micro }})", - "manager_components_region_continent_STL": "Nord America ", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "Nord America ", - "manager_components_region_IST": "Istanbul", - "manager_components_region_IST_micro": "Istanbul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "Nord America ", - "manager_components_region_continent_IST": "Medio Oriente", - "manager_components_region_localize": "Localizza", - "manager_components_region_location_SBG": "Europa centrale (Francia)", - "manager_components_region_location_WAW": "Europa centrale (Polonia)", - "manager_components_region_location_BHS": "Nord America (Canada)", - "manager_components_region_location_ERI": "Europa Occidentale (Gran Bretagna)", - "manager_components_region_location_GRA": "Europa Occidentale (Francia)", - "manager_components_region_location_PAR": "Europa occidentale (Francia)", - "manager_components_region_location_GS": "Europa Occidentale", - "manager_components_region_location_MAD": "Europa Occidentale", - "manager_components_region_location_BRU": "Europa Occidentale", - "manager_components_region_location_LIM": "Europa centrale (Germania)", - "manager_components_region_location_RBX": "Europa Occidentale (Francia)", - "manager_components_region_location_DE": "Europa centrale (Germania)", - "manager_components_region_location_UK": "Europa Occidentale (Gran Bretagna)", - "manager_components_region_location_SGP": "Asia Pacifica (Singapore)", - "manager_components_region_location_MUM": "Asia Pacifica (Mumbai)", - "manager_components_region_location_SYD": "Oceania (Australia)", - "manager_components_region_location_US": "Stati Uniti", - "manager_components_region_location_DAL": "Stati Uniti", - "manager_components_region_continent_SBG": "Europa centrale", - "manager_components_region_continent_WAW": "Europa centrale", - "manager_components_region_continent_BHS": "Nord America ", - "manager_components_region_continent_GRA": "Europa Occidentale", - "manager_components_region_continent_RBX": "Europa Occidentale", - "manager_components_region_continent_GS": "Europa Occidentale", - "manager_components_region_continent_MAD": "Sud Europa", - "manager_components_region_continent_BRU": "Europa Occidentale", - "manager_components_region_continent_DE": "Europa centrale", - "manager_components_region_continent_UK": "Europa Occidentale", - "manager_components_region_continent_SGP": "Asia Pacifica", - "manager_components_region_continent_MUM": "Asia Pacifica", - "manager_components_region_continent_SYD": "Oceania", - "manager_components_region_continent_SHA": "Europa Occidentale", - "manager_components_region_continent_CA": "Nord America ", - "manager_components_region_continent_MRS": "Europa Occidentale", - "manager_components_region_continent_PAR": "Europa occidentale" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_pl_PL.json b/packages/manager-react-components/src/hooks/region/translations/Messages_pl_PL.json deleted file mode 100644 index d907c3a3bc78..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_pl_PL.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Strasburg (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Strasburg", - "manager_components_region_SBG_micro": "Strasburg ({{micro}})", - "manager_components_region_MRS": "Marsylia", - "manager_components_region_MRS_micro": "Marsylia ({{micro}})", - "manager_components_region_PAR": "Paryż", - "manager_components_region_PAR_micro": "Paryż ({{micro}})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{micro}})", - "manager_components_region_ERI": "Londyn", - "manager_components_region_ERI_micro": "Londyn ({{micro}})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{micro}})", - "manager_components_region_LIM": "Limburg", - "manager_components_region_LIM_micro": "Limburg ({{micro}})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{micro}})", - "manager_components_region_WAW": "Warszawa", - "manager_components_region_WAW_micro": "Warszawa ({{micro}})", - "manager_components_region_DE": "Frankfurt", - "manager_components_region_DE_micro": "Frankfurt ({{micro}})", - "manager_components_region_UK": "Londyn", - "manager_components_region_UK_micro": "Londyn ({{micro}})", - "manager_components_region_SGP": "Singapur", - "manager_components_region_SGP_micro": "Singapur ({{micro}})", - "manager_components_region_MUM": "Mumbaj", - "manager_components_region_MUM_micro": "Mumbaj ({{micro}})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{micro}})", - "manager_components_region_continent_VIN": "Ameryka Północna", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{micro}})", - "manager_components_region_continent_HIL": "Ameryka Północna", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "Ameryka Północna", - "manager_components_region_OR_micro": "Oregon ({{micro}})", - "manager_components_region_VA": "Wirginia", - "manager_components_region_VA_micro": "Wirginia ({{micro}})", - "manager_components_region_continent_VA": "Ameryka Północna", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{micro}})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{micro}})", - "manager_components_region_continent_TOR": "Ameryka Północna", - "manager_components_region_US": "Stany Zjednoczone", - "manager_components_region_US_micro": "Stany Zjednoczone ({{micro}})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madryt", - "manager_components_region_BRU": "Bruksela", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "Ameryka Północna", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{micro}})", - "manager_components_region_MAD_micro": "Madryt ({{micro}})", - "manager_components_region_BRU_micro": "Bruksela ({{micro}})", - "manager_components_region_DAL_micro": "Dallas ({{micro}})", - "manager_components_region_PRG": "Praga", - "manager_components_region_PRG_micro": "Praga ({{micro}})", - "manager_components_region_continent_PRG": "Europa Środkowa", - "manager_components_region_AMS": "Amsterdam", - "manager_components_region_AMS_micro": "Amsterdam ({{micro}})", - "manager_components_region_continent_AMS": "Europa Zachodnia", - "manager_components_region_MIL": "Mediolan", - "manager_components_region_MIL_micro": "Mediolan ({{micro}})", - "manager_components_region_continent_MIL": "Europa Południowa", - "manager_components_region_ZRH": "Zurych", - "manager_components_region_ZRH_micro": "Zurych ({{micro}})", - "manager_components_region_continent_ZRH": "Europa Zachodnia", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{micro}})", - "manager_components_region_continent_LAX": "Ameryka Północna", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{micro}})", - "manager_components_region_continent_CHI": "Ameryka Północna", - "manager_components_region_NYC": "Nowy Jork", - "manager_components_region_NYC_micro": "Nowy Jork ({{micro}})", - "manager_components_region_continent_NYC": "Ameryka Północna", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{micro}})", - "manager_components_region_continent_MIA": "Ameryka Północna", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{micro}})", - "manager_components_region_continent_PAO": "Ameryka Północna", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{micro}})", - "manager_components_region_continent_DEN": "Ameryka Północna", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{micro}})", - "manager_components_region_continent_ATL": "Ameryka Północna", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{micro}})", - "manager_components_region_continent_RBA": "Afryka", - "manager_components_region_TYO": "Tokio", - "manager_components_region_TYO_micro": "Tokio ({{micro}})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bengaluru", - "manager_components_region_BLR_micro": "Bengaluru ({{micro}})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubaj", - "manager_components_region_DXB_micro": "Dubaj ({{micro}})", - "manager_components_region_continent_DXB": "Bliski Wschód", - "manager_components_region_JKT": "Dżakarta", - "manager_components_region_JKT_micro": "Dżakarta ({{micro}})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luksemburg", - "manager_components_region_LUX_micro": "Luksemburg ({{micro}})", - "manager_components_region_continent_LUX": "Europa Zachodnia", - "manager_components_region_MEX": "Meksyk", - "manager_components_region_MEX_micro": "Meksyk ({{micro}})", - "manager_components_region_continent_MEX": "Ameryka Środkowa", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{micro}})", - "manager_components_region_continent_SAO": "Ameryka Południowa", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{micro}})", - "manager_components_region_continent_TUN": "Afryka Północna", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{micro}})", - "manager_components_region_continent_AUS": "Ameryka Północna", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{micro}})", - "manager_components_region_continent_BOS": "Ameryka Północna", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{micro}})", - "manager_components_region_continent_SEA": "Ameryka Północna", - "manager_components_region_BUH": "Bukareszt", - "manager_components_region_BUH_micro": "Bukareszt ({{micro}})", - "manager_components_region_continent_BUH": "Europa Środkowa", - "manager_components_region_PHL": "Filadelfia", - "manager_components_region_PHL_micro": "Filadelfia ({{micro}})", - "manager_components_region_continent_PHL": "Ameryka Północna", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{micro}})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{micro}})", - "manager_components_region_continent_BUE": "Ameryka Południowa", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{micro}})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsinki", - "manager_components_region_HEL_micro": "Helsinki ({{micro}})", - "manager_components_region_continent_HEL": "Europa Północna", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{micro}})", - "manager_components_region_continent_HOU": "Ameryka Północna", - "manager_components_region_SOF": "Sofia", - "manager_components_region_SOF_micro": "Sofia ({{micro}})", - "manager_components_region_continent_SOF": "Europa Środkowa", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{micro}})", - "manager_components_region_continent_OSL": "Europa Północna", - "manager_components_region_STO": "Sztokholm", - "manager_components_region_STO_micro": "Sztokholm ({{micro}})", - "manager_components_region_continent_STO": "Europa Północna", - "manager_components_region_TPE": "Tajpej", - "manager_components_region_TPE_micro": "Tajpej ({{micro}})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{micro}})", - "manager_components_region_continent_SCL": "Ameryka Południowa", - "manager_components_region_SEL": "Seul", - "manager_components_region_SEL_micro": "Seul ({{micro}})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogota", - "manager_components_region_BOG_micro": "Bogota ({{micro}})", - "manager_components_region_continent_BOG": "Ameryka Południowa", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{micro}})", - "manager_components_region_continent_LAG": "Afryka", - "manager_components_region_CPT": "Kapsztad", - "manager_components_region_CPT_micro": "Kapsztad ({{micro}})", - "manager_components_region_continent_CPT": "Afryka", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{micro}})", - "manager_components_region_continent_NBO": "Afryka", - "manager_components_region_ABJ": "Abidżan", - "manager_components_region_ABJ_micro": "Abidżan ({{micro}})", - "manager_components_region_continent_ABJ": "Afryka", - "manager_components_region_CAI": "Kair", - "manager_components_region_CAI_micro": "Kair ({{micro}})", - "manager_components_region_continent_CAI": "Afryka", - "manager_components_region_DOH": "Ad-Dauha", - "manager_components_region_DOH_micro": "Ad-Dauha ({{micro}})", - "manager_components_region_continent_DOH": "Bliski Wschód", - "manager_components_region_LAU": "Lozanna", - "manager_components_region_LAU_micro": "Lozanna ({{micro}})", - "manager_components_region_continent_LAU": "Europa Zachodnia", - "manager_components_region_CPH": "Kopenhaga", - "manager_components_region_CPH_micro": "Kopenhaga ({{micro}})", - "manager_components_region_continent_CPH": "Europa Północna", - "manager_components_region_LIS": "Lizbona", - "manager_components_region_LIS_micro": "Lizbona ({{micro}})", - "manager_components_region_continent_LIS": "Europa Południowa", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{micro}})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "New Delhi", - "manager_components_region_ICD_micro": "New Delhi ({{micro}})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{micro}})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{micro}})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{micro}})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{micro}})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Wiedeń", - "manager_components_region_VIE_micro": "Wiedeń ({{micro}})", - "manager_components_region_continent_VIE": "Europa Zachodnia", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{micro}})", - "manager_components_region_continent_DLN": "Europa Zachodnia", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{micro}})", - "manager_components_region_continent_VAN": "Ameryka Północna", - "manager_components_region_TLV": "Tel Awiw", - "manager_components_region_TLV_micro": "Tel Awiw ({{micro}})", - "manager_components_region_continent_TLV": "Bliski Wschód", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{micro}})", - "manager_components_region_continent_MNC": "Europa Zachodnia", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{micro}})", - "manager_components_region_continent_CLT": "Ameryka Północna", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{micro}})", - "manager_components_region_continent_BNA": "Ameryka Północna", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{micro}})", - "manager_components_region_continent_SLC": "Ameryka Północna", - "manager_components_region_STL": "St. Louis", - "manager_components_region_STL_micro": "St. Louis ({{micro}})", - "manager_components_region_continent_STL": "Ameryka Północna", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{micro}})", - "manager_components_region_continent_IND": "Ameryka Północna", - "manager_components_region_IST": "Stambuł", - "manager_components_region_IST_micro": "Stambuł ({{micro}})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{micro}})", - "manager_components_region_continent_PHX": "Ameryka Północna", - "manager_components_region_continent_IST": "Bliski Wschód", - "manager_components_region_localize": "Lokalizacja", - "manager_components_region_location_SBG": "Europa Środkowa (Francja)", - "manager_components_region_location_WAW": "Europa Środkowa (Polska)", - "manager_components_region_location_BHS": "Ameryka Północna (Kanada)", - "manager_components_region_location_ERI": "Europa Zachodnia (Wielka Brytania)", - "manager_components_region_location_GRA": "Europa Zachodnia (Francja)", - "manager_components_region_location_PAR": "Europa Zachodnia (Francja)", - "manager_components_region_location_GS": "Europa Zachodnia", - "manager_components_region_location_MAD": "Europa Zachodnia", - "manager_components_region_location_BRU": "Europa Zachodnia", - "manager_components_region_location_LIM": "Europa Środkowa (Niemcy)", - "manager_components_region_location_RBX": "Europa Zachodnia (Francja)", - "manager_components_region_location_DE": "Europa Środkowa (Niemcy)", - "manager_components_region_location_UK": "Europa Zachodnia (Wielka Brytania)", - "manager_components_region_location_SGP": "Azja-Pacyfik (Singapur)", - "manager_components_region_location_MUM": "Azja-Pacyfik (Mumbaj)", - "manager_components_region_location_SYD": "Oceania (Australia)", - "manager_components_region_location_US": "Stany Zjednoczone", - "manager_components_region_location_DAL": "Stany Zjednoczone", - "manager_components_region_continent_SBG": "Europa Środkowa", - "manager_components_region_continent_WAW": "Europa Środkowa", - "manager_components_region_continent_BHS": "Ameryka Północna", - "manager_components_region_continent_GRA": "Europa Zachodnia", - "manager_components_region_continent_RBX": "Europa Zachodnia", - "manager_components_region_continent_GS": "Europa Zachodnia", - "manager_components_region_continent_MAD": "Europa Południowa", - "manager_components_region_continent_BRU": "Europa Zachodnia", - "manager_components_region_continent_DE": "Europa Środkowa", - "manager_components_region_continent_UK": "Europa Zachodnia", - "manager_components_region_continent_SGP": "Azja-Pacyfik", - "manager_components_region_continent_MUM": "Azja-Pacyfik", - "manager_components_region_continent_SYD": "Oceania", - "manager_components_region_continent_SHA": "Europa Zachodnia", - "manager_components_region_continent_CA": "Ameryka Północna", - "manager_components_region_continent_MRS": "Europa Zachodnia", - "manager_components_region_continent_PAR": "Europa Zachodnia" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/Messages_pt_PT.json b/packages/manager-react-components/src/hooks/region/translations/Messages_pt_PT.json deleted file mode 100644 index 53427d40240d..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/Messages_pt_PT.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "manager_components_region_SBG1": "Estrasburgo (SBG1)", - "manager_components_region_BHS1": "Beauharnois (BHS1)", - "manager_components_region_GRA1": "Gravelines (GRA1)", - "manager_components_region_SBG": "Estrasburgo", - "manager_components_region_SBG_micro": "Estrasburgo ({{ micro }})", - "manager_components_region_MRS": "Marselha", - "manager_components_region_MRS_micro": "Marselha ({{ micro }})", - "manager_components_region_PAR": "Paris", - "manager_components_region_PAR_micro": "Paris ({{ micro }})", - "manager_components_region_BHS": "Beauharnois", - "manager_components_region_BHS_micro": "Beauharnois ({{ micro }})", - "manager_components_region_ERI": "Londres", - "manager_components_region_ERI_micro": "Londres ({{ micro }})", - "manager_components_region_GRA": "Gravelines", - "manager_components_region_GRA_micro": "Gravelines ({{ micro }})", - "manager_components_region_LIM": "Limburgo", - "manager_components_region_LIM_micro": "Limburgo ({{ micro }})", - "manager_components_region_RBX": "Roubaix", - "manager_components_region_RBX_micro": "Roubaix ({{ micro }})", - "manager_components_region_WAW": "Varsóvia", - "manager_components_region_WAW_micro": "Varsóvia ({{ micro }})", - "manager_components_region_DE": "Frankfurt ", - "manager_components_region_DE_micro": "Frankfurt ({{ micro }})", - "manager_components_region_UK": "Londres", - "manager_components_region_UK_micro": "Londres ({{ micro }})", - "manager_components_region_SGP": "Singapura", - "manager_components_region_SGP_micro": "Singapura ({{ micro }})", - "manager_components_region_MUM": "Mumbai", - "manager_components_region_MUM_micro": "Mumbai ({{ micro }})", - "manager_components_region_VIN": "Vint Hill", - "manager_components_region_VIN_micro": "Vint Hill ({{ micro }})", - "manager_components_region_continent_VIN": "América do Norte", - "manager_components_region_HIL": "Hillsboro", - "manager_components_region_HIL_micro": "Hillsboro ({{ micro }})", - "manager_components_region_continent_HIL": "América do Norte", - "manager_components_region_OR": "Oregon", - "manager_components_region_continent_OR": "América do Norte", - "manager_components_region_OR_micro": "Oregon ({{ micro }})", - "manager_components_region_VA": "Virgínia", - "manager_components_region_VA_micro": "Virgínia ({{ micro }})", - "manager_components_region_continent_VA": "América do Norte", - "manager_components_region_SYD": "Sydney", - "manager_components_region_SYD_micro": "Sydney ({{ micro }})", - "manager_components_region_TOR": "Toronto", - "manager_components_region_TOR_micro": "Toronto ({{ micro }})", - "manager_components_region_continent_TOR": "América do Norte", - "manager_components_region_US": "Estados Unidos", - "manager_components_region_US_micro": "Estados Unidos ({{ micro }})", - "manager_components_region_GS": "GS", - "manager_components_region_MAD": "Madrid", - "manager_components_region_BRU": "Bruxelas", - "manager_components_region_DAL": "Dallas", - "manager_components_region_continent_DAL": "América do Norte", - "manager_components_region_SHA_micro": "Gravelines (SHADOW-EU-1)", - "manager_components_region_GS_micro": "Gridscale ({{ micro }})", - "manager_components_region_MAD_micro": "Madrid ({{ micro }})", - "manager_components_region_BRU_micro": "Bruxelas ({{ micro }})", - "manager_components_region_DAL_micro": "Dallas ({{ micro }})", - "manager_components_region_PRG": "Praga", - "manager_components_region_PRG_micro": "Praga ({{ micro }})", - "manager_components_region_continent_PRG": "Europa Central", - "manager_components_region_AMS": "Amesterdão", - "manager_components_region_AMS_micro": "Amesterdão ({{ micro }})", - "manager_components_region_continent_AMS": "Europa Ocidental", - "manager_components_region_MIL": "Milão", - "manager_components_region_MIL_micro": "Milão ({{ micro }})", - "manager_components_region_continent_MIL": "Sul da Europa", - "manager_components_region_ZRH": "Zurique", - "manager_components_region_ZRH_micro": "Zurique ({{ micro }})", - "manager_components_region_continent_ZRH": "Europa Ocidental", - "manager_components_region_LAX": "Los Angeles", - "manager_components_region_LAX_micro": "Los Angeles ({{ micro }})", - "manager_components_region_continent_LAX": "América do Norte", - "manager_components_region_CHI": "Chicago", - "manager_components_region_CHI_micro": "Chicago ({{ micro }})", - "manager_components_region_continent_CHI": "América do Norte", - "manager_components_region_NYC": "Nova Iorque ", - "manager_components_region_NYC_micro": "Nova Iorque ({{ micro }})", - "manager_components_region_continent_NYC": "América do Norte", - "manager_components_region_MIA": "Miami", - "manager_components_region_MIA_micro": "Miami ({{ micro }})", - "manager_components_region_continent_MIA": "América do Norte", - "manager_components_region_PAO": "Palo Alto", - "manager_components_region_PAO_micro": "Palo Alto ({{ micro }})", - "manager_components_region_continent_PAO": "América do Norte", - "manager_components_region_DEN": "Denver", - "manager_components_region_DEN_micro": "Denver ({{ micro }})", - "manager_components_region_continent_DEN": "América do Norte", - "manager_components_region_ATL": "Atlanta", - "manager_components_region_ATL_micro": "Atlanta ({{ micro }})", - "manager_components_region_continent_ATL": "América do Norte", - "manager_components_region_RBA": "Rabat", - "manager_components_region_RBA_micro": "Rabat ({{ micro }})", - "manager_components_region_continent_RBA": "África", - "manager_components_region_TYO": "Tóquio", - "manager_components_region_TYO_micro": "Tóquio ({{ micro }})", - "manager_components_region_continent_TYO": "APAC", - "manager_components_region_BLR": "Bangalore", - "manager_components_region_BLR_micro": "Bangalore ({{ micro }})", - "manager_components_region_continent_BLR": "APAC", - "manager_components_region_DXB": "Dubai", - "manager_components_region_DXB_micro": "Dubai ({{ micro }})", - "manager_components_region_continent_DXB": "Médio Oriente", - "manager_components_region_JKT": "Jacarta", - "manager_components_region_JKT_micro": "Jacarta ({{ micro }})", - "manager_components_region_continent_JKT": "APAC", - "manager_components_region_LUX": "Luxemburgo", - "manager_components_region_LUX_micro": "Luxemburgo ({{ micro }})", - "manager_components_region_continent_LUX": "Europa Ocidental", - "manager_components_region_MEX": "México", - "manager_components_region_MEX_micro": "México ({{ micro }})", - "manager_components_region_continent_MEX": "América Central", - "manager_components_region_SAO": "São Paulo", - "manager_components_region_SAO_micro": "São Paulo ({{ micro }})", - "manager_components_region_continent_SAO": "América do Sul", - "manager_components_region_TUN": "Tunis", - "manager_components_region_TUN_micro": "Tunis ({{ micro }})", - "manager_components_region_continent_TUN": "Norte de África", - "manager_components_region_AUS": "Austin", - "manager_components_region_AUS_micro": "Austin ({{ micro }})", - "manager_components_region_continent_AUS": "América do Norte", - "manager_components_region_BOS": "Boston", - "manager_components_region_BOS_micro": "Boston ({{ micro }})", - "manager_components_region_continent_BOS": "América do Norte", - "manager_components_region_SEA": "Seattle", - "manager_components_region_SEA_micro": "Seattle ({{ micro }})", - "manager_components_region_continent_SEA": "América do Norte", - "manager_components_region_BUH": "Bucareste", - "manager_components_region_BUH_micro": "Bucareste ({{ micro }})", - "manager_components_region_continent_BUH": "Europa Central", - "manager_components_region_PHL": "Filadélfia", - "manager_components_region_PHL_micro": "Filadélfia ({{ micro }})", - "manager_components_region_continent_PHL": "América do Norte", - "manager_components_region_BKK": "Bangkok", - "manager_components_region_BKK_micro": "Bangkok ({{ micro }})", - "manager_components_region_continent_BKK": "APAC", - "manager_components_region_BUE": "Buenos Aires", - "manager_components_region_BUE_micro": "Buenos Aires ({{ micro }})", - "manager_components_region_continent_BUE": "América do Sul", - "manager_components_region_AKL": "Auckland", - "manager_components_region_AKL_micro": "Auckland ({{ micro }})", - "manager_components_region_continent_AKL": "APAC", - "manager_components_region_HEL": "Helsínquia", - "manager_components_region_HEL_micro": "Helsínquia ({{ micro }})", - "manager_components_region_continent_HEL": "Norte da Europa", - "manager_components_region_HOU": "Houston", - "manager_components_region_HOU_micro": "Houston ({{ micro }})", - "manager_components_region_continent_HOU": "América do Norte", - "manager_components_region_SOF": "Sófia", - "manager_components_region_SOF_micro": "Sófia ({{ micro }})", - "manager_components_region_continent_SOF": "Europa Central", - "manager_components_region_OSL": "Oslo", - "manager_components_region_OSL_micro": "Oslo ({{ micro }})", - "manager_components_region_continent_OSL": "Norte da Europa", - "manager_components_region_STO": "Estocolmo", - "manager_components_region_STO_micro": "Estocolmo ({{ micro }})", - "manager_components_region_continent_STO": "Norte da Europa", - "manager_components_region_TPE": "Taipé", - "manager_components_region_TPE_micro": "Taipé ({{ micro }})", - "manager_components_region_continent_TPE": "APAC", - "manager_components_region_SCL": "Santiago", - "manager_components_region_SCL_micro": "Santiago ({{ micro }})", - "manager_components_region_continent_SCL": "América do Sul", - "manager_components_region_SEL": "Seul", - "manager_components_region_SEL_micro": "Seul ({{ micro }})", - "manager_components_region_continent_SEL": "APAC", - "manager_components_region_BOG": "Bogotá", - "manager_components_region_BOG_micro": "Bogotá ({{ micro }})", - "manager_components_region_continent_BOG": "América do Sul", - "manager_components_region_LAG": "Lagos", - "manager_components_region_LAG_micro": "Lagos ({{ micro }})", - "manager_components_region_continent_LAG": "África", - "manager_components_region_CPT": "Cidade do Cabo", - "manager_components_region_CPT_micro": "Cidade do Cabo ({{ micro }})", - "manager_components_region_continent_CPT": "África", - "manager_components_region_NBO": "Nairobi", - "manager_components_region_NBO_micro": "Nairobi ({{ micro }})", - "manager_components_region_continent_NBO": "África", - "manager_components_region_ABJ": "Abidjan", - "manager_components_region_ABJ_micro": "Abidjan ({{ micro }})", - "manager_components_region_continent_ABJ": "África", - "manager_components_region_CAI": "Cairo", - "manager_components_region_CAI_micro": "Cairo ({{ micro }})", - "manager_components_region_continent_CAI": "África", - "manager_components_region_DOH": "Doha", - "manager_components_region_DOH_micro": "Doha ({{ micro }})", - "manager_components_region_continent_DOH": "Médio Oriente", - "manager_components_region_LAU": "Lausanne", - "manager_components_region_LAU_micro": "Lausanne ({{ micro }})", - "manager_components_region_continent_LAU": "Europa Ocidental", - "manager_components_region_CPH": "Copenhaga", - "manager_components_region_CPH_micro": "Copenhaga ({{ micro }})", - "manager_components_region_continent_CPH": "Norte da Europa", - "manager_components_region_LIS": "Lisboa", - "manager_components_region_LIS_micro": "Lisboa ({{ micro }})", - "manager_components_region_continent_LIS": "Sul da Europa", - "manager_components_region_MEL": "Melbourne", - "manager_components_region_MEL_micro": "Melbourne ({{ micro }})", - "manager_components_region_continent_MEL": "APAC", - "manager_components_region_ICD": "Nova Deli", - "manager_components_region_ICD_micro": "Nova Deli ({{ micro }})", - "manager_components_region_continent_ICD": "APAC", - "manager_components_region_OSA": "Osaka", - "manager_components_region_OSA_micro": "Osaka ({{ micro }})", - "manager_components_region_continent_OSA": "APAC", - "manager_components_region_KUL": "Kuala Lumpur", - "manager_components_region_KUL_micro": "Kuala Lumpur ({{ micro }})", - "manager_components_region_continent_KUL": "APAC", - "manager_components_region_MNL": "Manila", - "manager_components_region_MNL_micro": "Manila ({{ micro }})", - "manager_components_region_continent_MNL": "APAC", - "manager_components_region_SGN": "Ho Chi Minh", - "manager_components_region_SGN_micro": "Ho Chi Minh ({{ micro }})", - "manager_components_region_continent_SGN": "APAC", - "manager_components_region_VIE": "Viena", - "manager_components_region_VIE_micro": "Viena ({{ micro }})", - "manager_components_region_continent_VIE": "Europa Ocidental", - "manager_components_region_DLN": "Dublin", - "manager_components_region_DLN_micro": "Dublin ({{ micro }})", - "manager_components_region_continent_DLN": "Europa Ocidental", - "manager_components_region_VAN": "Vancouver", - "manager_components_region_VAN_micro": "Vancouver ({{ micro }})", - "manager_components_region_continent_VAN": "América do Norte", - "manager_components_region_TLV": "Telavive-Yafo", - "manager_components_region_TLV_micro": "Telavive-Yafo ({{ micro }})", - "manager_components_region_continent_TLV": "Médio Oriente", - "manager_components_region_MNC": "Manchester", - "manager_components_region_MNC_micro": "Manchester ({{ micro }})", - "manager_components_region_continent_MNC": "Europa Ocidental", - "manager_components_region_CLT": "Charlotte", - "manager_components_region_CLT_micro": "Charlotte ({{ micro }})", - "manager_components_region_continent_CLT": "América do Norte", - "manager_components_region_BNA": "Nashville", - "manager_components_region_BNA_micro": "Nashville ({{ micro }})", - "manager_components_region_continent_BNA": "América do Norte", - "manager_components_region_SLC": "Salt Lake City", - "manager_components_region_SLC_micro": "Salt Lake City ({{ micro }})", - "manager_components_region_continent_SLC": "América do Norte", - "manager_components_region_STL": "Saint-Louis", - "manager_components_region_STL_micro": "Saint-Louis ({{ micro }})", - "manager_components_region_continent_STL": "América do Norte", - "manager_components_region_IND": "Indianapolis", - "manager_components_region_IND_micro": "Indianapolis ({{ micro }})", - "manager_components_region_continent_IND": "América do Norte", - "manager_components_region_IST": "Istambul", - "manager_components_region_IST_micro": "Istambul ({{ micro }})", - "manager_components_region_PHX": "Phoenix", - "manager_components_region_PHX_micro": "Phoenix ({{ micro }})", - "manager_components_region_continent_PHX": "América do Norte", - "manager_components_region_continent_IST": "Médio Oriente", - "manager_components_region_localize": "Localizar", - "manager_components_region_location_SBG": "Europa Central (França)", - "manager_components_region_location_WAW": "Europa Central (Polónia)", - "manager_components_region_location_BHS": "América do Norte (Canadá)", - "manager_components_region_location_ERI": "Europa Ocidental (Reino Unido)", - "manager_components_region_location_GRA": "Europa Ocidental (França)", - "manager_components_region_location_PAR": "Europa Ocidental (França)", - "manager_components_region_location_GS": "Western Europe", - "manager_components_region_location_MAD": "Western Europe", - "manager_components_region_location_BRU": "Western Europe", - "manager_components_region_location_LIM": "Europa Central (Alemanha)", - "manager_components_region_location_RBX": "Europa Ocidental (França)", - "manager_components_region_location_DE": "Europa Central (Alemanha)", - "manager_components_region_location_UK": "Europa Ocidental (Reino Unido)", - "manager_components_region_location_SGP": "Ásia-Pacífico (Singapura)", - "manager_components_region_location_MUM": "Ásia-Pacífico (Mumbai)", - "manager_components_region_location_SYD": "Oceânia (Austrália)", - "manager_components_region_location_US": "Estados Unidos", - "manager_components_region_location_DAL": "Estados Unidos", - "manager_components_region_continent_SBG": "Europa Central", - "manager_components_region_continent_WAW": "Europa Central", - "manager_components_region_continent_BHS": "América do Norte", - "manager_components_region_continent_GRA": "Europa Ocidental", - "manager_components_region_continent_RBX": "Europa Ocidental", - "manager_components_region_continent_GS": "Europa Ocidental", - "manager_components_region_continent_MAD": "Sul da Europa", - "manager_components_region_continent_BRU": "Europa Ocidental", - "manager_components_region_continent_DE": "Europa Central", - "manager_components_region_continent_UK": "Europa Ocidental", - "manager_components_region_continent_SGP": "Ásia-Pacífico", - "manager_components_region_continent_MUM": "Ásia-Pacífico", - "manager_components_region_continent_SYD": "Oceânia", - "manager_components_region_continent_SHA": "Europa Ocidental", - "manager_components_region_continent_CA": "América do Norte", - "manager_components_region_continent_MRS": "Europa Ocidental", - "manager_components_region_continent_PAR": "Europa Ocidental" -} diff --git a/packages/manager-react-components/src/hooks/region/translations/index.ts b/packages/manager-react-components/src/hooks/region/translations/index.ts deleted file mode 100644 index adab359b147d..000000000000 --- a/packages/manager-react-components/src/hooks/region/translations/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { buildTranslationManager } from '../../../utils/translation-helper'; - -const translationLoaders = { - de_DE: () => import('./Messages_de_DE.json'), - en_GB: () => import('./Messages_en_GB.json'), - es_ES: () => import('./Messages_es_ES.json'), - fr_CA: () => import('./Messages_fr_CA.json'), - fr_FR: () => import('./Messages_fr_FR.json'), - it_IT: () => import('./Messages_it_IT.json'), - pl_PL: () => import('./Messages_pl_PL.json'), - pt_PT: () => import('./Messages_pt_PT.json'), -}; - -buildTranslationManager(translationLoaders, 'region'); diff --git a/packages/manager-react-components/src/hooks/region/useTranslatedMicroRegions.tsx b/packages/manager-react-components/src/hooks/region/useTranslatedMicroRegions.tsx deleted file mode 100644 index bc6c4b7460a3..000000000000 --- a/packages/manager-react-components/src/hooks/region/useTranslatedMicroRegions.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import './translations'; - -export const isLocalZone = (region: string) => { - const localZonePattern = /^lz/i; - return localZonePattern.test( - region - .split('-') - ?.slice(2) - ?.join('-'), - ); -}; - -export const getMacroRegion = (region: string): string => { - const regionSubStrings = region.split('-'); - const macroRegionMap = [ - null, - regionSubStrings[0].split(/(\d)/)[0], - regionSubStrings[0], - regionSubStrings[2], - regionSubStrings[2] === 'LZ' ? regionSubStrings[3] : regionSubStrings[2], - regionSubStrings[3], - ]; - return macroRegionMap[regionSubStrings.length] || 'Unknown_Macro_Region'; -}; - -export const useTranslatedMicroRegions = () => { - const { i18n, t } = useTranslation('region'); - - return { - translateMicroRegion: (region: string) => { - const macro = getMacroRegion(region); - if (i18n.exists(`region:manager_components_region_${macro}_micro`)) { - return t(`manager_components_region_${macro}_micro`, { micro: region }); - } - return ''; - }, - translateMacroRegion: (region: string) => { - const macro = getMacroRegion(region); - if (i18n.exists(`region:manager_components_region_${macro}`)) { - return t(`manager_components_region_${macro}`); - } - return ''; - }, - translateContinentRegion: (region: string) => { - const macro = getMacroRegion(region); - if (i18n.exists(`region:manager_components_region_continent_${macro}`)) { - return t(`manager_components_region_continent_${macro}`); - } - return ''; - }, - }; -}; diff --git a/packages/manager-react-components/src/hooks/services/api/get.ts b/packages/manager-react-components/src/hooks/services/api/get.ts deleted file mode 100644 index 0fa45d514a38..000000000000 --- a/packages/manager-react-components/src/hooks/services/api/get.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { apiClient } from '@ovh-ux/manager-core-api'; -import { ServiceDetails } from '../services.type'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type GetResourceServiceIdParams = { - /** Filter on a specific service family */ - resourceName: string; -}; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getResourceServiceIdQueryKey = ({ - resourceName = '', -}: GetResourceServiceIdParams) => [`get/services${resourceName}`]; - -/** - * allowedServices operations : List all services allowed in this vrack - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getResourceServiceId = async ({ - resourceName, -}: GetResourceServiceIdParams) => - apiClient.v6.get( - `/services${resourceName ? `?resourceName=${resourceName}` : ''}`, - ); - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getServiceDetails = async (serviceId: number | string) => - apiClient.v6.get(`/services/${serviceId}`); diff --git a/packages/manager-react-components/src/hooks/services/api/index.ts b/packages/manager-react-components/src/hooks/services/api/index.ts deleted file mode 100644 index f55f5725b9d9..000000000000 --- a/packages/manager-react-components/src/hooks/services/api/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -export * from './get'; -export * from './put'; -export * from './post'; diff --git a/packages/manager-react-components/src/hooks/services/api/post.ts b/packages/manager-react-components/src/hooks/services/api/post.ts deleted file mode 100644 index e5b80bd90a48..000000000000 --- a/packages/manager-react-components/src/hooks/services/api/post.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { apiClient } from '@ovh-ux/manager-core-api'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type DeleteServiceParams = { - serviceId: number; -}; - -/** - * Terminiate a service - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const deleteService = async ({ serviceId }: DeleteServiceParams) => - apiClient.v6.post<{ message: string }>(`/services/${serviceId}/terminate`); diff --git a/packages/manager-react-components/src/hooks/services/api/put.ts b/packages/manager-react-components/src/hooks/services/api/put.ts deleted file mode 100644 index 5078ace53580..000000000000 --- a/packages/manager-react-components/src/hooks/services/api/put.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { apiClient } from '@ovh-ux/manager-core-api'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UpdateServiceNameParams = { - /** Service id */ - serviceId: number; - /** Service new display name */ - displayName: string; -}; - -/** - * Update a service's display name - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const updateServiceName = async ({ - serviceId, - displayName, -}: UpdateServiceNameParams) => - apiClient.v6.put(`/services/${serviceId}`, { - displayName, - }); diff --git a/packages/manager-react-components/src/hooks/services/hooks/index.ts b/packages/manager-react-components/src/hooks/services/hooks/index.ts deleted file mode 100644 index 3699d019e1f9..000000000000 --- a/packages/manager-react-components/src/hooks/services/hooks/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -export * from './useDeleteService'; -export * from './useUpdateServiceName'; -export * from './useServiceDetails'; diff --git a/packages/manager-react-components/src/hooks/services/hooks/useDeleteService.ts b/packages/manager-react-components/src/hooks/services/hooks/useDeleteService.ts deleted file mode 100644 index 72a24046f79a..000000000000 --- a/packages/manager-react-components/src/hooks/services/hooks/useDeleteService.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { - getResourceServiceId, - getResourceServiceIdQueryKey, - deleteService, -} from '../api'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type DeleteServiceMutationParams = { - resourceName: string; -}; - -/** - * @deprecated The constant is deprecated and will be removed in MRC V3. - */ -export const deleteServiceMutationKey = ['delete-service']; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UseDeleteServiceParams = { - onSuccess?: () => void; - onError?: (result: ApiError) => void; - mutationKey?: string[]; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useDeleteService = ({ - onSuccess, - onError, - mutationKey = deleteServiceMutationKey, -}: UseDeleteServiceParams) => { - const queryClient = useQueryClient(); - const { mutate: terminateService, ...mutation } = useMutation({ - mutationKey, - mutationFn: async ({ resourceName }: DeleteServiceMutationParams) => { - const { data } = await queryClient.fetchQuery< - ApiResponse, - ApiError - >({ - queryKey: getResourceServiceIdQueryKey({ resourceName }), - queryFn: () => getResourceServiceId({ resourceName }), - }); - return deleteService({ serviceId: data[0] }); - }, - onSuccess, - onError, - }); - - return { - terminateService, - ...mutation, - }; -}; diff --git a/packages/manager-react-components/src/hooks/services/hooks/useServiceDetails.ts b/packages/manager-react-components/src/hooks/services/hooks/useServiceDetails.ts deleted file mode 100644 index e076cf6ebaf9..000000000000 --- a/packages/manager-react-components/src/hooks/services/hooks/useServiceDetails.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { - getResourceServiceId, - getResourceServiceIdQueryKey, - getServiceDetails, -} from '../api'; -import { ServiceDetails } from '../services.type'; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getServiceDetailsQueryKey = (resourceName: string) => [ - 'service-details', - resourceName, -]; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UseServiceDetailsParams = { - queryKey?: string[]; - resourceName: string; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useServiceDetails = ({ - queryKey, - resourceName, -}: UseServiceDetailsParams) => { - const queryClient = useQueryClient(); - return useQuery, ApiError>({ - queryKey: queryKey ?? getServiceDetailsQueryKey(resourceName), - queryFn: async () => { - const { data } = await queryClient.fetchQuery< - ApiResponse, - ApiError - >({ - queryKey: getResourceServiceIdQueryKey({ resourceName }), - queryFn: () => getResourceServiceId({ resourceName }), - }); - return getServiceDetails(data[0]); - }, - }); -}; diff --git a/packages/manager-react-components/src/hooks/services/hooks/useUpdateServiceName.ts b/packages/manager-react-components/src/hooks/services/hooks/useUpdateServiceName.ts deleted file mode 100644 index 91c237dccf76..000000000000 --- a/packages/manager-react-components/src/hooks/services/hooks/useUpdateServiceName.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { ApiError, ApiResponse } from '@ovh-ux/manager-core-api'; -import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { - updateServiceName, - getResourceServiceId, - getResourceServiceIdQueryKey, -} from '../api'; - -/** - * @deprecated The constant is deprecated and will be removed in MRC V3. - */ -export const updateServiceNameMutationKey = ['put/services/displayName']; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UpdateServiceNameMutationParams = { - /** Resource name or id */ - resourceName: string; - /** Resource new display name */ - displayName?: string; -}; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UseUpdateServiceDisplayNameParams = { - onSuccess?: () => void; - onError?: (result: ApiError) => void; - mutationKey?: string[]; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useUpdateServiceDisplayName = ({ - onSuccess, - onError, - mutationKey = updateServiceNameMutationKey, -}: UseUpdateServiceDisplayNameParams) => { - const queryClient = useQueryClient(); - const { mutate: updateDisplayName, ...mutation } = useMutation({ - mutationKey, - mutationFn: async ({ - resourceName, - displayName, - }: UpdateServiceNameMutationParams) => { - const { data: servicesId } = await queryClient.fetchQuery< - ApiResponse - >({ - queryKey: getResourceServiceIdQueryKey({ resourceName }), - queryFn: () => getResourceServiceId({ resourceName }), - }); - return updateServiceName({ serviceId: servicesId[0], displayName }); - }, - onSuccess, - onError, - }); - return { - updateDisplayName, - ...mutation, - }; -}; diff --git a/packages/manager-react-components/src/hooks/services/index.ts b/packages/manager-react-components/src/hooks/services/index.ts deleted file mode 100644 index b68cf89cb666..000000000000 --- a/packages/manager-react-components/src/hooks/services/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -export * from './api'; -export * from './hooks'; -export * from './mocks'; -export * from './services.type'; diff --git a/packages/manager-react-components/src/hooks/services/mocks/index.ts b/packages/manager-react-components/src/hooks/services/mocks/index.ts deleted file mode 100644 index 8d1706bc9f66..000000000000 --- a/packages/manager-react-components/src/hooks/services/mocks/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -export * from './services.handler'; -export * from './services.mock'; diff --git a/packages/manager-react-components/src/hooks/services/mocks/services.handler.ts b/packages/manager-react-components/src/hooks/services/mocks/services.handler.ts deleted file mode 100644 index 84feaee58147..000000000000 --- a/packages/manager-react-components/src/hooks/services/mocks/services.handler.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { ServiceDetails } from '../services.type'; -import { defaultServiceResponse, servicesMockErrors } from './services.mock'; - -/** - * @deprecated Move GetServicesMocksParams out of mrc - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type GetServicesMocksParams = { - getServicesKo?: boolean; - getDetailsServicesKo?: boolean; - updateServicesKo?: boolean; - deleteServicesKo?: boolean; - serviceResponse?: ServiceDetails; -}; - -/** - * @deprecated Move getServicesMocks out of mrc - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getServicesMocks = ({ - getServicesKo, - getDetailsServicesKo, - updateServicesKo, - deleteServicesKo, - serviceResponse = defaultServiceResponse, -}: GetServicesMocksParams): any[] => [ - { - url: '/services/:id/terminate', - response: () => - deleteServicesKo - ? { - message: servicesMockErrors.delete, - } - : null, - status: deleteServicesKo ? 500 : 200, - method: 'post', - api: 'v6', - }, - { - url: '/services/:id', - response: () => - updateServicesKo - ? { - message: servicesMockErrors.update, - } - : null, - status: updateServicesKo ? 500 : 200, - method: 'put', - api: 'v6', - }, - { - url: '/services/:id', - response: () => - getDetailsServicesKo - ? { - message: servicesMockErrors.getDetails, - } - : serviceResponse, - status: getDetailsServicesKo ? 500 : 200, - method: 'get', - api: 'v6', - }, - { - url: '/services', - response: () => - getServicesKo - ? { - message: servicesMockErrors.get, - } - : [1234567890], - status: getServicesKo ? 500 : 200, - method: 'get', - api: 'v6', - }, -]; diff --git a/packages/manager-react-components/src/hooks/services/mocks/services.mock.ts b/packages/manager-react-components/src/hooks/services/mocks/services.mock.ts deleted file mode 100644 index 021d4066b797..000000000000 --- a/packages/manager-react-components/src/hooks/services/mocks/services.mock.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { CurrencyCode } from '../../../enumTypes'; -import { ServiceDetails } from '../services.type'; - -/** - * @deprecated The constant is deprecated and will be removed in MRC V3. - */ -export const servicesMockErrors = { - delete: 'Delete services error', - update: 'Update services error', - get: 'Get services error', - getDetails: 'Get services details error', -}; - -/** - * @deprecated The constant is deprecated and will be removed in MRC V3. - */ -export const defaultServiceResponse: ServiceDetails = { - route: { - path: '/api/path/{id}', - url: '/api/path/id', - vars: [ - { - key: 'id', - value: 'id', - }, - ], - }, - billing: { - nextBillingDate: '2024-11-21T09:03:18Z', - expirationDate: '2024-11-21T09:03:18Z', - plan: { - code: 'code', - invoiceName: 'invoiceName', - }, - pricing: { - capacities: ['renew'], - description: 'Installation pricing', - interval: 1, - duration: 'P1M', - minimumQuantity: 1, - maximumQuantity: null, - minimumRepeat: 1, - maximumRepeat: null, - price: { currencyCode: CurrencyCode.EUR, text: '0.00 €', value: 0 }, - priceInUcents: 0, - pricingMode: 'default', - pricingType: 'rental', - engagementConfiguration: null, - }, - group: null, - lifecycle: { - current: { - pendingActions: [], - terminationDate: null, - creationDate: '2024-10-21T09:03:18Z', - state: 'active', - }, - capacities: { - actions: ['earlyRenewal', 'terminateAtExpirationDate'], - }, - }, - renew: { - current: { - mode: 'automatic', - nextDate: '2024-11-21T09:03:18Z', - period: 'P1M', - }, - capacities: { mode: ['automatic', 'manual'] }, - }, - engagement: null, - engagementRequest: null, - }, - resource: { - displayName: 'Test', - name: 'id-test', - state: 'active', - product: { - name: 'test', - description: 'description', - }, - resellingProvider: null, - }, - serviceId: 1234567890, - parentServiceId: null, - customer: { - contacts: [ - { - customerCode: 'adminCustomerCode', - type: 'administrator', - }, - { - customerCode: 'technicalCustomerCode', - type: 'technical', - }, - { - customerCode: 'billingCustomerCode', - type: 'billing', - }, - ], - }, - tags: [], -}; diff --git a/packages/manager-react-components/src/hooks/services/services.type.ts b/packages/manager-react-components/src/hooks/services/services.type.ts deleted file mode 100644 index 0b232662e1ab..000000000000 --- a/packages/manager-react-components/src/hooks/services/services.type.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import { CurrencyCode } from '../../enumTypes'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type EndRuleStrategy = - | 'CANCEL_SERVICE' - | 'REACTIVATE_ENGAGEMENT' - | 'STOP_ENGAGEMENT_FALLBACK_DEFAULT_PRICE' - | 'STOP_ENGAGEMENT_KEEP_PRICE'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type LifecycleAction = - | 'earlyRenewal' - | 'terminate' - | 'terminateAtEngagementDate' - | 'terminateAtExpirationDate'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type LifecycleState = - | 'active' - | 'error' - | 'rupture' - | 'terminated' - | 'toRenew' - | 'unpaid' - | 'unrenewed'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type PricingCapacity = - | 'consumption' - | 'detach' - | 'downgrade' - | 'dynamic' - | 'installation' - | 'renew' - | 'upgrade'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type EndAction = - | 'CANCEL_SERVICE' - | 'REACTIVATE_ENGAGEMENT' - | 'STOP_ENGAGEMENT_FALLBACK_DEFAULT_PRICE' - | 'STOP_ENGAGEMENT_KEEP_PRICE'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type RenewMode = 'automatic' | 'manual'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type EngagementType = 'periodic' | 'upfront'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type PricingType = 'consumption' | 'purchase' | 'rental'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type CustomerContact = { - customerCode: string; - type: 'administrator' | 'billing' | 'technical'; -}; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type ResourceStatus = - | 'active' - | 'deleted' - | 'suspended' - | 'toActivate' - | 'toDelete' - | 'toSuspend'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type ServiceDetails = { - billing: { - engagement: { - endDate: string | null; - /** - * Describes the rule applied at the end of the Engagement - */ - endRule: { - possibleStrategies: EndRuleStrategy[]; - strategy: EndRuleStrategy; - } | null; - } | null; - engagementRequest: { - pricingMode: string; - requestDate: string; - }; - expirationDate: string; - group: { - id: number; - }; - lifecycle: { - capacities: { - actions: LifecycleAction[]; - }; - current: { - creationDate: string; - pendingActions: LifecycleAction[]; - state: LifecycleState; - terminationDate: string; - }; - }; - nextBillingDate: string; - plan: { - code: string; - invoiceName: string; - }; - pricing: { - capacities: PricingCapacity[]; - description: string; - duration: string; - engagementConfiguration: { - defaultEndAction: EndAction; - duration: string; - type: EngagementType; - }; - interval: number; - maximumQuantity: number | null; - maximumRepeat: number | null; - minimumQuantity: number; - minimumRepeat: number; - price: { - currencyCode: CurrencyCode; - priceInUcents?: number | null; - text: string; - value: number; - }; - priceInUcents?: number; - pricingMode: string; - pricingType: PricingType; - }; - renew: { - capacities: { mode: RenewMode[] }; - current: { - mode: RenewMode | null; - nextDate: string | null; - period: string; - }; - }; - }; - customer: { - contacts: CustomerContact[]; - }; - parentServiceId: number | null; - resource: { - displayName: string; - name: string; - product: { - description: string; - name: string; - }; - resellingProvider: 'ovh.ca' | 'ovh.eu'; - state: ResourceStatus; - }; - route: { - path: string; - url: string; - vars: { key: string; value: string }[]; - }; - serviceId: number; - tags: string[]; -}; diff --git a/packages/manager-react-components/src/hooks/tasks/hooks/useTask.ts b/packages/manager-react-components/src/hooks/tasks/hooks/useTask.ts deleted file mode 100644 index c6c65bcc848a..000000000000 --- a/packages/manager-react-components/src/hooks/tasks/hooks/useTask.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -import React from 'react'; -import { ApiError, ApiResponse, apiClient } from '@ovh-ux/manager-core-api'; -import { useQuery } from '@tanstack/react-query'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type UseTaskParams = { - resourceUrl: string; - apiVersion?: 'v2' | 'v6'; - taskId?: number | string; - queryKey?: string[]; - onSuccess?: () => void; - onError?: () => void; - onFinish?: () => void; - refetchIntervalTime?: number; -}; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getDefaultQueryKey = (taskId: number | string) => [ - 'manage-task', - taskId, -]; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useTask = ({ - resourceUrl, - apiVersion = 'v2', - taskId, - queryKey, - onSuccess, - onError, - onFinish, - refetchIntervalTime = 2000, -}: UseTaskParams) => { - const [isSuccess, setIsSuccess] = React.useState(false); - const [isPending, setIsPending] = React.useState(false); - const [isError, setIsError] = React.useState(false); - - const { error } = useQuery< - ApiResponse<{ status: 'DONE' | 'PENDING' | 'RUNNING' }>, - ApiError - >({ - staleTime: 0, - queryKey: queryKey || getDefaultQueryKey(taskId || resourceUrl), - queryFn: async () => { - const url = `/${resourceUrl - .split('/') - .filter(Boolean) - .concat(['task', taskId as string]) - .join('/')}`; - - try { - setIsPending(true); - const result = await apiClient[apiVersion].get(url); - if (apiVersion === 'v2') { - if (result.data?.status === 'DONE') { - setIsPending(false); - setIsSuccess(true); - setIsError(false); - onSuccess?.(); - onFinish?.(); - } - if (result.data?.status === 'ERROR') { - setIsPending(false); - setIsSuccess(false); - setIsError(true); - onError?.(); - onFinish?.(); - throw result; - } - } - return result; - } catch (err) { - if (apiVersion === 'v6') { - if (err?.response?.status === 404) { - setIsPending(false); - setIsSuccess(true); - setIsError(false); - onSuccess?.(); - } else { - setIsPending(false); - setIsError(true); - setIsSuccess(false); - onError?.(); - } - onFinish?.(); - } - throw err; - } - }, - enabled: !!taskId, - retry: false, - refetchInterval: (query) => { - if (apiVersion === 'v6') { - return query.state.status !== 'error' ? refetchIntervalTime : undefined; - } - return !['DONE', 'ERROR'].includes(query.state.data?.data?.status) - ? refetchIntervalTime - : undefined; - }, - }); - - return { - error, - isError, - isPending, - isSuccess, - }; -}; diff --git a/packages/manager-react-components/src/hooks/tasks/index.ts b/packages/manager-react-components/src/hooks/tasks/index.ts deleted file mode 100644 index 2f9221623c5e..000000000000 --- a/packages/manager-react-components/src/hooks/tasks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' or already moved - */ -export * from './hooks/useTask'; diff --git a/packages/manager-react-components/src/hooks/useCatalogPrice.ts b/packages/manager-react-components/src/hooks/useCatalogPrice.ts deleted file mode 100644 index c48b379e73c7..000000000000 --- a/packages/manager-react-components/src/hooks/useCatalogPrice.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { useMe } from './useMe'; - -export const ASIA_FORMAT = ['SG', 'ASIA', 'AU', 'IN']; -export const FRENCH_FORMAT = [ - 'CZ', - 'ES', - 'FR', - 'GB', - 'IE', - 'IT', - 'LT', - 'MA', - 'NL', - 'PL', - 'PT', - 'TN', -]; -export const GERMAN_FORMAT = ['DE', 'FI', 'SN']; -export const HOUR_IN_MONTH = 730; - -export interface CatalogPriceOptions { - hideTaxLabel?: boolean; - exclVat?: boolean; -} - -export const priceToUcent = (price: number) => price * 100_000_000; -export const priceFromUcent = (price: number) => price / 100_000_000; - -/** - * Handle the conversion on the frontend side from hourly price to monthly. - * In the future, this conversion should be handled on the backend to improve efficiency and maintainability. - * */ -export const convertHourlyPriceToMonthly = (hourlyPrice: number) => - hourlyPrice * HOUR_IN_MONTH; - -export const useCatalogPrice = ( - maximumFractionDigits?: number, - options?: CatalogPriceOptions, -) => { - const { i18n, t } = useTranslation('order-price'); - const { me } = useMe(); - - const isFrench = FRENCH_FORMAT.includes(me?.ovhSubsidiary); - const isAsia = ASIA_FORMAT.includes(me?.ovhSubsidiary); - const isGerman = GERMAN_FORMAT.includes(me?.ovhSubsidiary); - const isTaxExcl = options?.exclVat || isAsia || isFrench || isGerman; - - const getTextPrice = (priceInCents: number) => { - const priceToFormat = priceFromUcent(priceInCents); - const numberFormatOptions = { - style: 'currency', - currency: me?.currency?.code, - ...(maximumFractionDigits !== undefined ? { maximumFractionDigits } : {}), - }; - return me - ? new Intl.NumberFormat( - i18n.language?.replace('_', '-'), - numberFormatOptions as Parameters[1], - ).format(priceToFormat) - : ''; - }; - - const getFormattedCatalogPrice = (price: number): string => - isTaxExcl && !options?.hideTaxLabel && !isGerman - ? t('order_catalog_price_tax_excl_label', { - price: getTextPrice(price), - }) - : getTextPrice(price); - - const getFormattedHourlyCatalogPrice = (price: number) => - `${getFormattedCatalogPrice(price)} / ${t( - `order_catalog_price_interval_hour`, - )}`; - - const getFormattedMonthlyCatalogPrice = (price: number) => - `${getFormattedCatalogPrice(price)} / ${t( - `order_catalog_price_interval_month`, - )}`; - - return { - getTextPrice, - getFormattedCatalogPrice, - getFormattedHourlyCatalogPrice, - getFormattedMonthlyCatalogPrice, - }; -}; diff --git a/packages/manager-react-components/src/hooks/useMe.ts b/packages/manager-react-components/src/hooks/useMe.ts deleted file mode 100644 index 94332939d6b6..000000000000 --- a/packages/manager-react-components/src/hooks/useMe.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; -import { ShellContext } from '@ovh-ux/manager-react-shell-client'; - -export interface IMe { - ovhSubsidiary: string; - currency: { - code: string; - }; -} - -export const useMe = () => { - const context = useContext(ShellContext); - const [me, setMe] = useState(null); - - useEffect(() => { - setMe(context?.environment?.getUser()); - }, [context?.environment]); - - return { me }; -}; diff --git a/packages/manager-react-components/src/hooks/useProjectRegions.ts b/packages/manager-react-components/src/hooks/useProjectRegions.ts deleted file mode 100644 index a4d3b2ad045d..000000000000 --- a/packages/manager-react-components/src/hooks/useProjectRegions.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-pci-common - */ -import { fetchIcebergV6 } from '@ovh-ux/manager-core-api'; -import { useQuery } from '@tanstack/react-query'; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -type TRegionService = { - endpoint: string; - name: string; - status: string; -}; - -/** - * @deprecated The type is deprecated and will be removed in MRC V3. - */ -export type TRegion = { - continentCode: string; - datacenterLocation: string; - name: string; - status: string; - type: string; - ipCountries: string[]; - services: TRegionService[]; -}; - -/** - * @deprecated This function is deprecated and will be removed in MRC V3. - */ -export const getProjectRegions = async ( - projectId: string, -): Promise => { - const { data } = await fetchIcebergV6({ - route: `/cloud/project/${projectId}/region`, - }); - return data; -}; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectRegions = (projectId: string) => - useQuery({ - queryKey: ['project', projectId, 'regions'], - queryFn: () => getProjectRegions(projectId), - }); - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectLocalRegions = (projectId: string) => - useQuery({ - queryKey: ['project', projectId, 'regions', 'local'], - queryFn: () => getProjectRegions(projectId), - select: (regions) => - regions.filter(({ type = [] }) => type === 'localzone'), - }); - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectNonLocalRegions = (projectId: string) => - useQuery({ - queryKey: ['project', projectId, 'regions', 'non-local'], - queryFn: () => getProjectRegions(projectId), - select: (regions) => - regions.filter(({ type = [] }) => type !== 'localzone'), - }); diff --git a/packages/manager-react-components/src/hooks/useProjectUrl.ts b/packages/manager-react-components/src/hooks/useProjectUrl.ts deleted file mode 100644 index 894bb92da989..000000000000 --- a/packages/manager-react-components/src/hooks/useProjectUrl.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @deprecated This file is deprecated. Do not use any of its exports. - * @deprecated file will be removed in MRC v3, all code will be move in @ovh-ux/manager-module-common-api' - */ -import { useParams } from 'react-router-dom'; -import { useContext, useEffect, useState } from 'react'; -import { ShellContext } from '@ovh-ux/manager-react-shell-client'; - -/** - * @deprecated This hook is deprecated and will be removed in MRC V3. - */ -export const useProjectUrl = (appName: string) => { - const { projectId } = useParams(); - const { navigation } = useContext(ShellContext).shell; - - const [url, setUrl] = useState('public-cloud'); - - useEffect(() => { - navigation - .getURL(appName, `#/pci/projects/${projectId}`, {}) - .then((data) => { - setUrl(data as string); - }); - }, [projectId, navigation, appName]); - - return url; -}; diff --git a/packages/manager-react-components/src/index.ts b/packages/manager-react-components/src/index.ts deleted file mode 100644 index 18cdbf1cf5b2..000000000000 --- a/packages/manager-react-components/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './tailwind/theme.css'; -import '@ovhcloud/ods-themes/default'; -import './enumTypes'; -import './lib.scss'; - -export * from './hooks/datagrid'; -export * from './components'; diff --git a/packages/manager-react-components/src/lib.scss b/packages/manager-react-components/src/lib.scss deleted file mode 100644 index 6c4e569bdb8f..000000000000 --- a/packages/manager-react-components/src/lib.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import './components/templates/error/error.scss'; -@import './components/templates/update-name-modal/update-name-modal.scss'; -@import './components/filters/filters.scss'; -@import './components/navigation/menus/action/action.scss'; -@import './components/action-banner/action-banner.scss'; -@import './components/drawer/drawer.scss'; diff --git a/packages/manager-react-components/src/lib.ts b/packages/manager-react-components/src/lib.ts deleted file mode 100644 index 540d305f6f32..000000000000 --- a/packages/manager-react-components/src/lib.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './lib.scss'; - -export * from './hooks'; -export * from './hooks/feature-availability/useFeatureAvailability'; -export * from './components'; -export * from './enumTypes'; -export * from './utils/click-utils'; diff --git a/packages/manager-react-components/src/utils/index.ts b/packages/manager-react-components/src/utils/index.ts deleted file mode 100644 index cb135c0e767b..000000000000 --- a/packages/manager-react-components/src/utils/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -export const uniqBy = function uniqBy(arr: I[], cb: (item: I) => U) { - return [ - ...arr - .reduce((map: Map, item?: I) => { - if (!map.has(cb(item))) map.set(cb(item), item); - - return map; - }, new Map()) - .values(), - ]; -}; - -// eslint-disable-next-line -export const hashCode = (param: any) => { - let h = 0; - const s = ((p): string => { - switch (typeof p) { - case 'number': - return `${p}`; - case 'bigint': - return `${p}`; - case 'string': - return `${p}`; - case 'boolean': - return `${p}`; - case 'object': - return JSON.stringify(p); - case 'function': - return 'function'; - case 'undefined': - return 'undefined'; - default: - return 'symbol'; - } - })(param); - const l = s?.length || 0; - let i = 0; - // eslint-disable-next-line - if (l > 0) while (i < l) h = ((h << 5) - h + s.charCodeAt(i++)) | 0; - return h; -}; diff --git a/packages/manager-react-components/src/utils/test.provider.tsx b/packages/manager-react-components/src/utils/test.provider.tsx deleted file mode 100644 index 556d2501df79..000000000000 --- a/packages/manager-react-components/src/utils/test.provider.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { ComponentType } from 'react'; -import { render, RenderOptions, RenderResult } from '@testing-library/react'; -import { I18nextProvider } from 'react-i18next'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import i18n from '../../.storybook/i18n'; - -import '@testing-library/jest-dom'; -import 'element-internals-polyfill'; - -export const queryClient = new QueryClient({ - defaultOptions: { - mutations: { - retry: false, - }, - queries: { - retry: false, - }, - }, -}); - -const Wrappers = ({ children }: { children: React.ReactElement }) => { - return ( - - {children} - - ); -}; - -const customRender = ( - ui: React.JSX.Element, - options?: Omit, -): RenderResult => - render(ui, { wrapper: Wrappers as ComponentType, ...options }); - -export { customRender as render }; diff --git a/packages/manager-react-components/src/utils/translation-helper.ts b/packages/manager-react-components/src/utils/translation-helper.ts deleted file mode 100644 index b9c93fb336eb..000000000000 --- a/packages/manager-react-components/src/utils/translation-helper.ts +++ /dev/null @@ -1,97 +0,0 @@ -import i18next from 'i18next'; - -/** - * Manager Fallback Language - */ -const fallbackLang = 'fr_FR'; - -/** - * Normalizes a language code to a format compatible with i18next loaders. - * Converts hyphenated codes to underscore format and ensures region codes exist. - * - * Examples: - * - 'fr-FR' → 'fr_FR' - * - 'en' → 'en_GB' - * - 'de' → 'de_DE' - * - * @param {string} language - The language code from browser or user input. - * @returns {string} - Normalized language code in the format `ll_RR` (e.g., 'en_GB'). - */ -export const normalizeLanguageCode = (language) => { - let normalizedLang = language.replace('-', '_'); - - if (!normalizedLang.includes('_')) { - if (normalizedLang === 'en') { - normalizedLang = 'en_GB'; - } else { - normalizedLang = `${normalizedLang}_${normalizedLang.toUpperCase()}`; - } - } - - return normalizedLang; -}; - -/** - * Builds a resource loader function for dynamic translation loading. - * This function is partially applied with a `translationLoaders` map and a namespace. - * @param translationLoaders - A map of language codes to dynamic import functions for translations. - * @param namespace - The namespace under which translations will be added. - */ -export const buildTranslationResources = - (translationLoaders, namespace) => async (language) => { - const normalizedLang = normalizeLanguageCode(language); - - // Always load fallbackLang once, before the user language - if (!i18next.hasResourceBundle(fallbackLang, namespace)) { - try { - console.info(`Loading fallback language: ${fallbackLang} for namespace: ${namespace}`); - const fallbackModule = await translationLoaders[fallbackLang](); - i18next.addResources(fallbackLang, namespace, fallbackModule.default || fallbackModule); - } catch (error) { - console.error(`Failed to load fallback translations (${fallbackLang}):`, error); - } - } - - // Then load the requested language - if (normalizedLang !== fallbackLang && !i18next.hasResourceBundle(normalizedLang, namespace)) { - try { - const module = await translationLoaders[normalizedLang](); - i18next.addResources(normalizedLang, namespace, module.default || module); - } catch (error) { - console.warn(`Could not load ${normalizedLang}. Will fallback to ${fallbackLang}.`, error); - } - } - - return true; - }; - -/** - * Initializes and manages i18next language updates and resource loading. - * Listens to i18next events and browser language changes to update translations dynamically. - * - * @param {Record Promise>} translationLoaders - - * A map of language codes to dynamic import functions. - * @param {string} namespace - The namespace to be used for loading translations. - */ -export const buildTranslationManager = (translationLoaders, namespace) => { - const loadTranslations = buildTranslationResources( - translationLoaders, - namespace, - ); - - const handleLanguageChange = async (lang) => { - const normalizedLang = normalizeLanguageCode(lang); - await loadTranslations(normalizedLang); - console.log('Language changed to:', normalizedLang); - }; - - if (i18next.isInitialized) { - handleLanguageChange(i18next.language); - } else { - i18next.on('initialized', () => { - handleLanguageChange(i18next.language); - }); - } - - i18next.on('languageChanged', handleLanguageChange); -}; diff --git a/packages/manager-react-components/src/vite-env.d.ts b/packages/manager-react-components/src/vite-env.d.ts deleted file mode 100644 index 1055a925e0e7..000000000000 --- a/packages/manager-react-components/src/vite-env.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -declare const __VERSION__: string; -// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -declare const __REGION__: string; -declare module '*.jpg' { - const path: string; - export default path; -} -declare module '*.jpeg' { - const path: string; - export default path; -} -declare module '*.svg' { - const path: string; - export default path; -} -declare module '*.png' { - const path: string; - export default path; -} diff --git a/packages/manager-react-components/tsconfig.json b/packages/manager-react-components/tsconfig.json deleted file mode 100644 index abcf6d446ddb..000000000000 --- a/packages/manager-react-components/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "es2021"], - "target": "ES2020", - "types": ["vite/client", "node", "jest"], - "module": "ESNext", - "outDir": "dist", - "allowJs": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "moduleResolution": "Node", - "isolatedModules": true, - "declaration": true, - "declarationDir": "./dist/types", - "jsx": "react-jsx" - }, - "files": ["global.d.ts"], - "include": ["src/**/*", "*.spec.tsx"], - "exclude": ["./src/**/*.stories.*"] -} diff --git a/packages/manager-react-components/vitest.config.js b/packages/manager-react-components/vitest.config.js deleted file mode 100644 index 99b768e2f4b0..000000000000 --- a/packages/manager-react-components/vitest.config.js +++ /dev/null @@ -1,33 +0,0 @@ -import path from 'path'; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - test: { - setupFiles: 'vitest.setup.js', - globals: true, - environment: 'jsdom', - coverage: { - include: ['src'], - exclude: [], - }, - testTimeout: 60000, - fileParallelism: false, - maxWorkers: 1, - pollOptions: { - forks: { - singleFork: true, - }, - threads: { - singleThread: true, - }, - }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - }, - mainFields: ['module'], - }, -}); diff --git a/packages/manager-react-components/vitest.setup.js b/packages/manager-react-components/vitest.setup.js deleted file mode 100644 index a5cd04853bae..000000000000 --- a/packages/manager-react-components/vitest.setup.js +++ /dev/null @@ -1,26 +0,0 @@ -// vitest.setup.js -import '@testing-library/jest-dom'; -import 'element-internals-polyfill'; - -// This polyfill exists because of an issue with jsdom and the EventTarget class -// when testing a component with an OdsDatepicker (addEventListener crashes at component initialization). -// Fix from issue: https://github.com/jsdom/jsdom/issues/2156 -global.EventTarget = class { - listeners = {}; - - addEventListener(type, listener) { - this.listeners = this.listeners || {}; - (this.listeners[type] || (this.listeners[type] = new Set())).add(listener); - } - - removeEventListener(type, listener) { - if (this.listeners && this.listeners[type]) { - this.listeners[type].delete(listener); - } - } - - dispatchEvent(event) { - this.listeners[event.type].forEach((listener) => listener(event)); - return !event.defaultPrevented; - } -}; diff --git a/packages/manager-tools/manager-generator/template/package.json b/packages/manager-tools/manager-generator/template/package.json index eedfc227a09a..76e362a0b4a3 100644 --- a/packages/manager-tools/manager-generator/template/package.json +++ b/packages/manager-tools/manager-generator/template/package.json @@ -1,8 +1,8 @@ { "name": "{{packageName}}", "version": "0.0.0", - "description": "{{description}}", "private": true, + "description": "{{description}}", "repository": { "type": "git", "url": "git+https://github.com/ovh/manager.git", @@ -24,13 +24,13 @@ "@ovh-ux/manager-core-api": "*", "@ovh-ux/manager-core-utils": "*", "@ovh-ux/manager-pci-common": "^2.4.2", - "@ovh-ux/manager-react-components": "^2.36.0", "@ovh-ux/manager-react-core-application": "*", "@ovh-ux/manager-react-shell-client": "*", - "@ovhcloud/ods-components": "^18.6.2", - "@ovhcloud/ods-themes": "^18.6.2", + "@ovh-ux/muk": "^0.0.1", "@ovh-ux/request-tagger": "*", "@ovh-ux/shell": "^4.5.7", + "@ovhcloud/ods-components": "^18.6.2", + "@ovhcloud/ods-themes": "^18.6.2", "@tanstack/react-query": "^5.51.21", "axios": "^1.1.2", "clsx": "^1.2.1", @@ -53,6 +53,6 @@ "{{regions}}" ], "universes": [ - "{{universes}}" + "{{universes}}" ] -} +} \ No newline at end of file diff --git a/packages/manager-tools/manager-generator/template/src/components/dashboard/general-information/GeneralInformationTile.component.tsx b/packages/manager-tools/manager-generator/template/src/components/dashboard/general-information/GeneralInformationTile.component.tsx index b46257a67baf..172061baed96 100644 --- a/packages/manager-tools/manager-generator/template/src/components/dashboard/general-information/GeneralInformationTile.component.tsx +++ b/packages/manager-tools/manager-generator/template/src/components/dashboard/general-information/GeneralInformationTile.component.tsx @@ -1,20 +1,30 @@ import React from 'react'; -import { ManagerTile } from '@ovh-ux/manager-react-components'; +import { ManagerTile } from '@ovh-ux/muk'; import { GeneralInformationProps } from '@/types/GeneralInfo.type'; -export default function GeneralInformationTile({ tiles }: GeneralInformationProps) { +export default function GeneralInformationTile({ + tiles, +}: GeneralInformationProps) { return (
{tiles.map((tile) => ( - + {tile.title} {tile.items.map((item) => ( - {item.label} - {item.value} + + {item.label} + + + {item.value} +
))} diff --git a/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.spec.ts b/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.spec.ts index fa5117abc64f..03107fa30513 100644 --- a/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.spec.ts +++ b/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.spec.ts @@ -15,7 +15,7 @@ async function loadSubject(listingApi: 'v6Iceberg' | 'v2' | 'v6' | undefined) { const useResourcesIcebergV6 = vi.fn(); const useResourcesV6 = vi.fn(); - vi.doMock('@ovh-ux/manager-react-components', () => ({ + vi.doMock('@ovh-ux/muk', () => ({ useResourcesIcebergV2, useResourcesIcebergV6, useResourcesV6, @@ -24,12 +24,12 @@ async function loadSubject(listingApi: 'v6Iceberg' | 'v2' | 'v6' | undefined) { // Import the subject AFTER mocks are set const mod = await import('./useResources'); // <-- ← adjust to your real path - return { + return ({ ...mod, mocks: { useResourcesIcebergV2, useResourcesIcebergV6, useResourcesV6 }, - } as unknown as { - useResources: (typeof import('./useResources'))['useResources']; - useListingData: (typeof import('./useResources'))['useListingData']; + } as unknown) as { + useResources: typeof import('./useResources')['useResources']; + useListingData: typeof import('./useResources')['useListingData']; mocks: { useResourcesIcebergV2: ReturnType; useResourcesIcebergV6: ReturnType; @@ -53,7 +53,10 @@ describe('useResources', () => { mocks.useResourcesIcebergV6.mockReturnValue(sample); const { result } = renderHook(() => - useResources<{ id: number }>({ route: '/things', queryKey: ['listing', '/things'] }), + useResources<{ id: number }>({ + route: '/things', + queryKey: ['listing', '/things'], + }), ); expect(mocks.useResourcesIcebergV6).toHaveBeenCalledWith({ @@ -78,7 +81,10 @@ describe('useResources', () => { mocks.useResourcesIcebergV2.mockReturnValue(sample); const { result } = renderHook(() => - useResources<{ id: string }>({ route: '/legacy', queryKey: ['listing', '/legacy'] }), + useResources<{ id: string }>({ + route: '/legacy', + queryKey: ['listing', '/legacy'], + }), ); expect(mocks.useResourcesIcebergV2).toHaveBeenCalled(); @@ -102,7 +108,11 @@ describe('useResources', () => { mocks.useResourcesV6.mockReturnValue(sample); const { result } = renderHook(() => - useResources<{ id: number }>({ route: '/new', queryKey: ['listing', '/new'], columns: [] }), + useResources<{ id: number }>({ + route: '/new', + queryKey: ['listing', '/new'], + columns: [], + }), ); expect(mocks.useResourcesV6).toHaveBeenCalledWith({ @@ -130,7 +140,9 @@ describe('useListingData', () => { status: 'success' as const, }); - const { result } = renderHook(() => useListingData<{ id: number }>('/things')); + const { result } = renderHook(() => + useListingData<{ id: number }>('/things'), + ); expect(result.current.items).toEqual([{ id: 1 }, { id: 2 }]); expect(result.current.total).toBe(999); // uses totalCount when present @@ -154,7 +166,9 @@ describe('useListingData', () => { status: 'pending' as const, }); - const { result } = renderHook(() => useListingData<{ id: string }>('/legacy')); + const { result } = renderHook(() => + useListingData<{ id: string }>('/legacy'), + ); expect(result.current.items).toHaveLength(3); expect(result.current.total).toBe(3); // fallback to items.length @@ -174,7 +188,9 @@ describe('useListingData', () => { status: 'success' as const, }); - const { result } = renderHook(() => useListingData>('/empty')); + const { result } = renderHook(() => + useListingData>('/empty'), + ); expect(result.current.items).toEqual([]); expect(result.current.total).toBe(0); diff --git a/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.ts b/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.ts index 87ee6a2c7f30..5ae7f9bef50a 100644 --- a/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.ts +++ b/packages/manager-tools/manager-generator/template/src/data/hooks/useResources.ts @@ -5,10 +5,13 @@ import { useResourcesIcebergV2, useResourcesIcebergV6, useResourcesV6, -} from '@ovh-ux/manager-react-components'; +} from '@ovh-ux/muk'; import { APP_FEATURES } from '@/App.constants'; -import { ResourcesFacadeResult, UseResourcesParams } from '@/types/ClientApi.type'; +import { + ResourcesFacadeResult, + UseResourcesParams, +} from '@/types/ClientApi.type'; import { ListingDataResultType } from '@/types/Listing.type'; function mapResponseWithTotal(response: { @@ -82,9 +85,9 @@ function createResourcesFactory>() { }; } -export function useResources = Record>( - params: UseResourcesParams, -): ResourcesFacadeResult { +export function useResources< + T extends Record = Record +>(params: UseResourcesParams): ResourcesFacadeResult { const resourcesFactory = createResourcesFactory(); const api = APP_FEATURES.listingApi; @@ -93,9 +96,9 @@ export function useResources = Record = Record>( - route: string, -): ListingDataResultType { +export function useListingData< + T extends Record = Record +>(route: string): ListingDataResultType { const raw = useResources({ route, queryKey: ['listing', route], @@ -103,7 +106,8 @@ export function useListingData = Record>(() => { const items = raw?.flattenData ?? []; - const total = typeof raw?.totalCount === 'number' ? raw.totalCount : items.length; + const total = + typeof raw?.totalCount === 'number' ? raw.totalCount : items.length; const fetchNextPage = raw?.hasNextPage && raw?.fetchNextPage diff --git a/packages/manager-tools/manager-generator/template/src/hooks/layout/useBreadcrumb.tsx b/packages/manager-tools/manager-generator/template/src/hooks/layout/useBreadcrumb.tsx index 51965f2dad5c..15523a03024f 100644 --- a/packages/manager-tools/manager-generator/template/src/hooks/layout/useBreadcrumb.tsx +++ b/packages/manager-tools/manager-generator/template/src/hooks/layout/useBreadcrumb.tsx @@ -4,7 +4,7 @@ import { useContext, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useProject } from '@ovh-ux/manager-pci-common'; -import { useProjectUrl } from '@ovh-ux/manager-react-components'; +import { useProjectUrl } from '@ovh-ux/muk'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { BreadcrumbItem, BreadcrumbProps } from '@/types/Breadcrumb.type'; @@ -56,7 +56,11 @@ export const useBreadcrumb = ({ rootLabel, appName }: BreadcrumbProps) => { useEffect(() => { const fetchRoot = async () => { try { - const response = await shell?.navigation.getURL(appName as string, '#/', {}); + const response = await shell?.navigation.getURL( + appName as string, + '#/', + {}, + ); const rootItem = { label: rootLabel, href: String(response), diff --git a/packages/manager-tools/manager-generator/template/src/hooks/listing/useListingColumns.tsx b/packages/manager-tools/manager-generator/template/src/hooks/listing/useListingColumns.tsx index 660c8a3e6d1b..547ed6383875 100644 --- a/packages/manager-tools/manager-generator/template/src/hooks/listing/useListingColumns.tsx +++ b/packages/manager-tools/manager-generator/template/src/hooks/listing/useListingColumns.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { OdsText } from '@ovhcloud/ods-components/react'; -import type { DatagridColumn } from '@ovh-ux/manager-react-components'; +import type { DatagridColumn } from '@ovh-ux/muk'; import { DefaultListingItemType } from '@/types/Listing.type'; diff --git a/packages/manager-tools/manager-generator/template/src/index.scss b/packages/manager-tools/manager-generator/template/src/index.scss index a5c684d9dcdd..c40d7f03465d 100644 --- a/packages/manager-tools/manager-generator/template/src/index.scss +++ b/packages/manager-tools/manager-generator/template/src/index.scss @@ -1,7 +1,7 @@ @tailwind utilities; @import '@ovhcloud/ods-themes/default'; -@import '@ovh-ux/manager-react-components/dist/style.css'; +@import '@ovh-ux/muk/dist/style.css'; html { font-family: var(--ods-font-family-default); diff --git a/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.page.tsx b/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.page.tsx index d9136a254042..e5a54c49e489 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.page.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.page.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { OdsTab, OdsTabs } from '@ovhcloud/ods-components/react'; -import { BaseLayout } from '@ovh-ux/manager-react-components'; +import { BaseLayout } from '@ovh-ux/muk'; import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; import { appName } from '@/App.constants'; @@ -61,11 +61,15 @@ export default function DashboardPage() { } }} > - {t(tab.title)} + + {t(tab.title)} + ))} - ) : undefined + ) : ( + undefined + ) } /> diff --git a/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.spec.tsx b/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.spec.tsx index 257e72748f78..88aa43e3ba93 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.spec.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/dashboard/Dashboard.spec.tsx @@ -44,7 +44,7 @@ interface BaseLayoutProps { breadcrumb: ReactNode; tabs: ReactNode; } -vi.mock('@ovh-ux/manager-react-components', () => ({ +vi.mock('@ovh-ux/muk', () => ({ BaseLayout: ({ header, backLinkLabel, onClickReturn, breadcrumb, tabs }: BaseLayoutProps) => (

{header.title}

diff --git a/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.page.tsx b/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.page.tsx index ccdf1eebc2c5..e919d6f33071 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.page.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.page.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { OdsText } from '@ovhcloud/ods-components/react'; -import { Clipboard, LinkType, Links } from '@ovh-ux/manager-react-components'; +import { Clipboard, LinkType, Links } from '@ovh-ux/muk'; import GeneralInformationTile from '@/components/dashboard/general-information/GeneralInformationTile.component'; @@ -16,13 +16,18 @@ export default function GeneralInformationPage() { id: 'description', label: 'Description', value: ( - Hosted Private Cloud – VMware vSphere service running in GRA region. + + Hosted Private Cloud – VMware vSphere service running in GRA + region. + ), }, { id: 'api-url', label: 'API URL', - value: , + value: ( + + ), }, { id: 'service-id', diff --git a/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.spec.tsx b/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.spec.tsx index 980c1f939744..66faa6375357 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.spec.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/dashboard/general-information/GeneralInformation.spec.tsx @@ -15,8 +15,8 @@ interface LinksProps { } // --- Mock Clipboard & Links --- -vi.mock('@ovh-ux/manager-react-components', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@ovh-ux/muk', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, Clipboard: ({ value }: ClipboardProps) => ( @@ -49,14 +49,16 @@ describe('GeneralInformationPage', () => { it('renders service details and allows clipboard copy', () => { const writeText = vi.fn<() => Promise>(); - Object.assign(navigator, { + Object.assign(navigator, ({ clipboard: { writeText }, - } as unknown as Navigator); + } as unknown) as Navigator); render(); expect( - screen.getByText(/Hosted Private Cloud – VMware vSphere service running in GRA region./i), + screen.getByText( + /Hosted Private Cloud – VMware vSphere service running in GRA region./i, + ), ).toBeInTheDocument(); const clipboards = screen.getAllByTestId('clipboard'); @@ -67,7 +69,9 @@ describe('GeneralInformationPage', () => { expect(clipboards[1]).toHaveAttribute('data-value', 'srv-12345-abcde'); fireEvent.click(clipboards.at(0)!); // non-null assertion for TS - expect(writeText).toHaveBeenCalledWith('https://api.ovh.com/1.0/hostedprivatecloud'); + expect(writeText).toHaveBeenCalledWith( + 'https://api.ovh.com/1.0/hostedprivatecloud', + ); expect(screen.getByText('Active')).toBeInTheDocument(); }); diff --git a/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.page.tsx b/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.page.tsx index df518510a677..72a6e5008d56 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.page.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.page.tsx @@ -12,7 +12,7 @@ import { DataGridTextCell, Datagrid, useDataGrid, -} from '@ovh-ux/manager-react-components'; +} from '@ovh-ux/muk'; import { APP_FEATURES, appName } from '@/App.constants'; import Breadcrumb from '@/components/breadcrumb/Breadcrumb.component'; @@ -31,9 +31,13 @@ export default function ListingPage() { appName, }); - const { items, total, isLoading, hasNextPage, fetchNextPage } = useListingData( - APP_FEATURES.listingEndpoint, - ); + const { + items, + total, + isLoading, + hasNextPage, + fetchNextPage, + } = useListingData(APP_FEATURES.listingEndpoint); const baseColumns = useListingColumns(); @@ -49,13 +53,18 @@ export default function ListingPage() { label: t('listing:auto_column', 'Result'), isSortable: false, cell: (row: ListingItemType) => ( - {row ? JSON.stringify(row) : EMPTY} + + {row ? JSON.stringify(row) : EMPTY} + ), }, ]; }, [baseColumns, t]); - const initialSort = useMemo(() => ({ id: columns[0]?.id ?? 'auto', desc: false }), [columns]); + const initialSort = useMemo( + () => ({ id: columns[0]?.id ?? 'auto', desc: false }), + [columns], + ); const { sorting, setSorting } = useDataGrid(initialSort); const totalItems = Number.isFinite(total) ? total : items.length; diff --git a/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.spec.tsx b/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.spec.tsx index ea90b219c569..db7820578e99 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.spec.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/listing/Listing.spec.tsx @@ -23,7 +23,12 @@ interface BaseLayoutProps { interface DataGridProps { topbar?: ReactNode; - columns: { id: string; label: string; isSortable?: boolean; cell?: (row: T) => ReactNode }[]; + columns: { + id: string; + label: string; + isSortable?: boolean; + cell?: (row: T) => ReactNode; + }[]; items: T[]; totalItems: number; hasNextPage: boolean; @@ -31,7 +36,7 @@ interface DataGridProps { isLoading: boolean; } -vi.mock('@ovh-ux/manager-react-components', () => ({ +vi.mock('@ovh-ux/muk', () => ({ BaseLayout: ({ header, children, breadcrumb }: BaseLayoutProps) => (

{header.title}

@@ -63,7 +68,9 @@ vi.mock('@ovh-ux/manager-react-components', () => ({
), // eslint-disable-next-line react/no-multi-comp - DataGridTextCell: ({ children }: { children: ReactNode }) => {children}, + DataGridTextCell: ({ children }: { children: ReactNode }) => ( + {children} + ), useDataGrid: () => ({ sorting: [] as string[], setSorting: vi.fn() }), })); @@ -85,7 +92,12 @@ vi.mock('@/hooks/layout/useBreadcrumb', () => ({ })); vi.mock('@/hooks/listing/useListingColumns', () => ({ useListingColumns: () => [ - { id: 'id', label: 'listing:id', isSortable: true, cell: (row: { id: string }) => row.id }, + { + id: 'id', + label: 'listing:id', + isSortable: true, + cell: (row: { id: string }) => row.id, + }, ], })); vi.mock('@/data/hooks/useResources', () => ({ @@ -158,6 +170,8 @@ describe('ListingPage', () => { const { default: Page } = await import('./Listing.page'); render(); - expect(screen.getByTestId('columns')).toHaveTextContent('listing:auto_column'); + expect(screen.getByTestId('columns')).toHaveTextContent( + 'listing:auto_column', + ); }); }); diff --git a/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.page.tsx b/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.page.tsx index 114eb92a62c8..ef7f8393fc49 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.page.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.page.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { OdsText } from '@ovhcloud/ods-components/react'; -import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components'; +import { Card, OnboardingLayout } from '@ovh-ux/muk'; import { useGuideLinks, useOnboardingContent } from '@/hooks/onboarding/useOnboardingData'; import type { OnboardingLinksType } from '@/types/Onboarding.type'; diff --git a/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.spec.tsx b/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.spec.tsx index 1cdf2752980d..4c90ab5602d2 100644 --- a/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.spec.tsx +++ b/packages/manager-tools/manager-generator/template/src/pages/onboarding/Onboarding.spec.tsx @@ -14,7 +14,7 @@ vi.mock('react-i18next', () => ({ }), })); -// --- Mock manager-react-components --- +// --- Mock manager-ui-kit --- interface OnboardingLayoutProps { title: string; img?: { src: string; alt: string }; @@ -29,7 +29,7 @@ interface CardProps { hrefLabel: string; } -vi.mock('@ovh-ux/manager-react-components', () => ({ +vi.mock('@ovh-ux/muk', () => ({ OnboardingLayout: ({ title, img, diff --git a/packages/manager-tools/manager-generator/template/src/routes/Routes.tsx b/packages/manager-tools/manager-generator/template/src/routes/Routes.tsx index d37e2dfa7878..8ac87264cf6d 100644 --- a/packages/manager-tools/manager-generator/template/src/routes/Routes.tsx +++ b/packages/manager-tools/manager-generator/template/src/routes/Routes.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Navigate, Route } from 'react-router-dom'; -import { ErrorBoundary } from '@ovh-ux/manager-react-components'; +import { ErrorBoundary } from '@ovh-ux/muk'; import { PageType } from '@ovh-ux/manager-react-shell-client'; import NotFound from '@/pages/not-found/404.page'; @@ -11,11 +11,15 @@ import { redirectionApp, subRoutes, urls } from './Routes.constants'; const MainLayoutPage = React.lazy(() => import('@/pages/Main.layout')); -const OnboardingPage = React.lazy(() => import('@/pages/onboarding/Onboarding.page')); +const OnboardingPage = React.lazy(() => + import('@/pages/onboarding/Onboarding.page'), +); -const DashboardPage = React.lazy(() => import('@/pages/dashboard/Dashboard.page')); -const GeneralInformationPage = React.lazy( - () => import('@/pages/dashboard/general-information/GeneralInformation.page'), +const DashboardPage = React.lazy(() => + import('@/pages/dashboard/Dashboard.page'), +); +const GeneralInformationPage = React.lazy(() => + import('@/pages/dashboard/general-information/GeneralInformation.page'), ); const HelpPage = React.lazy(() => import('@/pages/dashboard/help/Help.page')); diff --git a/packages/manager-tools/manager-generator/template/src/types/ClientApi.type.ts b/packages/manager-tools/manager-generator/template/src/types/ClientApi.type.ts index 34ab3f688162..b2a2a290a87f 100644 --- a/packages/manager-tools/manager-generator/template/src/types/ClientApi.type.ts +++ b/packages/manager-tools/manager-generator/template/src/types/ClientApi.type.ts @@ -1,7 +1,7 @@ import React from 'react'; import type { Filter } from '@ovh-ux/manager-core-api'; -import type { ColumnSort, DatagridColumn } from '@ovh-ux/manager-react-components'; +import type { ColumnSort, DatagridColumn } from '@ovh-ux/muk'; export type ApiVersion = 'v2' | 'v6'; diff --git a/packages/manager-tools/manager-generator/template/tailwind.config.mjs b/packages/manager-tools/manager-generator/template/tailwind.config.mjs index ec6b4f4d8433..d0b8756c1a2f 100644 --- a/packages/manager-tools/manager-generator/template/tailwind.config.mjs +++ b/packages/manager-tools/manager-generator/template/tailwind.config.mjs @@ -6,7 +6,7 @@ const require = createRequire(import.meta.url); const pkgDir = (name) => path.dirname(require.resolve(`${name}/package.json`)); const toGlob = (dir) => `${dir.replace(/\\/g, '/')}/**/*.{js,jsx,ts,tsx}`; -const reactComponentsDir = pkgDir('@ovh-ux/manager-react-components'); +const reactComponentsDir = pkgDir('@ovh-ux/muk'); const pciCommonDir = pkgDir('@ovh-ux/manager-pci-common'); const isPciConfig = '{{isPci}}'; @@ -17,10 +17,7 @@ const baseTailwindConfig = [ toGlob(reactComponentsDir), ]; -export const pciTailwindConfig = [ - ...baseTailwindConfig, - toGlob(pciCommonDir), -]; +export const pciTailwindConfig = [...baseTailwindConfig, toGlob(pciCommonDir)]; /** @type {import('tailwindcss').Config} */ export default { diff --git a/packages/manager-tools/manager-legacy-tools/test-utils/package.json b/packages/manager-tools/manager-legacy-tools/test-utils/package.json index 15bd5d3973cc..54919d9dab1f 100644 --- a/packages/manager-tools/manager-legacy-tools/test-utils/package.json +++ b/packages/manager-tools/manager-legacy-tools/test-utils/package.json @@ -32,8 +32,10 @@ }, "peerDependencies": { "@ovh-ux/manager-core-api": "^0.9.0", + "@ovhcloud/ods-common-core": ">=17.0.0 <19.0.0", + "@ovhcloud/ods-components": ">=17.0.0 <19.0.0", "@tanstack/react-query": "5.x", "i18next": "23.x", "react": "18.x" } -} +} \ No newline at end of file diff --git a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/common-selectors.ts b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/common-selectors.ts index 06773b855429..2e8ba4a64500 100644 --- a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/common-selectors.ts +++ b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/common-selectors.ts @@ -16,5 +16,5 @@ export const getNthElementByTestId = ({ ...options }: { testId: string; index?: number } & waitForOptions): Promise => waitFor(() => screen.getAllByTestId(testId), options).then( - (response) => response[index], + (response) => response[index]!, ); diff --git a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/msw.ts b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/msw.ts index 1a6075f29124..a0224cba93ff 100644 --- a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/msw.ts +++ b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/msw.ts @@ -22,7 +22,7 @@ export const toMswHandlers = (handlers: Handler[] = []): RequestHandler[] => once, }: Handler) => http[method]( - `${baseUrl ?? apiClient?.[api]?.getUri?.()}${ + `${baseUrl ?? apiClient[api]?.getUri() ?? ''}${ url.startsWith('/') ? '' : '/' }${url}`, async ({ request, params, cookies }) => { diff --git a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/ui-test-helpers-ods18.ts b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/ui-test-helpers-ods18.ts index 945b14779185..7daad219d481 100644 --- a/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/ui-test-helpers-ods18.ts +++ b/packages/manager-tools/manager-legacy-tools/test-utils/src/utils/ui-test-helpers-ods18.ts @@ -40,7 +40,7 @@ export const assertOdsModalText = ({ waitFor( () => expect( - within(container.querySelector('ods-modal')).getByText(text, { + within(container.querySelector('ods-modal')!).getByText(text, { exact: false, }), ).toBeVisible(), @@ -67,7 +67,7 @@ const getOdsButton = async ({ nth = 0, ...options }: GetOdsButtonParams) => { - let button: HTMLElement; + let button: HTMLElement | undefined; await waitFor( () => { const htmlTag = isLink ? 'ods-link' : 'ods-button'; @@ -77,12 +77,12 @@ const getOdsButton = async ({ // filter by icon button = Array.from(buttonList).filter( (btn) => btn.getAttribute('icon') === iconName, - )[nth]; + )[nth]!; } else { // filter by label or altLabel button = Array.from(buttonList).filter((btn) => - [label, altLabel].includes(btn.getAttribute('label')), - )[nth]; + [label, altLabel].includes(btn.getAttribute('label') ?? ''), + )[nth]!; } if (isLink) { @@ -96,7 +96,7 @@ const getOdsButton = async ({ }, { ...WAIT_FOR_DEFAULT_OPTIONS, ...options }, ); - return button; + return button!; }; type GetOdsButtonByLabelParams = Omit & { @@ -168,5 +168,5 @@ export const selectOdsSelectOption = async ({ const event = new CustomEvent('odsChange', { detail: { value }, }); - waitFor(() => fireEvent(select, event)); + return waitFor(() => fireEvent(select, event)); }; diff --git a/packages/manager-tools/manager-muk-cli/README.md b/packages/manager-tools/manager-muk-cli/README.md new file mode 100644 index 000000000000..6d9accf4605c --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/README.md @@ -0,0 +1,266 @@ +# 🧩 manager-muk-cli + +A Node.js CLI designed to **automate maintenance, synchronization, and documentation** of the `@ovh-ux/manager-ui-kit` with the **OVHcloud Design System (ODS)**. + +It checks ODS versions, detects missing components, generates passthroughs (hooks, constants, types), and **synchronizes ODS component documentation** directly from GitHub, using a fully-streamed, cache-aware architecture. + +--- + +## 🚀 1. Features Overview + +### 1.1 `--check-versions` + +Checks npm for new ODS package releases and compares them with the versions declared in `manager-ui-kit/package.json`. + +```bash +yarn muk-cli --check-versions +``` + +**Example Output** + +``` +ℹ 🔍 Checking ODS package versions... +⚠ Updates available: +ℹ @ovhcloud/ods-components: 18.6.2 → 18.6.4 +ℹ @ovhcloud/ods-react: 19.0.1 → 19.1.0 +ℹ @ovhcloud/ods-themes: 19.0.1 → 19.1.0 +``` + +--- + +### 1.2 `--check-components` + +Compares ODS React components with those in `manager-ui-kit/src/components`, identifying missing or outdated ones. + +```bash +yarn muk-cli --check-components +``` + +**Example Output** + +``` +ℹ 📁 Found 34 local components +ℹ 📦 Fetching ODS React v19.2.0 tarball... +⚠ Missing 8 ODS components: +ℹ • form-field +ℹ • form-field-label +ℹ • range +ℹ • range-thumb +ℹ • range-track +``` + +--- + +### 1.3 `--update-versions` + +Automatically updates all ODS dependencies in `package.json` to their latest versions, validates, and runs post-update checks. + +```bash +yarn muk-cli --update-versions +``` + +**Example Output** + +``` +✔ Updated 3 ODS dependencies +✔ @ovhcloud/ods-components: 18.6.2 → 18.6.4 +✔ @ovhcloud/ods-react: 19.0.1 → 19.2.0 +✔ @ovhcloud/ods-themes: 19.0.1 → 19.2.0 +✔ package.json successfully updated. +``` + +If all are current: + +``` +✅ All ODS versions are already up to date! +✨ Done in 1.78s. +``` + +--- + +### 1.4 `--add-components` + +Generates **missing ODS components** from the ODS React source tarball, preserving hooks, constants, and external types. + +```bash +yarn muk-cli --add-components +``` + +#### Supported Scenarios + +- Simple components (no children, e.g. `badge`, `progress-bar`) +- Nested components (with subcomponents, e.g. `form-field`, `datepicker`, `range`) +- Hook passthroughs (`useFormField`) +- Constants passthroughs (`DatepickerConstants`) +- External type re-exports + +--- + +### 1.5 `--add-components-documentation` + +Fetches and synchronizes **official ODS component documentation** (`.mdx` files) from the [ovh/design-system](https://github.com/ovh/design-system) repository directly into `manager-wiki`. + +```bash +yarn muk-cli --add-components-documentation +``` + +#### 🧠 What It Does + +1. Detects the latest `@ovhcloud/ods-react` version from npm. +2. Downloads (or reuses) the GitHub tarball for that version. +3. Streams documentation files under `/storybook/stories/components/`. +4. Extracts per-component documentation and writes it to: + ``` + packages/manager-wiki/stories/manager-ui-kit/components//base-component-doc/ + ``` +5. Caches the tarball for **7 days** to avoid redundant downloads. +6. Synchronizes Storybook base-documents: + ``` + packages/manager-wiki/stories/manager-ui-kit/base-documents/ + ``` +7. Rewrites imports: + +- `../../../src/` → `../../../base-documents/` +- `ods-react/src/` → `@ovhcloud/ods-react` + +**Example Output** + +``` +ℹ 📦 Starting Design System documentation sync… +ℹ ℹ️ ODS React latest version: 19.2.1 +✔ 💾 Served 85 documentation files from cache. +ℹ 📁 Found existing component: 'accordion' +✔ ✅ Sync complete — 45 new, 42 updated, 171 files streamed. +``` + +--- + +## ⚙️ 2. Streaming Architecture + +### 2.1 High-Level Flow + +``` +GitHub tarball (.tar.gz) + │ + ├─▶ streamTarGz() + ├─▶ extractDesignSystemDocs() + ├─▶ createAsyncQueue() + └─▶ streamComponentDocs() +``` + +### 2.2 Core Streaming Functions + +#### Stream Extraction + +```js +pipeline( + https.get(url), + zlib.createGunzip(), + tar.extract({ onentry(entry) { ... } }) +); +``` + +Each `entry` is processed **as it’s read** — no full buffering. + +#### Stream Bridge + +```js +const queue = createAsyncQueue(); +await extractDesignSystemDocs({ onFileStream: q.push }); +await streamComponentDocs(queue); +``` + +Manages concurrency and backpressure. + +#### Stream Consumer + +```js +await pipeline(fileStream, fs.createWriteStream(destFile)); +``` + +Backpressure-safe, cleans up on error. + +#### Cache Layer + +``` +target/.cache/ods-docs/ +├── ods-docs-meta.json +└── ods-docs-files.json +``` + +TTL: 7 days — fully reusable offline. + +--- + +## 🧱 3. Codebase Layout + +``` +manager-muk-cli/ +├─ src/ +│ ├─ commands/ +│ ├─ core/ +│ ├─ config/ +└─ target/.cache/ods-docs/ +``` + +--- + +## 🧠 4. Design Principles + +| Principle | Description | +| ------------------- | -------------------------------- | +| **Streaming-first** | All I/O ops use Node streams | +| **Memory-safe** | Constant memory footprint | +| **Composable** | Modular, small functions | +| **Idempotent** | Deterministic results | +| **Offline-safe** | Cache-first retry | +| **Verbose logging** | Emoji logs for visibility | +| **Configurable** | Rewrite rules in `muk-config.js` | + +--- + +## ✅ 5. Advantages + +- 🔁 Auto-synced ODS documentation and base-docs +- ⚡ Cached and resumable (7-day TTL) +- 🧩 Full parity with ODS React components +- 🧠 Low-memory async pipeline +- 🧱 Modular, testable, CI-ready +- 🔧 Configurable rewriting rules + +--- + +## 🧩 6. Cache Troubleshooting + +```bash +rm -rf packages/manager-tools/manager-muk-cli/target/.cache/ods-docs +``` + +Rebuilds clean cache on rerun. + +--- + +## 🧩 7. Configuration Extraction + +`muk-config.js` centralizes regex, rewrite, and Storybook folder logic. + +```js +export const MUK_IMPORT_REWRITE_RULES = [ + { + name: 'base-documents', + pattern: /((?:\..\/){2,3})src\//g, + replacer: (_, prefix) => `${prefix}base-documents/`, + }, + { + name: 'ods-react', + pattern: /(['"])[^'"]*ods-react\/src[^'"]*/g, + replacer: (_, quote) => `${quote}@ovhcloud/ods-react`, + }, +]; +``` + +--- + +## 🪪 8. License + +BSD-3-Clause © OVH SAS diff --git a/packages/manager-tools/manager-muk-cli/eslint.config.mjs b/packages/manager-tools/manager-muk-cli/eslint.config.mjs new file mode 100644 index 000000000000..f86179cef392 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/eslint.config.mjs @@ -0,0 +1,15 @@ +import { + complexityJsxTsxConfig, + complexityTsJsConfig, +} from '@ovh-ux/manager-static-analysis-kit/eslint/complexity'; +import { javascriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/javascript'; +import { prettierEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/prettier'; +import { vitestEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tests'; + +export default [ + javascriptEslintConfig, + vitestEslintConfig, + prettierEslintConfig, + complexityJsxTsxConfig, + complexityTsJsConfig, +]; diff --git a/packages/manager-tools/manager-muk-cli/package.json b/packages/manager-tools/manager-muk-cli/package.json new file mode 100644 index 000000000000..4c3a5532eab6 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "@ovh-ux/manager-muk-cli", + "version": "0.0.1", + "private": true, + "description": "", + "license": "BSD-3-Clause", + "author": "OVH SAS", + "type": "module", + "bin": { + "muk-cli": "src/index.js" + }, + "scripts": { + "lint:modern": "manager-lint --config eslint.config.mjs './src/**/*.js'", + "lint:modern:fix": "manager-lint --fix --config eslint.config.mjs './src/**/*.js'" + }, + "dependencies": { + "pacote": "^21.0.3", + "tar-stream": "^3.1.7", + "semver": "^7.7.3" + }, + "devDependencies": { + "@ovh-ux/manager-static-analysis-kit": "*", + "typescript": "^5.9.2" + } +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js b/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js new file mode 100644 index 000000000000..b7894972e578 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js @@ -0,0 +1,54 @@ +import { EMOJIS, MUK_WIKI_COMPONENTS } from '../config/muk-config.js'; +import { + rewriteWikiComponentImports, + syncComponentDocs, + syncStorybookBaseDocuments, +} from '../core/component-documentation-utils.js'; +import { aggregateOperationsStats, runPostUpdateChecks, safeSync } from '../core/tasks-utils.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * CLI Command: addComponentsDocumentation + * + * Synchronizes all Design System documentation artifacts into the Manager Wiki. + * + * This process performs two major synchronization operations: + * + * 1. **Component Base Documentation** — retrieves and updates + * each component’s `base-component-doc` folder in the Wiki. + * + * 2. **Storybook Source Documentation** — downloads and mirrors all + * Storybook `components`, `constants`, and `helpers` under: + * `packages/manager-wiki/stories/manager-ui-kit/base-documents/` + * + * Both steps use streaming extraction from the ODS tarball, + * ensuring minimal memory usage and safe incremental updates. + * + * @async + * @returns {Promise} Completes when both synchronizations finish. + */ +export async function addComponentsDocumentation() { + logger.info(`${EMOJIS.package} Starting Design System documentation sync…`); + + // 1 Components + const componentResult = await safeSync('component base-docs', syncComponentDocs); + + // 2 Storybook base-documents + const storybookResult = await safeSync('storybook base-documents', syncStorybookBaseDocuments); + + // 3 Rewrite imports + rewriteWikiComponentImports(MUK_WIKI_COMPONENTS); + + // 4 Aggregate + const stats = aggregateOperationsStats([componentResult, storybookResult]); + logger.success( + `${EMOJIS.check} Sync complete — ${stats.created} new, ${stats.updated} updated, ${stats.total} files streamed.`, + ); + + // Run validation tasks (install, build, tests) after workspace updates + runPostUpdateChecks(); + + logger.info( + `${EMOJIS.rocket} Components updated and imports normalized under 'stories/manager-ui-kit'.`, + ); +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/add-components.js b/packages/manager-tools/manager-muk-cli/src/commands/add-components.js new file mode 100644 index 000000000000..53a69bbee504 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/add-components.js @@ -0,0 +1,364 @@ +#!/usr/bin/env node +import path from 'node:path'; + +import { EMOJIS, MUK_COMPONENTS_SRC, ODS_EXCLUDED_COMPONENTS } from '../config/muk-config.js'; +import { + buildComponentsIndexTemplate, + buildSubcomponentPropsTemplate, + buildSubcomponentSpecTemplate, + buildSubcomponentTemplate, + getComponentTemplates, +} from '../config/muk-template-config.js'; +import { groupComponentsDynamically } from '../core/component-utils.js'; +import { + createFile, + ensureDir, + readFile, + toKebabCase, + toPascalCase, + writeFile, +} from '../core/file-utils.js'; +import { + detectHasChildrenFromTarball, + detectHasTypeExportFromIndex, + extractOdsComponentsExportsByCategory, +} from '../core/ods-components-tarball-utils.js'; +import { runPostUpdateChecks } from '../core/tasks-utils.js'; +import { logger } from '../utils/log-manager.js'; +import { checkComponents } from './check-components.js'; + +/** + * Builds all necessary file and directory metadata for a given component. + * Generates standard paths and template contents for component, props, tests, and index files. + * + * @param {string} componentName - The name of the ODS component (e.g. "FormField"). + * @param {boolean} [hasChildren=false] - Whether the component has nested subcomponents. + * @returns {{ + * componentName: string, + * folderName: string, + * paths: { base: string, tests: string }, + * files: Record + * }} A metadata object describing directories and files to create. + */ +function buildComponentFiles(componentName, hasChildren = false) { + const folderName = toKebabCase(componentName); + const pascalName = toPascalCase(componentName); + + const componentDir = path.join(MUK_COMPONENTS_SRC, folderName); + const testsDir = path.join(componentDir, '__tests__'); + const templates = getComponentTemplates(pascalName, hasChildren); + + return { + componentName: pascalName, + folderName, + paths: { base: componentDir, tests: testsDir }, + files: { + component: { + path: path.join(componentDir, `${pascalName}.component.tsx`), + content: templates.component, + }, + props: { path: path.join(componentDir, `${pascalName}.props.ts`), content: templates.props }, + test: { + path: path.join(testsDir, `${pascalName}.snapshot.test.tsx`), + content: templates.test, + }, + index: { path: path.join(componentDir, 'index.ts'), content: templates.index }, + }, + }; +} + +/** + * Generates all subcomponents for a parent component, including folders, props, component, and test files. + * + * @async + * @param {string} parentName - The name of the parent ODS component (e.g. "FormField"). + * @param {string} parentPascal - The parent component name in PascalCase. + * @param {string} basePath - Path to the parent component directory. + * @param {string[]} subcomponents - List of subcomponent names to generate. + * @returns {Promise} Resolves when all subcomponents are created. + */ +async function createSubcomponents(parentName, parentPascal, basePath, subcomponents) { + if (!subcomponents.length) return; + + for (const subName of subcomponents) { + const kebabSubName = toKebabCase(subName); + const subPascal = toPascalCase(subName); + + const subDir = path.join(basePath, kebabSubName); + const subTestsDir = path.join(subDir, '__tests__'); + ensureDir(subDir); + ensureDir(subTestsDir); + + const hasChildren = await detectHasChildrenFromTarball(parentName, subName); + const hasOwnType = await detectHasTypeExportFromIndex(parentName, subName); + + createFile( + path.join(subDir, `${subPascal}.props.ts`), + buildSubcomponentPropsTemplate(subPascal, parentPascal, hasOwnType, hasChildren), + ); + createFile( + path.join(subDir, `${subPascal}.component.tsx`), + buildSubcomponentTemplate(subPascal, hasChildren), + ); + createFile( + path.join(subTestsDir, `${subPascal}.component.spec.tsx`), + buildSubcomponentSpecTemplate(subPascal), + ); + + logger.info( + `${EMOJIS.folder} Created subcomponent '${subName}' (${hasOwnType ? '🧩 own type' : '↩ parent type'}, ${hasChildren ? '👶 has children' : '🚫 stateless'})`, + ); + } +} + +/** + * Appends external type imports and exports to a component's props file. + * + * @param {string} propsPath - Absolute path to the component’s props file. + * @param {string[]} externalTypes - List of type identifiers to import/export. + * @returns {void} + */ +function appendExternalTypesToProps(propsPath, externalTypes) { + if (!externalTypes.length) return; + + const existing = readFile(propsPath, false); + const alreadyPresent = externalTypes.some((type) => existing.includes(type)); + if (alreadyPresent) return; + + const typeImports = externalTypes.map((t) => `type ${t}`).join(',\n '); + const snippet = ` +/** + * External types (from contexts, utils, or shared ODS exports) + */ +import { + ${typeImports} +} from '@ovhcloud/ods-react'; + +export type { ${externalTypes.join(', ')} }; +`; + + writeFile(propsPath, `${existing.trim()}\n${snippet}\n`); + logger.info(`🧩 Added ${externalTypes.length} external types to props`); +} + +/** + * Creates passthroughs for hooks and constants, and appends external types. + * + * @async + * @param {string} componentName - PascalCase name of the component. + * @param {string} baseDir - Absolute path to the component directory. + * @param {string} odsName - Original ODS package name (used for import extraction). + * @param {string} propsPath - Path to the props file where external types may be added. + * @returns {Promise<{ hasHooks: boolean, hasConstants: boolean }>} Whether hooks/constants were created. + */ +async function createHooksAndConstants(componentName, baseDir, odsName, propsPath) { + const { hooks, constants, externalTypes } = await extractOdsComponentsExportsByCategory(odsName); + + // 🪝 Hooks passthrough + if (hooks.length) { + const hooksDir = path.join(baseDir, 'hooks'); + ensureDir(hooksDir); + + const hookFile = path.join(hooksDir, `use${componentName}.ts`); + const hookImports = hooks.map((h) => (h.startsWith('use') ? h : `type ${h}`)).join(', '); + const hookContent = `import { ${hookImports} } from '@ovhcloud/ods-react'; + +export { ${hooks.join(', ')} }; +`; + createFile(hookFile, hookContent); + logger.info(`🪝 Created hook passthrough for ${componentName} (${hooks.length} identifiers)`); + } + + // ⚙️ Constants passthrough + if (constants.length) { + const constantsDir = path.join(baseDir, 'constants'); + ensureDir(constantsDir); + + const constFile = path.join(constantsDir, `${componentName}Constants.ts`); + const constContent = `import { + ${constants.join(',\n ')} +} from '@ovhcloud/ods-react'; + +export { + ${constants.join(',\n ')} +}; +`; + createFile(constFile, constContent); + logger.info( + `⚙️ Created constants passthrough for ${componentName} (${constants.length} identifiers)`, + ); + } + + // 🧩 External types passthrough + appendExternalTypesToProps(propsPath, externalTypes); + + return { hasHooks: hooks.length > 0, hasConstants: constants.length > 0 }; +} + +/** + * Updates the component’s index.ts to export subcomponents, hooks, and constants. + * + * @param {{ + * indexPath: string, + * subcomponents: string[], + * hasHooks: boolean, + * hasConstants: boolean, + * componentName: string + * }} params - Configuration for index file updates. + * @returns {void} + */ +function updateComponentIndex({ indexPath, subcomponents, hasHooks, hasConstants, componentName }) { + let content = readFile(indexPath, false).trimEnd(); + const additions = []; + + for (const sub of subcomponents) { + const subPascal = toPascalCase(sub); + additions.push(`export { ${subPascal} } from './${toKebabCase(sub)}/${subPascal}.component';`); + } + + if (hasHooks) additions.push(`export * from './hooks/use${componentName}';`); + if (hasConstants) additions.push(`export * from './constants/${componentName}Constants';`); + + if (additions.length) { + writeFile(indexPath, `${content}\n\n${additions.join('\n')}\n`); + logger.info(`🔗 Updated index.ts with ${additions.length} new exports`); + } +} + +/** + * Generates the full directory and file structure for a component and its subcomponents. + * + * @async + * @param {string} parentName - The main ODS component name. + * @param {string[]} [subcomponents=[]] - Optional list of subcomponents to generate. + * @returns {Promise} + */ +async function generateComponentStructure(parentName, subcomponents = []) { + const hasChildren = await detectHasChildrenFromTarball(parentName); + + // 🛑 Skip abstract or invalid components that have neither source nor subcomponents + if (hasChildren === null && !subcomponents.length) { + logger.warn(`⚠ Skipping '${parentName}' — not a valid ODS component.`); + return; + } + + const { componentName, paths, files } = buildComponentFiles(parentName, hasChildren); + + Object.values(paths).forEach(ensureDir); + Object.values(files).forEach(({ path: p, content }) => createFile(p, content)); + + await createSubcomponents(parentName, componentName, paths.base, subcomponents); + + const { hasHooks, hasConstants } = await createHooksAndConstants( + componentName, + paths.base, + parentName, + files.props.path, + ); + + updateComponentIndex({ + indexPath: files.index.path, + subcomponents, + hasHooks, + hasConstants, + componentName, + }); + + logger.success(`✔ Component structure ready for ${componentName}`); +} + +/** + * Detects missing ODS components and generates their folder structures in Manager UI Kit. + * + * @async + * @returns {Promise} List of parent components that were created. + */ +async function createComponentsStructure() { + logger.info(`${EMOJIS.info} Starting ODS → Manager UI Kit component sync...`); + + const { missingComponents = [] } = await checkComponents({ returnOnly: true }); + const relevant = missingComponents.filter((name) => !ODS_EXCLUDED_COMPONENTS.includes(name)); + // .filter((name) => ['form-field', 'range'].some((k) => name.includes(k))); + + if (!relevant.length) { + logger.success('✅ All relevant ODS components are already present.'); + return []; + } + + const grouped = await groupComponentsDynamically(relevant); + const created = []; + + for (const [parent, children] of Object.entries(grouped)) { + await generateComponentStructure(parent, children); + created.push(parent); + } + + logger.success(`🎉 Created folder structure for ${created.length} components.`); + return created; +} + +/** + * Updates the root Manager UI Kit index.ts to include newly created components. + * + * @param {string[]} newComponents - List of component names to export. + * @returns {void} + */ +function updateComponentsIndexExports(newComponents) { + if (!newComponents?.length) return; + + const indexPath = path.join(MUK_COMPONENTS_SRC, 'index.ts'); + let content = readFile(indexPath, false); + if (!content.trim()) content = buildComponentsIndexTemplate(); + + const additions = []; + + for (const name of newComponents) { + const folder = toKebabCase(name); + const line = `export * from './${folder}';`; + if (!content.includes(line)) additions.push(line); + } + + if (additions.length) { + writeFile(indexPath, `${content.trimEnd()}\n${additions.join('\n')}\n`); + logger.success( + `${EMOJIS.check} Added ${additions.length} new export${additions.length > 1 ? 's' : ''}`, + ); + } else { + logger.info(`${EMOJIS.info} No new exports required in index.ts`); + } +} + +/** + * CLI entrypoint — synchronizes and scaffolds missing ODS components. + * + * Public command — updateComponents + * - Step 1: Create missing component structures + * - Step 2: Populate base templates + * - Step 3: Update UI Kit index exports + * - Step 4: Run validation tasks + * + * @async + * @returns {Promise} + */ +export async function addComponents() { + logger.info(`${EMOJIS.info} Running component synchronization pipeline...`); + + // Create missing component structures + // Populate base templates + const created = await createComponentsStructure(); + + if (!created.length) { + logger.success('✅ No new components to add.'); + return; + } + + // Update UI Kit index exports + await updateComponentsIndexExports(created); + + // Run validation tasks (install, build, tests) after workspace updates + runPostUpdateChecks(); + + logger.success( + `🏁 Component sync completed — ${created.length} new folder${created.length > 1 ? 's' : ''} created.`, + ); +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/check-components.js b/packages/manager-tools/manager-muk-cli/src/commands/check-components.js new file mode 100644 index 000000000000..53cdf0b8453d --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/check-components.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +import { promises as fs } from 'node:fs'; + +import { EMOJIS, MUK_COMPONENTS_SRC } from '../config/muk-config.js'; +import { + extractOdsComponentsTarball, + getOdsComponentsPackageMetadata, +} from '../core/ods-components-tarball-utils.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * Retrieve all local Manager UI Kit (MUK) components. + * + * @returns {Promise} - A list of local component folder names. + */ +async function getLocalComponents() { + const entries = await fs.readdir(MUK_COMPONENTS_SRC, { withFileTypes: true }); + const componentDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); + + logger.info(`${EMOJIS.folder} Found ${componentDirs.length} local components`); + return componentDirs; +} + +/** + * Extract exported ODS React components by analyzing index.ts files. + * Only considers exports from `./components/...`, excluding contexts, constants, and utils. + * + * Handles: + * - Multi-line exports + * - Type exports + * - Redundant prefixes (e.g., combobox-combobox-item) + * - Root component fallback + */ +export async function getRemoteOdsComponents() { + const { version, tarball } = await getOdsComponentsPackageMetadata(); + logger.info(`${EMOJIS.package} Fetching ODS React v${version} tarball: ${tarball}`); + + const files = await extractOdsComponentsTarball(); + const components = new Set(); + + // Locate index.ts files under src/components//src/index.ts + const indexFiles = [...files.keys()].filter((p) => + /src\/components\/[^/]+\/src\/index\.ts$/.test(p), + ); + + for (const filePath of indexFiles) { + const rootMatch = filePath.match(/src\/components\/([^/]+)\/src\/index\.ts$/); + if (!rootMatch) continue; + const root = rootMatch[1]; + + const fileBuffer = files.get(filePath); + if (!fileBuffer) { + logger.warn(`${EMOJIS.warn} Skipping ${filePath} (not found in tarball)`); + continue; + } + + const content = fileBuffer.toString('utf8'); + + // Match multi-line export blocks from components folder only + const exportBlocks = content.matchAll( + /export\s*\{[\s\S]*?\}\s*from\s*['"]\.\/components\/([^/'"]+)\/?/g, + ); + + for (const match of exportBlocks) { + let sub = match[1].trim(); + + if (sub.startsWith(`${root}-`)) sub = sub.slice(root.length + 1); + if (sub === root) continue; + + components.add(`${root}-${sub}`); + } + + components.add(root); + } + + const componentList = [...components].sort(); + logger.info( + `${EMOJIS.check} Extracted ${componentList.length} exported ODS components (public API only).`, + ); + + return componentList; +} + +/** + * Compare the component sets between local MUK and remote ODS React sources. + * + * @param {object} [options] + * @param {boolean} [options.returnOnly=false] - If true, skip summary logging and only return data. + * @returns {Promise<{missingComponents: T[], extraLocalComponents: T[]}>} + */ +export async function checkComponents({ returnOnly = false } = {}) { + logger.info(`${EMOJIS.info} Comparing component parity between ODS React and Manager UI Kit...`); + + const [localComponents, remoteComponents] = await Promise.all([ + getLocalComponents(), + getRemoteOdsComponents(), + ]); + + const missingComponents = remoteComponents.filter((remote) => !localComponents.includes(remote)); + const extraLocalComponents = localComponents.filter((local) => !remoteComponents.includes(local)); + + if (returnOnly) { + return { missingComponents, extraLocalComponents }; + } + + logger.info( + `ℹ ODS Components: ${remoteComponents.length}, Local Components: ${localComponents.length}`, + ); + + if (missingComponents.length === 0) { + logger.success(`${EMOJIS.check} All ODS components exist locally.`); + } else { + logger.warn(`⚠ Missing ${missingComponents.length} ODS components:`); + missingComponents.forEach((name) => logger.info(`• ${name}`)); + } + + if (extraLocalComponents.length > 0) { + logger.debug( + `${EMOJIS.folder} Local-only components (${extraLocalComponents.length}): ${extraLocalComponents.join(', ')}`, + ); + } + + return { missingComponents, extraLocalComponents }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js b/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js new file mode 100644 index 000000000000..6c10033ed49d --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node +import path from 'node:path'; + +import { EMOJIS, MUK_COMPONENTS_PATH, TARGET_PACKAGES } from '../config/muk-config.js'; +import { getOutdatedPackages } from '../core/npm-utils.js'; +import { loadJson } from '../utils/json-utils.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * Compare local and remote ODS package versions. + * + * Uses semantic versioning to detect: + * - ⚠️ Outdated packages (local < npm) + * - 🚀 Ahead-of-latest packages (local > npm) + * - ℹ️ Non-standard or pre-release versions + * + * Logs a readable summary to the console and returns structured results. + * + * @returns {Promise>} + * + * @example + * const results = await checkVersions(); + * // Logs: + * // ⚠ Outdated packages: + * // @ovh-ux/ods-core: 19.3.0 → 19.4.0 + * // + * // 🚀 Locally ahead of npm: + * // @ovh-ux/ods-ui-kit: local 19.5.0-rc.1 > npm 19.4.0 + */ +export async function checkVersions() { + const pkgPath = path.join(MUK_COMPONENTS_PATH, 'package.json'); + const localPkg = await loadJson(pkgPath); + + const results = await getOutdatedPackages(localPkg, TARGET_PACKAGES); + + if (results.length === 0) { + logger.success(`${EMOJIS.success} All ODS packages are up to date!`); + return []; + } + + const outdated = results.filter((r) => r.status === 'outdated'); + const ahead = results.filter((r) => r.status === 'ahead'); + const unknown = results.filter((r) => r.status === 'unknown'); + + if (outdated.length > 0) { + logger.warn(`\n${EMOJIS.warn} Outdated packages:`); + for (const { name, local, latest } of outdated) { + logger.info(` ${name}: ${local} → ${latest}`); + } + } + + if (ahead.length > 0) { + logger.info(`\n${EMOJIS.rocket} Locally ahead of npm:`); + for (const { name, local, latest } of ahead) { + logger.info(` ${name}: local ${local} > npm ${latest}`); + } + } + + if (unknown.length > 0) { + logger.info(`\n${EMOJIS.info} Non-standard or pre-release versions:`); + for (const { name, local, latest } of unknown) { + logger.info(` ${name}: local ${local} vs npm ${latest}`); + } + } + + if (outdated.length === 0 && ahead.length === 0 && unknown.length === 0) { + logger.success(`${EMOJIS.success} All ODS packages are up to date!`); + } + + return results; +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/update-versions.js b/packages/manager-tools/manager-muk-cli/src/commands/update-versions.js new file mode 100644 index 000000000000..242dff5c9d7f --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/update-versions.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +import path from 'node:path'; + +import { EMOJIS, MUK_COMPONENTS_PATH, TARGET_PACKAGES } from '../config/muk-config.js'; +import { getOutdatedPackages } from '../core/npm-utils.js'; +import { runPostUpdateChecks } from '../core/tasks-utils.js'; +import { loadJson, writeJson } from '../utils/json-utils.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * 🚀 Update ODS package versions in `manager-react-components/package.json` + * only if a newer version exists on npm (never downgrade or overwrite newer local versions). + * + * ### Process Overview + * 1. Load the local `package.json` for `manager-react-components`. + * 2. Retrieve latest npm versions for {@link TARGET_PACKAGES}. + * 3. Use {@link getOutdatedPackages} (semver-aware) to compare versions. + * 4. Update only packages where `status === 'outdated'`. + * 5. Save the updated `package.json` and run post-update checks. + * + * @async + * @function updateOdsVersions + * @returns {Promise} Resolves when update and validation complete. + * + * @example + * ```bash + * yarn muk-cli --update-version + * ``` + */ +export async function updateOdsVersions() { + const pkgPath = path.join(MUK_COMPONENTS_PATH, 'package.json'); + const pkgJson = await loadJson(pkgPath); + + // getOutdatedPackages now returns [{ name, local, latest, status }] + const allComparisons = await getOutdatedPackages(pkgJson, TARGET_PACKAGES); + + // Only consider packages that are strictly outdated + const updates = allComparisons.filter((pkg) => pkg.status === 'outdated'); + + if (updates.length === 0) { + logger.success(`${EMOJIS.success} All ODS versions are already up to date or ahead of npm!`); + return; + } + + // Apply updates only for outdated packages + for (const { name, local, latest } of updates) { + if (pkgJson.devDependencies?.[name]) { + pkgJson.devDependencies[name] = `^${latest}`; + } + if (pkgJson.peerDependencies?.[name]) { + pkgJson.peerDependencies[name] = `^${latest}`; + } + logger.info(`${EMOJIS.info} ${name}: ${local} → ${latest}`); + } + + // Persist updated package.json + await writeJson(pkgPath, pkgJson); + logger.success(`${EMOJIS.check} Updated ${updates.length} ODS dependencies in package.json`); + logger.info(`📦 Saved to: ${pkgPath}`); + + // Run post-update validation (install + lint + test) + runPostUpdateChecks(); +} diff --git a/packages/manager-tools/manager-muk-cli/src/config/muk-config.js b/packages/manager-tools/manager-muk-cli/src/config/muk-config.js new file mode 100644 index 000000000000..6513b66c8523 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/config/muk-config.js @@ -0,0 +1,267 @@ +import path from 'node:path'; + +/** + * Enable / Disable caching components + * @type {boolean} + */ +export const DISABLE_ODS_COMPONENTS_CACHE = false; + +/** + * Absolute directory path for caching extracted ODS tarball files and metadata. + * Used to persist tarball contents between CLI runs. + * + * @constant {string} + */ +export const ODS_COMPONENTS_CACHE_DIR = path.resolve( + 'packages/manager-tools/manager-muk-cli/target/.cache/ods-tarball', +); + +/** + * Absolute path to the cached JSON file representing extracted tarball contents. + * This file stores the list of ODS files and their relative paths. + * + * @constant {string} + */ +export const ODS_COMPONENTS_TAR_CACHE_FILE = path.join( + ODS_COMPONENTS_CACHE_DIR, + 'ods-tarball-files.json', +); + +/** + * Absolute path to the metadata JSON file for the cached ODS tarball. + * Metadata includes version number, checksum, and timestamp. + * + * @constant {string} + */ +export const ODS_COMPONENTS_META_CACHE_FILE = path.join( + ODS_COMPONENTS_CACHE_DIR, + 'ods-tarball-meta.json', +); + +/** + * Cache settings for ODS documentation tarball + */ +export const ODS_DOCS_CACHE_DIR = path.resolve( + 'packages/manager-tools/manager-muk-cli/target/.cache/ods-docs', +); + +/** + * Path to the cached ODS documentation file map. + * + * Stores serialized tarball extraction results as `{ [entryPath]: Buffer }`. + * Used to avoid re-downloading or re-extracting ODS tarballs when fresh. + * + * @constant + * @type {string} + * @example + * "/packages/manager-tools/manager-muk-cli/target/.cache/ods-docs/ods-docs-files.json" + */ +export const ODS_DOCS_TAR_CACHE_FILE = path.join(ODS_DOCS_CACHE_DIR, 'ods-docs-files.json'); + +/** + * Path to the ODS documentation cache metadata file. + * + * Contains metadata describing the cache state: + * ``` + * { + * "version": "19.2.1", + * "checksum": "abc123...", + * "timestamp": 1728201000000 + * } + * ``` + * + * Used by `tarball-cache-utils.js` to validate cache freshness and version. + * + * @constant + * @type {string} + * @example + * "/packages/manager-tools/manager-muk-cli/target/.cache/ods-docs/ods-docs-meta.json" + */ +export const ODS_DOCS_META_CACHE_FILE = path.join(ODS_DOCS_CACHE_DIR, 'ods-docs-meta.json'); + +/** + * Global flag controlling whether ODS documentation caching is disabled. + * + * When `true`, the system skips all cache reads/writes and always downloads + * the latest ODS tarball. Useful for debugging or CI environments. + * + * @constant + * @type {boolean} + * @default false + * @example + * // Force re-fetch documentation on each run + * export const DISABLE_ODS_DOCS_CACHE = true; + */ +export const DISABLE_ODS_DOCS_CACHE = false; + +/** + * Absolute path to the Manager UI Kit (MUK) base package. + * + * @constant {string} + */ +export const MUK_COMPONENTS_PATH = path.resolve('packages/manager-ui-kit'); + +/** + * Absolute path to the source components directory within the Manager UI Kit. + * + * @constant {string} + */ +export const MUK_COMPONENTS_SRC = path.join(MUK_COMPONENTS_PATH, 'src', 'components'); + +/** + * Absolute path to the Manager Wiki base package. + * + * @constant {string} + */ +export const MUK_WIKI_PATH = path.resolve('packages/manager-wiki'); + +/** + * Absolute path to the Manager Wiki components directory. + * This is where base component documentation (e.g. `base-component-doc`) is stored. + * + * @constant {string} + */ +export const MUK_WIKI_COMPONENTS = path.join( + MUK_WIKI_PATH, + 'stories', + 'manager-ui-kit', + 'components', +); + +/** + * Absolute path to the Manager Wiki components directory. + * This is where base component documentation (e.g. `base-component-doc`) is stored. + * + * @constant {string} + */ +export const MUK_WIKI_BASED_DOCUMENT = path.join( + MUK_WIKI_PATH, + 'stories', + 'manager-ui-kit', + 'base-documents', +); + +/** + * Wiki import-rewrite configuration + */ +export const MUK_IMPORT_REWRITE_RULES = [ + { + name: 'base-documents', + pattern: /((?:\.\.\/){2,3})src\//g, + replacer: (_, prefix) => `${prefix}base-documents/`, + }, + { + name: 'ods-react', + pattern: /(['"])[^'"]*ods-react\/src[^'"]*/g, + replacer: (_, quote) => `${quote}@ovhcloud/ods-react`, + }, +]; + +/** + * Storybook folder and path configuration + */ +export const MUK_STORYBOOK_FOLDERS = ['components', 'constants', 'helpers']; + +export const MUK_STORYBOOK_ENTRY_REGEX = + /packages\/storybook\/src\/(components|constants|helpers)\//; + +/** + * NPM package names that are validated and potentially updated + * during version synchronization and documentation refresh. + * + * @constant {string[]} + */ +export const TARGET_PACKAGES = [ + '@ovhcloud/ods-components', + '@ovhcloud/ods-react', + '@ovhcloud/ods-themes', +]; + +/** + * ODS React Package Name + * @type {string} + */ +export const ODS_REACT_PACKAGE_NAME = '@ovhcloud/ods-react'; + +/** + * Base URL for NPM registry metadata queries. + * + * @constant {string} + */ +export const NPM_REGISTRY_BASE = 'https://registry.npmjs.org'; + +/** + * Endpoint for retrieving the latest metadata of the ODS React package. + * Includes version, tarball URL, and dependency list. + * + * @constant {string} + */ +export const ODS_COMPONENTS_LATEST_URL = `${NPM_REGISTRY_BASE}/@ovhcloud%2Fods-react/latest`; + +/** + * Subpath (within the GitHub tarball) that contains the ODS component stories. + * Used to filter relevant entries during extraction. + * + * @constant {string} + */ +export const ODS_TAR_COMPONENTS_PATH = '/packages/storybook/stories/components/'; + +/** + * Absolute path (within the ODS tarball) to Storybook base source files. + * These include components, constants, and helpers inside `packages/storybook/src`. + * + * @constant {string} + */ +export const ODS_TAR_STORYBOOK_PATH = 'packages/storybook/src/'; + +/** + * GitHub repository slug (organization/name) where the ODS tarball is hosted. + * + * @constant {string} + */ +export const ODS_GITHUB_REPO_NAME = 'ovh/design-system'; + +/** + * ODS Repository Base Url where the ODS tarball is hosted. + * + * @constant {string} + */ +export const ODS_GITHUB_REPO_BASE_URL = `https://github.com/${ODS_GITHUB_REPO_NAME}/archive/refs/tags`; + +/** + * Emoji constants used for consistent CLI log formatting. + * These provide visual cues in console output. + * + * @typedef {Object} Emojis + * @property {string} info - Informational messages. + * @property {string} check - Validation success. + * @property {string} folder - Directory operations. + * @property {string} package - Package operations. + * @property {string} warn - Warnings or recoverable issues. + * @property {string} error - Fatal or critical errors. + * @property {string} success - Successful completion. + * @property {string} disk - File system operations. + * @property {string} rocket - Final success or completion. + * + * @constant {Emojis} + */ +export const EMOJIS = { + info: 'ℹ️', + check: '✅', + folder: '📁', + package: '📦', + warn: '⚠️', + error: '❌', + success: '🎉', + disk: '💾', + rocket: '🚀', +}; + +/** + * List of ODS component names excluded from synchronization. + * These components are either deprecated, handled manually, + * or incompatible with automated documentation sync. + * + * @constant {string[]} + */ +export const ODS_EXCLUDED_COMPONENTS = ['pagination']; diff --git a/packages/manager-tools/manager-muk-cli/src/config/muk-template-config.js b/packages/manager-tools/manager-muk-cli/src/config/muk-template-config.js new file mode 100644 index 000000000000..a23b92e081c4 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/config/muk-template-config.js @@ -0,0 +1,227 @@ +/** + * Generate content for props file. + * @param {string} componentName - PascalCase component name. + * @param {boolean} hasChildren - Whether the component supports children. + * @returns {string} + */ +export function buildPropsTemplate(componentName, hasChildren = false) { + const importBase = `import { ${componentName}Prop as Ods${componentName}Props } from '@ovhcloud/ods-react';`; + + if (hasChildren) { + return `${importBase} +import { PropsWithChildren } from 'react'; + +export type ${componentName}Props = PropsWithChildren & {}; +`; + } + + return `${importBase} + +export type ${componentName}Props = Ods${componentName}Props & {}; +`; +} + +/** + * Generate content for component file. + * @param {string} componentName - PascalCase component name. + * @param {boolean} hasChildren - Whether the component supports children. + * @returns {string} + */ +export function buildComponentTemplate(componentName, hasChildren = false) { + const importLine = `import { ${componentName} as Ods${componentName} } from '@ovhcloud/ods-react'; +import { ${componentName}Props } from './${componentName}.props';`; + + if (hasChildren) { + return `${importLine} + +export const ${componentName} = ({ children, ...others }: ${componentName}Props) => ( + {children} +); +`; + } + + return `${importLine} + +export const ${componentName} = (props: ${componentName}Props) => ; +`; +} + +/** + * Generate index.ts template. + * @param {string} componentName - PascalCase component name. + * @returns {string} + */ +export function buildIndexTemplate(componentName) { + return `export { ${componentName} } from './${componentName}.component'; +export type { ${componentName}Props } from './${componentName}.props'; +`; +} + +/** + * Generate snapshot test template. + * @param {string} componentName - PascalCase component name. + * @param {boolean} hasChildren - Whether the component supports children. + * @returns {string} + */ +export function buildSnapshotTestTemplate(componentName, hasChildren = false) { + if (hasChildren) { + return `import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { ${componentName}, ${componentName}Props } from '..'; + +describe('${componentName} Snapshot tests', () => { + it('renders the component with default props and children', () => { + const { container } = render(<${componentName}>Hello); + expect(container).toMatchSnapshot(); + }); +}); +`; + } + + return `import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { ${componentName}, ${componentName}Props } from '..'; + +describe('${componentName} Snapshot tests', () => { + it('renders the component with default props', () => { + const { container } = render(<${componentName} />); + expect(container).toMatchSnapshot(); + }); +}); +`; +} + +/** + * Generate the base template for components index.ts + * Used when no index.ts exists yet in manager-react-components/src/components/ + * + * @returns {string} + */ +export function buildComponentsIndexTemplate() { + return `/** + * Auto-generated Manager React Components index. + * This file aggregates all component exports from ./src/components/ + * Do not edit manually — managed by MUK CLI. + */ + +`; +} + +/** + * Generate spec test template for subcomponents. + * @param {string} componentName - PascalCase subcomponent name. + * @returns {string} + */ +export function buildSubcomponentSpecTemplate(componentName) { + return `import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { ${componentName} } from '../${componentName}.component'; + +describe('${componentName}', () => { + it('renders without crashing', () => { + const { container } = render(<${componentName} />); + expect(container).toBeTruthy(); + }); +}); +`; +} + +/** + * Generate subcomponent props definition. + * + * @param {string} subName - PascalCase subcomponent name + * @param {string} parentName - PascalCase parent component name + * @param {boolean} hasOwnType - Whether the ODS package exports a dedicated prop type for this subcomponent + * @param {boolean} hasChildren - Whether the subcomponent supports children + * @returns {string} + */ +export function buildSubcomponentPropsTemplate( + subName, + parentName, + hasOwnType = false, + hasChildren = false, +) { + if (hasOwnType) { + const importBase = `import { ${subName}Prop as Ods${subName}Props } from '@ovhcloud/ods-react';`; + + if (hasChildren) { + return `${importBase} +import { PropsWithChildren } from 'react'; + +export type ${subName}Props = PropsWithChildren & {}; +`; + } + + return `${importBase} + +export type ${subName}Props = Ods${subName}Props & {}; +`; + } + + // Fallback to parent props + if (hasChildren) { + return `import { PropsWithChildren } from 'react'; +import { ${parentName}Props } from '../${parentName}.props'; + +export type ${subName}Props = PropsWithChildren<${parentName}Props> & {}; +`; + } + + return `import { ${parentName}Props } from '../${parentName}.props'; + +export type ${subName}Props = ${parentName}Props & {}; +`; +} + +/** + * Generate subcomponent implementation. + * + * @param {string} componentName - PascalCase subcomponent name. + * @param {boolean} hasChildren - Whether the subcomponent supports children. + * @returns {string} + */ +export function buildSubcomponentTemplate(componentName, hasChildren = false) { + const importBase = `import { ${componentName} as ODS${componentName} } from '@ovhcloud/ods-react'; +import { ${componentName}Props } from './${componentName}.props';`; + + if (hasChildren) { + return `${importBase} +import { PropsWithChildren } from 'react'; + +export const ${componentName} = ({ children, ...props }: PropsWithChildren<${componentName}Props>) => ( + {children} +); +`; + } + + return `${importBase} + +export const ${componentName} = (props: ${componentName}Props) => ( + +); +`; +} + +/** + * Generate index template for subcomponents (future-proof for nested children). + * @param {string} componentName - PascalCase subcomponent name. + * @returns {string} + */ +export function buildSubcomponentIndexTemplate(componentName) { + return `export { ${componentName} } from './${componentName}.component'; +`; +} + +/** + * Extend getComponentTemplates with subcomponent templates + */ +export function getComponentTemplates(componentName, hasChildren = false) { + return { + props: buildPropsTemplate(componentName, hasChildren), + component: buildComponentTemplate(componentName, hasChildren), + index: buildIndexTemplate(componentName), + test: buildSnapshotTestTemplate(componentName, hasChildren), + subSpec: buildSubcomponentSpecTemplate(componentName), + subIndex: buildSubcomponentIndexTemplate(componentName), + }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js b/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js new file mode 100644 index 000000000000..8bafa58e0375 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js @@ -0,0 +1,397 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { pipeline } from 'node:stream/promises'; + +import { + EMOJIS, + MUK_IMPORT_REWRITE_RULES, + MUK_STORYBOOK_ENTRY_REGEX, + MUK_STORYBOOK_FOLDERS, + MUK_WIKI_BASED_DOCUMENT, + MUK_WIKI_COMPONENTS, + ODS_REACT_PACKAGE_NAME, + ODS_TAR_COMPONENTS_PATH, + ODS_TAR_STORYBOOK_PATH, +} from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { ensureDir } from './file-utils.js'; +import { fetchLatestVersion } from './npm-utils.js'; +import { + extractComponentDocumentationInfos, + extractDesignSystemDocs, +} from './ods-documentation-tarball-utils.js'; +import { createAsyncQueue } from './tasks-utils.js'; + +/** + * Prepares target directories for a component’s base documentation. + * + * **Behavior:** + * - If the component folder does not exist → create it. + * - If the `base-component-doc` folder already exists → delete its contents. + * - Always ensure the final directory structure exists before streaming files. + * + * **Why:** + * Each synchronization cycle must start from a clean baseline + * to avoid stale or conflicting documentation files. + * + * @param {string} componentDir - Absolute path to the component directory. + * @param {string} baseDocDir - Absolute path to the `base-component-doc` folder. + */ +function prepareComponentDocumentationDir(componentDir, baseDocDir) { + if (fs.existsSync(baseDocDir)) { + fs.rmSync(baseDocDir, { recursive: true, force: true }); + } else if (!fs.existsSync(componentDir)) { + ensureDir(componentDir); + } + ensureDir(baseDocDir); +} + +/** + * Initialize or refresh a component documentation directory. + * + * Handles three cases: + * - Component folder does not exist → create new folder and base-doc. + * - Component folder exists → clean and reinitialize base-doc. + * - Always ensures base-doc directory exists before file writes. + * + * @param {string} component - Component name. + * @returns {{componentDir: string, baseDocDir: string, isNew: boolean, isUpdated: boolean}} + */ +function initializeComponentDocs(component) { + const componentDir = path.join(MUK_WIKI_COMPONENTS, component); + const baseDocDir = path.join(componentDir, 'base-component-doc'); + const exists = fs.existsSync(componentDir); + + if (exists) { + logger.info(`${EMOJIS.folder} Found existing component: '${component}'`); + } else { + logger.info(`${EMOJIS.folder} Creating new component directory: '${component}'`); + } + + prepareComponentDocumentationDir(componentDir, baseDocDir); + + if (exists) { + logger.info(`${EMOJIS.disk} Cleared and ready: ${path.relative(process.cwd(), baseDocDir)}`); + } else { + logger.info(`${EMOJIS.rocket} Initialized new base-doc folder for '${component}'`); + } + + return { + componentDir, + baseDocDir, + isNew: !exists, + isUpdated: exists, + }; +} + +/** + * Stream a single documentation file to disk. + * + * Handles subdirectory creation, error handling, and logging. + * + * @param {import('node:stream').Readable} stream - The file stream. + * @param {string} baseDocDir - Base documentation directory for the component. + * @param {string} relPath - Relative path of the file within the component. + */ +async function writeComponentDocFile(stream, baseDocDir, relPath) { + const destFile = path.join(baseDocDir, relPath); + const subDir = path.dirname(destFile); + + ensureDir(subDir); + + const relativeTarget = path.relative(process.cwd(), destFile); + logger.info(`${EMOJIS.disk} Writing file → ${relativeTarget}`); + + try { + await pipeline(stream, fs.createWriteStream(destFile)); + } catch (err) { + logger.error(`${EMOJIS.error} Failed to write ${relativeTarget}: ${err.message}`); + } +} + +/** + * Stream extracted documentation files to disk directly as they are read + * from the ODS Design System tarball. + * + * This function orchestrates: + * 1️⃣ Parsing tar paths → (component, relPath) + * 2️⃣ Initializing per-component folders (once) + * 3️⃣ Writing documentation files via streaming + * + * @param {AsyncGenerator<{tarPath: string, stream: import('node:stream').Readable}>} fileStreamGenerator + * @returns {Promise<{created: number, updated: number, total: number}>} + */ +async function streamComponentDocs(fileStreamGenerator) { + const initializedComponents = new Set(); + let created = 0; + let updated = 0; + let total = 0; + + logger.info(`${EMOJIS.info} Starting component documentation sync (streaming mode)…`); + + for await (const { tarPath, stream } of fileStreamGenerator) { + const { component, relPath } = extractComponentDocumentationInfos(tarPath); + if (!component || !relPath) { + logger.debug?.(`Skipping unrelated entry: ${tarPath}`); + continue; + } + + let baseDocDir; + + // Initialize directories once per component + if (!initializedComponents.has(component)) { + const { baseDocDir: docDir, isNew, isUpdated } = initializeComponentDocs(component); + initializedComponents.add(component); + baseDocDir = docDir; + + if (isNew) created++; + if (isUpdated) updated++; + } else { + baseDocDir = path.join(MUK_WIKI_COMPONENTS, component, 'base-component-doc'); + } + + // Stream file into local folder + await writeComponentDocFile(stream, baseDocDir, relPath); + total++; + } + + logger.success( + `${EMOJIS.check} Completed streaming sync — created: ${created}, updated: ${updated}, files written: ${total}`, + ); + + return { created, updated, total }; +} + +/** + * Filter ODS tarball entries to include only component documentation files. + * + * Matches files under the Design System's component documentation path, such as: + * - documentation.mdx + * - technical-information.mdx + * - .stories.tsx (storybook files) + * + * @param {string} filePath - Full path of a tarball entry. + * @returns {boolean} True if the entry should be processed. + */ +function isOdsComponentDocEntry(filePath) { + return ( + filePath.includes(ODS_TAR_COMPONENTS_PATH) && + (filePath.endsWith('.mdx') || filePath.endsWith('.md') || filePath.endsWith('.stories.tsx')) + ); +} + +/** + * Synchronize all ODS component documentation files. + * Streams entries from GitHub tarball (or cache) into wiki component directories. + * + * Uses a streaming producer–consumer pipeline: + * - Producer → extractDesignSystemDocs (streams tar entries) + * - Queue → createAsyncQueue (handles backpressure) + * - Consumer → streamComponentDocs (writes files to disk) + * + * @async + * @returns {Promise<{created: number, updated: number, total: number}>} + */ +export async function syncComponentDocs() { + const queue = createAsyncQueue(); + + // 🧩 Producer: fetch latest ODS React version + const latestVersion = await fetchLatestVersion(ODS_REACT_PACKAGE_NAME); + logger.info(`${EMOJIS.info} ODS React latest version: ${latestVersion}`); + + await (async () => { + await extractDesignSystemDocs({ + tag: latestVersion, + filter: isOdsComponentDocEntry, + onFileStream: async (tarPath, fileStream) => { + await queue.push({ tarPath, stream: fileStream }); + }, + }); + queue.end(); // Signal the end of production + })(); + + // 💾 Consumer: write streamed documentation files + return streamComponentDocs(queue); +} + +/** + * Determines whether a tar entry corresponds to Storybook source files + * under `packages/storybook/src/{components,constants,helpers}`. + * + * @param {string} tarPath + * @returns {boolean} + */ +function isStorybookBaseDocEntry(tarPath) { + const normalized = tarPath.replaceAll('\\', '/'); + return MUK_STORYBOOK_ENTRY_REGEX.test(normalized); +} + +/** + * Maps a tar entry path under `packages/storybook/src/` + * to the Manager Wiki base-documents directory. + * + * Example: + * design-system-19.2.1/packages/storybook/src/helpers/date/formatDate.ts + * → packages/manager-wiki/stories/manager-ui-kit/base-documents/helpers/date/formatDate.ts + */ +function mapStorybookPathToWiki(tarPath) { + const normalized = tarPath.replaceAll('\\', '/'); + const marker = ODS_TAR_STORYBOOK_PATH; + const idx = normalized.indexOf(marker); + + if (idx === -1) { + logger.warn(`${EMOJIS.warn} Unexpected tar path: ${tarPath}`); + return path.join(MUK_WIKI_BASED_DOCUMENT, path.basename(tarPath)); + } + + const rel = normalized.substring(idx + marker.length); + return path.join(MUK_WIKI_BASED_DOCUMENT, rel); +} + +/** + * Writes a streamed Storybook file from the tarball to disk. + * Creates all required directories if they do not exist. + * + * @async + * @param {string} tarPath - Path of the file in the tar archive. + * @param {ReadableStream} fileStream - The stream for the tar entry. + * @returns {Promise<{created:number, updated:number}>} Statistics on the write operation. + */ +async function writeStorybookFile(tarPath, fileStream) { + const target = mapStorybookPathToWiki(tarPath); + await ensureDir(path.dirname(target)); + try { + await pipeline(fileStream, fs.createWriteStream(target)); + logger.debug(`${EMOJIS.disk} ${path.relative(process.cwd(), target)}`); + return { created: 1, updated: 0 }; + } catch (err) { + logger.error(`${EMOJIS.error} Failed to write ${target}: ${err.message}`); + return { created: 0, updated: 0 }; + } +} + +/** + * Ensures that all base Storybook folders exist in the wiki output, + * even if the tarball doesn’t contain any file for them. + * + * This prevents missing directories like "helpers" or "constants" + * when they have no eligible files or only subfolders. + * + * @param {string} baseDir - The base wiki directory (MUK_WIKI_BASED_DOCUMENT). + */ +function ensureBaseStorybookFolders(baseDir) { + for (const directory of MUK_STORYBOOK_FOLDERS) { + const target = path.join(baseDir, directory); + if (!fs.existsSync(target)) { + fs.mkdirSync(target, { recursive: true }); + logger.info( + `${EMOJIS.folder} Created base Storybook folder: ${path.relative(process.cwd(), target)}`, + ); + } + } +} + +/** + * Synchronizes all Storybook base documents from the ODS tarball into + * the Manager Wiki. This includes every file located under: + * + * - packages/storybook/src/components/ + * - packages/storybook/src/constants/ + * - packages/storybook/src/helpers/ + * + * @async + * @param {Object} [options] - Optional configuration. + * @param {string} [options.tag] - Specific ODS release tag to download. Defaults to latest. + * @returns {Promise<{created:number, updated:number, total:number}>} + * Count of streamed and written files. + * + * @example + * await syncStorybookBaseDocuments({ tag: '19.2.1' }) + * // → { created: 53, updated: 0, total: 53 } + */ +export async function syncStorybookBaseDocuments({ tag } = {}) { + let created = 0; + let updated = 0; + let total = 0; + + // 🗂 Ensure base folders exist even if empty + ensureBaseStorybookFolders(MUK_WIKI_BASED_DOCUMENT); + + await extractDesignSystemDocs({ + tag, + filter: isStorybookBaseDocEntry, + onFileStream: async (tarPath, fileStream) => { + const res = await writeStorybookFile(tarPath, fileStream); + created += res.created; + updated += res.updated; + total += 1; + }, + }); + + return { created, updated, total }; +} + +/** + * Recursively collect all source files inside a directory. + * @param {string} dir - The root directory to scan. + * @param {string[]} exts - File extensions to include (e.g. ['.ts', '.tsx', '.mdx']) + * @returns {string[]} Absolute paths of matching files. + */ +function collectSourceFiles(dir, exts = ['.ts', '.tsx', '.mdx']) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectSourceFiles(fullPath, exts)); + } else if (exts.includes(path.extname(entry.name))) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Apply import path rewrites for Manager Wiki component documentation. + * + * Rewrites two patterns: + * 1) `../../../src/` → `../../../base-documents/` + * 2) `/ods-react/src/` → `@ovhcloud/ods-react` + * + * Example: + * ```diff + * - import { CONTROL_CATEGORY } from '../../../src/constants/controls'; + * + import { CONTROL_CATEGORY } from '../../../base-documents/constants/controls'; + * + * - import { Accordion } from '../../../../ods-react/src/components/accordion/src'; + * + import { Accordion } from '@ovhcloud/ods-react'; + * ``` + * + * @param {string} componentsRoot - Absolute path to wiki components root (MUK_WIKI_COMPONENTS) + */ +export function rewriteWikiComponentImports(componentsRoot) { + const files = collectSourceFiles(componentsRoot); + let updatedCount = 0; + + logger.info(`${EMOJIS.info} Rewriting import paths inside wiki component documentation…`); + + for (const file of files) { + let content = fs.readFileSync(file, 'utf8'); + let modified = content; + + for (const rule of MUK_IMPORT_REWRITE_RULES) { + modified = modified.replace(rule.pattern, rule.replacer); + } + + if (modified !== content) { + fs.writeFileSync(file, modified, 'utf8'); + updatedCount++; + logger.info(`${EMOJIS.disk} Updated imports → ${path.relative(process.cwd(), file)}`); + } + } + + logger.success(`${EMOJIS.check} Rewrote imports in ${updatedCount} files.`); +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/component-utils.js b/packages/manager-tools/manager-muk-cli/src/core/component-utils.js new file mode 100644 index 000000000000..0e109ea0554a --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/component-utils.js @@ -0,0 +1,66 @@ +import { EMOJIS } from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { detectHasChildrenFromTarball } from './ods-components-tarball-utils.js'; + +/** + * Dynamically groups ODS components based on their naming structure. + * + * Example: + * ``` + * ['button', 'form-field', 'form-field-error', 'form-field-helper'] + * → { 'button': [], 'form-field': ['form-field-error', 'form-field-helper'] } + * ``` + * + * The algorithm: + * 1. Splits each kebab-case component name into parts. + * 2. For single-part names, marks them as standalone. + * 3. For multi-part names, progressively checks whether intermediate + * prefixes (e.g., `form-field`) represent actual component families + * that have subcomponents in the tarball. + * 4. Logs a summary of detected parent-child relationships. + * + * Why: + * - Some ODS components (like `form-field-error`) are logically children + * of a higher-level component (`form-field`). Grouping these helps + * generate structured documentation and avoid duplication. + * + * @async + * @param {string[]} components - All ODS component names in kebab-case. + * @returns {Promise>} - Map of `{ parent → [children] }`. + */ +export async function groupComponentsDynamically(components) { + const grouped = {}; + + for (const name of components) { + const parts = name.split('-'); + + // Single-part components (like "range") are always standalone + if (parts.length === 1) { + if (!grouped[name]) grouped[name] = []; + continue; + } + + // Multi-part components — progressively detect if any prefix is a parent + let parent = parts[0]; + let candidate = parts[0]; + + for (let i = 1; i < parts.length; i++) { + candidate = `${candidate}-${parts[i]}`; + const hasChildren = await detectHasChildrenFromTarball(candidate); + + if (hasChildren) parent = candidate; + } + + // Fallback: treat as flat component if no hierarchy detected + if (!grouped[parent]) grouped[parent] = []; + if (parent !== name) grouped[parent].push(name); + } + + // Log grouping summary for debugging and visibility + const summary = Object.entries(grouped) + .map(([p, c]) => `${p} → ${c.length ? c.join(', ') : '(no children)'}`) + .join('\n'); + logger.info(`${EMOJIS.package} Dynamic grouping summary:\n${summary}`); + + return grouped; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/file-utils.js b/packages/manager-tools/manager-muk-cli/src/core/file-utils.js new file mode 100644 index 000000000000..bd65ad2eaccc --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/file-utils.js @@ -0,0 +1,135 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { logger } from '../utils/log-manager.js'; + +/** + * Utility: convert kebab-case to PascalCase. + * Example: "breadcrumb-item" → "BreadcrumbItem" + * @param {string} name + * @returns {string} + */ +export function toPascalCase(name) { + return name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); +} + +/** + * Convert any string to kebab-case (e.g., "FormFieldLabel" → "form-field-label"). + * Ensures consistent folder naming across components and subcomponents. + */ +export function toKebabCase(value) { + return value + .replace(/\s*\(.*\)$/, '') // remove parentheses or version suffixes + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // camelCase → kebab-case + .replace(/--+/g, '-') // collapse multiple dashes + .toLowerCase(); +} + +/** + * Create directory recursively if it doesn't exist. + * @param {string} dirPath + */ +export function ensureDir(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + logger.info(`📁 Created folder: ${dirPath}`); + } +} + +/** + * Safely create a file with content. + * - Ensures parent directory exists + * - Avoids overwriting existing files + * - Handles write errors gracefully + * - Automatically trims leading blank lines + * + * @param {string} filePath - Absolute path of the file to create + * @param {string} content - File content + */ +export function createFile(filePath, content = '') { + try { + // ✅ Ensure the parent directory exists + const dirPath = path.dirname(filePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + logger.info(`📁 Created folder: ${dirPath}`); + } + + // ✅ Only create if not already existing + if (fs.existsSync(filePath)) { + logger.info(`ℹ File already exists, skipping: ${filePath}`); + return; + } + + fs.writeFileSync(filePath, content.trimStart() + '\n', 'utf8'); + logger.info(`📝 Created file: ${filePath}`); + } catch (err) { + logger.error(`❌ Failed to create file at ${filePath}: ${err.message}`); + } +} + +/** + * Safely read a file, returning an empty string if it doesn't exist. + * Logs a warning when missing, optionally controlled by a flag. + * + * @param {string} filePath - Absolute file path to read. + * @param {boolean} [warnIfMissing=true] - Whether to log a warning if file is missing. + * @returns {string} - File contents (or empty string if not found). + */ +export function readFile(filePath, warnIfMissing = true) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (err) { + if (err.code === 'ENOENT') { + if (warnIfMissing) logger.warn(`⚠ File not found: ${filePath}`); + return ''; + } + throw err; + } +} + +/** + * Safely overwrite a file with new content. + * - Ensures parent directory exists + * - Overwrites existing file content + * - Trims leading blank lines + * + * @param {string} filePath + * @param {string} content + */ +export function writeFile(filePath, content = '') { + try { + const dirPath = path.dirname(filePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + logger.info(`📁 Created folder: ${dirPath}`); + } + + fs.writeFileSync(filePath, content.trimStart() + '\n', 'utf8'); + logger.info(`📝 Updated file: ${filePath}`); + } catch (err) { + logger.error(`❌ Failed to write file at ${filePath}: ${err.message}`); + } +} + +/** + * Write a JSON file safely (pretty-printed). + * @param {string} file - Target path. + * @param {any} data - Serializable data. + */ +export function saveJson(file, data) { + ensureDir(path.dirname(file)); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +/** + * Read a JSON file if it exists. + * @param {string} file - File path. + * @returns {any|null} Parsed JSON or null if missing. + */ +export function loadJson(file) { + return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : null; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/npm-utils.js b/packages/manager-tools/manager-muk-cli/src/core/npm-utils.js new file mode 100644 index 000000000000..008b6972ed70 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/npm-utils.js @@ -0,0 +1,82 @@ +import https from 'node:https'; +import semver from 'semver'; + +import { EMOJIS, NPM_REGISTRY_BASE } from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * Fetch the latest version of a package from the npm registry. + * @param {string} pkgName - Package name (e.g., "@ovh-ux/ui-kit"). + * @returns {Promise} Latest version string (e.g., "19.4.0"). + */ +export async function fetchLatestVersion(pkgName) { + const url = `${NPM_REGISTRY_BASE}/${pkgName.replace('/', '%2F')}/latest`; + return new Promise((resolve, reject) => { + https + .get(url, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + resolve(parsed.version); + } catch (err) { + reject(err); + } + }); + }) + .on('error', reject); + }); +} + +/** + * Compare local ODS versions in a package.json object with latest npm versions. + * Detects both outdated and ahead-of-latest versions. + * + * @param {object} pkgJson - Parsed package.json contents. + * @param {string[]} targetPackages - List of packages to compare. + * @returns {Promise>} + */ +export async function getOutdatedPackages(pkgJson, targetPackages) { + const results = []; + + logger.info(`${EMOJIS.info} Checking ODS package versions...`); + + for (const name of targetPackages) { + const localVersion = + pkgJson.devDependencies?.[name] || + pkgJson.dependencies?.[name] || + pkgJson.peerDependencies?.[name]; + + if (!localVersion) { + logger.warn(`${EMOJIS.warn} ${name} not found in local package.json`); + continue; + } + + const cleanLocal = localVersion.replace(/[\^~]/g, ''); + const latest = await fetchLatestVersion(name); + + // Compare semantically + if (semver.valid(cleanLocal) && semver.valid(latest)) { + if (semver.gt(cleanLocal, latest)) { + logger.info( + `${EMOJIS.rocket} ${name} is ahead of npm (local ${cleanLocal} > latest ${latest})`, + ); + results.push({ name, local: cleanLocal, latest, status: 'ahead' }); + } else if (semver.lt(cleanLocal, latest)) { + logger.warn(`${EMOJIS.warn} ${name} is outdated (local ${cleanLocal} < latest ${latest})`); + results.push({ name, local: cleanLocal, latest, status: 'outdated' }); + } else { + logger.success(`${name} is up to date (${latest})`); + results.push({ name, local: cleanLocal, latest, status: 'equal' }); + } + } else { + // Fallback for non-semver or prerelease versions + const status = cleanLocal === latest ? 'equal' : 'unknown'; + results.push({ name, local: cleanLocal, latest, status }); + logger.info(`${EMOJIS.info} ${name} uses non-standard version (${cleanLocal})`); + } + } + + return results; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js new file mode 100644 index 000000000000..01e8c6bc2bc4 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js @@ -0,0 +1,330 @@ +import https from 'node:https'; + +import { + DISABLE_ODS_COMPONENTS_CACHE, + EMOJIS, + ODS_COMPONENTS_CACHE_DIR, + ODS_COMPONENTS_LATEST_URL, + ODS_COMPONENTS_META_CACHE_FILE, + ODS_COMPONENTS_TAR_CACHE_FILE, +} from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { toPascalCase } from './file-utils.js'; +import { createTarballCache } from './tarball-cache-utils.js'; +import { streamTarGz } from './tarball-utils.js'; + +/** + * Fetch the latest metadata for the ODS Components NPM package. + * + * This function retrieves the `version` and `dist.tarball` URL from the NPM registry. + * It is used by `extractOdsComponentsTarball()` to determine which version of the + * tarball to download. + * + * @async + * @returns {Promise<{ version: string, tarball: string }>} Package version and tarball URL. + */ +export async function getOdsComponentsPackageMetadata() { + return new Promise((resolve, reject) => { + https + .get(ODS_COMPONENTS_LATEST_URL, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + try { + const json = JSON.parse(data); + resolve({ version: json.version, tarball: json.dist.tarball }); + } catch (err) { + reject(err); + } + }); + }) + .on('error', reject); + }); +} + +/** + * Extracts and caches the contents of the latest ODS Components tarball. + * + * The tarball is downloaded from the NPM registry, decompressed, and parsed. + * Extracted file contents are stored in a `Map` keyed by their relative paths. + * + * To avoid redundant network calls, the result is cached locally using + * `createTarballCache()`. Cache can be disabled with the environment variable: + * `ADD_COMPONENTS_NO_CACHE=1` + * + * @async + * @param {RegExp} [pattern] - Optional RegExp to filter which paths to include. + * @returns {Promise>} Map of file paths → file contents (UTF-8). + */ +export async function extractOdsComponentsTarball(pattern) { + const { version, tarball } = await getOdsComponentsPackageMetadata(); + + const cache = createTarballCache({ + cacheDir: ODS_COMPONENTS_CACHE_DIR, + metaFile: ODS_COMPONENTS_META_CACHE_FILE, + dataFile: ODS_COMPONENTS_TAR_CACHE_FILE, + }); + + // Try to load from cache unless disabled + if (!DISABLE_ODS_COMPONENTS_CACHE) { + const cached = cache.load(version); + if (cached) { + if (pattern) { + return new Map([...cached.entries()].filter(([name]) => pattern.test(name))); + } + return cached; + } + } + + logger.info(`${EMOJIS.package} Fetching ODS React v${version} tarball: ${tarball}`); + + const files = new Map(); + + await streamTarGz( + tarball, + (entryPath) => !pattern || pattern.test(entryPath), + async (entryPath, content) => { + files.set(entryPath, content.toString()); + }, + ); + + cache.save(version, files); + return files; +} + +/** + * Component file path templates used to locate `.tsx` source files + * across various ODS directory structures. + * + * Some components live under `src/components`, others under + * `package/src/components`, and subcomponents often use PascalCase folders. + */ +const ODS_PATH_PATTERNS = { + withSub: { + legacyNested: 'components/${parent}/src/components/${target}/${pascalSub}.tsx', + flatModern: 'components/${parent}/src/${pascalSub}.tsx', + pascalFolder: 'components/${parent}/src/components/${pascalSub}/${pascalSub}.tsx', + }, + withoutSub: { + modern: 'components/${parent}/src/${pascalParent}.tsx', + legacyNested: 'components/${parent}/src/components/${parent}/${pascalParent}.tsx', + direct: 'components/${parent}/${pascalParent}.tsx', + }, +}; + +/** + * Expand a template string with provided path parameters. + * @param {string} template - Path template with placeholders. + * @param {object} context - Replacement values for placeholders. + * @returns {string} Expanded file path. + */ +function expandTemplate(template, context) { + return template + .replace(/\$\{parent\}/g, context.parent) + .replace(/\$\{target\}/g, context.target) + .replace(/\$\{pascalParent\}/g, context.pascalParent) + .replace(/\$\{pascalSub\}/g, context.pascalSub); +} + +/** + * Factory function that returns path builders for ODS component files. + * + * @param {string} parent - Parent component name (kebab-case). + * @param {string} [subcomponent] - Optional subcomponent name. + * @returns {{ + * buildAll(): string[], + * build(filter?: string): string[], + * buildByKey(key: string): string[] + * }} + */ +function createOdsComponentsPath(parent, subcomponent) { + const pascalParent = toPascalCase(parent); + const pascalSub = subcomponent ? toPascalCase(subcomponent) : pascalParent; + const target = subcomponent ?? parent; + const roots = ['src', 'package/src']; + const patternGroup = subcomponent ? ODS_PATH_PATTERNS.withSub : ODS_PATH_PATTERNS.withoutSub; + + const buildSet = (patterns) => + roots.flatMap((root) => + patterns.map( + (tpl) => `${root}/${expandTemplate(tpl, { parent, target, pascalParent, pascalSub })}`, + ), + ); + + return { + buildAll() { + return buildSet(Object.values(patternGroup)); + }, + build(filter) { + const filtered = Object.entries(patternGroup) + .filter(([key]) => !filter || key.includes(filter)) + .map(([, tpl]) => tpl); + return buildSet(filtered); + }, + buildByKey(key) { + const tpl = patternGroup[key]; + return tpl ? buildSet([tpl]) : []; + }, + }; +} + +/** + * Attempt to find and return a matching component source file from the tarball. + * + * @param {Map} files - Map of tarball entries. + * @param {string[]} possiblePaths - List of possible candidate paths. + * @param {string} name - Component name (for logging). + * @returns {string|null} File content as UTF-8 string, or null if not found. + */ +function findOdsComponentsSourceFile(files, possiblePaths, name) { + const fileEntry = possiblePaths.map((p) => files.get(p)).find(Boolean); + if (!fileEntry) { + logger.warn(`⚠ Could not find source file for ${name}`); + return null; + } + return fileEntry.toString('utf8'); +} + +/** + * Simple heuristic-based detection for `children` support in React components. + * Checks for patterns like: + * - `PropsWithChildren` + * - `children:` in prop definitions + * - `props.children` usage + * - JSX child placeholders + * + * @param {string} content - TypeScript source file content. + * @returns {boolean} Whether the component likely accepts `children`. + */ +function detectChildrenHeuristics(content) { + return [ + /\bPropsWithChildren\b/, + /\bchildren\s*:\s*/, + /props\.children/, + /<.*>\s*{.*children.*}\s*<\/.*>/s, + ].some((re) => re.test(content)); +} + +/** + * Detect whether an ODS component (or subcomponent) supports React `children`. + * + * Downloads (or loads from cache) the ODS tarball, locates the relevant `.tsx` file, + * and applies heuristic-based analysis. + * + * @async + * @param {string} parent - Component name (kebab-case). + * @param {string} [subcomponent] - Optional subcomponent. + * @returns {Promise} True if supports children, false otherwise, null if not found. + */ +export async function detectHasChildrenFromTarball(parent, subcomponent) { + const files = await extractOdsComponentsTarball(); + const factory = createOdsComponentsPath(parent, subcomponent); + const possiblePaths = factory.buildAll(); + const content = findOdsComponentsSourceFile(files, possiblePaths, subcomponent ?? parent); + + if (!content) return null; + const hasChildren = detectChildrenHeuristics(content); + + logger.info( + `${hasChildren ? '👶' : '🚫'} ${subcomponent ?? parent} ${ + hasChildren ? 'supports' : 'has no' + } children`, + ); + + return hasChildren; +} + +/** + * Detect whether a subcomponent exports its own prop type (e.g., `type MySubProp`). + * + * Used to infer type granularity for documentation generation. + * + * @async + * @param {string} parent - Parent component name. + * @param {string} subcomponent - Subcomponent name. + * @returns {Promise} Whether a prop type is exported from its index. + */ +export async function detectHasTypeExportFromIndex(parent, subcomponent) { + const files = await extractOdsComponentsTarball(); + const possiblePaths = [ + `src/components/${parent}/src/index.ts`, + `package/src/components/${parent}/src/index.ts`, + ]; + const fileEntry = possiblePaths.map((p) => files.get(p)).find(Boolean); + if (!fileEntry) { + logger.warn(`⚠ Could not find index.ts for ${parent}`); + return false; + } + + const content = fileEntry.toString('utf8'); + const subPascal = toPascalCase(subcomponent); + const typeName = `${subPascal}Prop`; + const typeExportRegex = new RegExp(`\\btype\\s+${typeName}\\b|\\b${typeName}\\b`, 'g'); + const found = typeExportRegex.test(content); + + logger.info( + `${found ? '🧩' : '🚫'} ${subcomponent} ${ + found ? 'exports' : 'does not export' + } its own Prop type`, + ); + return found; +} + +/** + * Extract categorized exports (hooks, constants, external types) from a component index file. + * + * @async + * @param {string} parent - Component name. + * @returns {Promise<{hooks: string[], constants: string[], externalTypes: string[]}>} + */ +export async function extractOdsComponentsExportsByCategory(parent) { + const files = await extractOdsComponentsTarball(/src\/components\/.*\/src\/index\.ts$/); + const entry = [...files.entries()].find(([p]) => + p.endsWith(`src/components/${parent}/src/index.ts`), + ); + + if (!entry) { + logger.warn(`⚠ No index.ts found for component '${parent}'`); + return { hooks: [], constants: [], externalTypes: [] }; + } + + const content = entry[1]; + const exports = [...content.matchAll(/export\s+\{([\s\S]*?)\}\s+from\s+['"](.*?)['"]/g)]; + + const hooks = new Set(); + const constants = new Set(); + const externalTypes = new Set(); + + for (const [, block, fromPath] of exports) { + const identifiers = block + .split(',') + .map((i) => i.trim()) + .filter(Boolean); + + // Ignore re-exports from subcomponents + if (fromPath.includes('components')) continue; + + identifiers + .map((id) => id.replace(/^type\s+/, '')) + .filter((id) => /^use[A-Z]/.test(id)) + .forEach((h) => hooks.add(h)); + + if (fromPath.includes('constants')) { + identifiers + .filter((id) => !/^type\s+/i.test(id) && !/^interface\s+/i.test(id)) + .forEach((c) => constants.add(c.replace(/^type\s+/, ''))); + continue; + } + + identifiers + .filter((id) => /^type\s+/i.test(id) || /^interface\s+/i.test(id)) + .map((id) => id.replace(/^(type|interface)\s+/, '')) + .forEach((t) => externalTypes.add(t)); + } + + return { + hooks: [...hooks].sort(), + constants: [...constants].sort(), + externalTypes: [...externalTypes].sort(), + }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js new file mode 100644 index 000000000000..09e6ea5ff03c --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js @@ -0,0 +1,167 @@ +import { Buffer } from 'node:buffer'; +import { Readable } from 'node:stream'; + +import { + DISABLE_ODS_DOCS_CACHE, + EMOJIS, + ODS_DOCS_CACHE_DIR, + ODS_DOCS_META_CACHE_FILE, + ODS_DOCS_TAR_CACHE_FILE, + ODS_GITHUB_REPO_BASE_URL, + ODS_TAR_COMPONENTS_PATH, +} from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { getOdsComponentsPackageMetadata } from './ods-components-tarball-utils.js'; +import { createTarballCache } from './tarball-cache-utils.js'; +import { streamTarGz } from './tarball-utils.js'; + +/** + * Normalize cached data to an iterable [path, buffer] format. + * @param {Map|Object} cached - Cached data structure. + * @returns {[string, Buffer][]} + */ +function normalizeCacheEntries(cached) { + if (cached instanceof Map) return [...cached.entries()]; + if (cached && typeof cached === 'object') return Object.entries(cached); + return []; +} + +/** + * Safely load cached ODS documentation, handling both Map and plain objects. + * @param {ReturnType} cache + * @param {string} version + * @returns {[string, Buffer][]|null} + */ +function getOdsDocsCache(cache, version) { + const cached = cache.load(version); + if (!cached) return null; + + const entries = normalizeCacheEntries(cached); + if (entries.length === 0) { + logger.warn(`⚠️ Cache is valid but empty, skipping stream.`); + return null; + } + + // 🧠 Validation: ensure cache includes storybook/src files + const hasStorybookSrc = entries.some(([p]) => p.includes('packages/storybook/src/')); + if (!hasStorybookSrc) { + logger.warn(`${EMOJIS.warn} Cache missing Storybook sources — forcing re-download.`); + return null; + } + + logger.info(`${EMOJIS.check} Using cached ODS documentation (v${version})`); + return entries; +} + +/** + * Stream documentation files from cache after applying filter. + */ +async function streamCachedDocs(entries, filter, onFile, onFileStream) { + let streamed = 0; + + for (const [entryPath, buffer] of entries) { + if (!filter(entryPath)) continue; + + const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); + if (onFileStream) { + const stream = Readable.from(buf); + await onFileStream(entryPath, stream); + } else { + await onFile(entryPath, buf); + } + streamed++; + } + + logger.success(`${EMOJIS.disk} Streamed ${streamed} documentation files from cache.`); +} + +/** + * Download ODS docs tarball, stream files, and save cache. + * The filter is applied *after* caching, so the cache always contains the full tarball. + */ +async function downloadAndCacheDocs({ url, version, filter, onFile, onFileStream, cache }) { + logger.info(`${EMOJIS.package} Fetching ODS Design System tarball from ${url}`); + const files = new Map(); + + await streamTarGz( + url, + () => true, + async (entryPath, content) => { + const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content); + files.set(entryPath, buffer); + + // Apply filter only for streamed consumer events + if (!filter(entryPath)) return; + + if (onFileStream) { + const stream = Readable.from(buffer); + await onFileStream(entryPath, stream); + } else { + await onFile(entryPath, buffer); + } + }, + ); + + cache.save(version, files); + logger.success( + `${EMOJIS.check} Cached ODS Design System documentation (v${version}) for future runs.`, + ); +} + +/** + * High-level orchestrator for ODS Design System documentation extraction. + * Uses cache when available, otherwise streams from tarball. + */ +export async function extractDesignSystemDocs({ + filter = () => true, + onFile = async () => {}, + onFileStream = null, + tag = null, +}) { + const { version } = await getOdsComponentsPackageMetadata(); + const resolvedTag = tag ?? version; + const url = `${ODS_GITHUB_REPO_BASE_URL}/v${resolvedTag}.tar.gz`; + + logger.info(`${EMOJIS.package} Preparing to extract ODS docs (v${resolvedTag})…`); + + const cache = createTarballCache({ + cacheDir: ODS_DOCS_CACHE_DIR, + metaFile: ODS_DOCS_META_CACHE_FILE, + dataFile: ODS_DOCS_TAR_CACHE_FILE, + }); + + if (!DISABLE_ODS_DOCS_CACHE) { + const entries = getOdsDocsCache(cache, version); + if (entries) { + await streamCachedDocs(entries, filter, onFile, onFileStream); + return; + } + } + + await downloadAndCacheDocs({ + url, + version, + filter, + onFile, + onFileStream, + cache, + }); +} + +/** + * Extract component-level info from tarball entry path. + */ +export function extractComponentDocumentationInfos(tarPath) { + const idx = tarPath.indexOf(ODS_TAR_COMPONENTS_PATH); + if (idx < 0) return { component: null, relPath: null }; + + const relFromComponents = tarPath.slice(idx + ODS_TAR_COMPONENTS_PATH.length); + const parts = relFromComponents.split('/').filter(Boolean); + if (parts.length < 2) return { component: null, relPath: null }; + + const [component, ...rest] = parts; + const relPath = rest.join('/'); + if (!relPath || relPath.endsWith('/')) return { component: null, relPath: null }; + + return { component, relPath }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js new file mode 100644 index 000000000000..1db785686b4b --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js @@ -0,0 +1,150 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; + +import { logger } from '../utils/log-manager.js'; +import { ensureDir, loadJson, saveJson } from './file-utils.js'; + +/** + * Validate cache metadata consistency with expected version. + * @param {object} meta - Metadata object read from cache. + * @param {string} version - Expected version string. + * @returns {string|null} Error message if invalid, otherwise null. + */ +function validateMeta(meta, version) { + if (!meta || typeof meta !== 'object') return 'invalid metadata'; + if (meta.version !== version) return `version mismatch (${meta.version} ≠ ${version})`; + return null; +} + +/** + * Validate cache freshness based on timestamp and TTL. + * @param {number} timestamp - Unix epoch of cache creation. + * @param {number} ttlMs - Time-to-live in milliseconds. + * @returns {{ expired: boolean, age?: number, message?: string }} + * `expired` is true if TTL exceeded, otherwise contains age in ms. + */ +function validateTTL(timestamp, ttlMs) { + const age = Date.now() - timestamp; + if (age > ttlMs) { + const daysOld = (age / 86_400_000).toFixed(1); + return { expired: true, message: `${daysOld} days old` }; + } + return { expired: false, age }; +} + +/** + * Compute SHA256 checksum for any serializable data. + * @param {object|string} obj - Object or string to hash. + * @returns {string} SHA256 hex digest. + */ +function computeChecksum(obj) { + const data = typeof obj === 'string' ? obj : JSON.stringify(obj); + return crypto.createHash('sha256').update(data).digest('hex'); +} + +/** + * Verify that a file map matches the checksum stored in metadata. + * @param {object|Map} files - Cached file mapping. + * @param {{ checksum: string }} meta - Metadata object containing checksum. + * @returns {boolean} True if checksum matches. + */ +function verifyChecksum(files, meta) { + const obj = files instanceof Map ? Object.fromEntries(files) : files; + return computeChecksum(obj) === meta.checksum; +} + +/** + * @typedef {object} TarballCacheConfig + * @property {string} cacheDir - Directory to store cache files. + * @property {string} metaFile - Path to metadata JSON file. + * @property {string} dataFile - Path to cached data JSON file. + * @property {number} [ttlMs=604800000] - Time-to-live in milliseconds (default: 7 days). + */ + +/** + * Create a TTL-aware, checksum-validated tarball cache handler. + * + * Provides: + * - `save(version, filesMap)` → persist cache + * - `load(version)` → load cache if valid, unexpired, and consistent + * - `clear()` → delete cache directory + * + * @param {TarballCacheConfig} config - Cache configuration. + * @returns {{ + * save: (version: string, filesMap: Map|object) => void, + * load: (version: string) => Map|null, + * clear: () => void + * }} + */ +export function createTarballCache({ + cacheDir, + metaFile, + dataFile, + ttlMs = 7 * 24 * 60 * 60 * 1000, +}) { + /** + * Remove all cache files from the cache directory. + */ + const clear = () => { + fs.rmSync(cacheDir, { recursive: true, force: true }); + logger.info(`🗑️ Cleared cache directory: ${cacheDir}`); + }; + + /** + * Persist current files and metadata to disk. + * @param {string} version - Version identifier. + * @param {Map|object} filesMap - Files to cache. + */ + const save = (version, filesMap) => { + try { + ensureDir(cacheDir); + const files = filesMap instanceof Map ? Object.fromEntries(filesMap) : filesMap || {}; + const checksum = computeChecksum(files); + const meta = { version, checksum, timestamp: Date.now() }; + + saveJson(dataFile, files); + saveJson(metaFile, meta); + logger.info(`💾 Saved cache for v${version} (TTL: ${(ttlMs / 86_400_000).toFixed(1)} days)`); + } catch (err) { + logger.error(`❌ Failed to save cache: ${err.message}`); + } + }; + + /** + * Attempt to load a valid cache from disk. + * @param {string} version - Version identifier. + * @returns {Map|null} Cached files map or null if invalid. + */ + const load = (version) => { + if (!fs.existsSync(metaFile) || !fs.existsSync(dataFile)) return null; + + try { + const meta = loadJson(metaFile); + const invalid = validateMeta(meta, version); + if (invalid) return (logger.warn(`⚠️ Invalid cache meta: ${invalid}`), clear(), null); + + const ttl = validateTTL(meta.timestamp, ttlMs); + if (ttl.expired) return (logger.warn(`⚠️ Cache expired (${ttl.message})`), clear(), null); + + const files = loadJson(dataFile); + + if (!files || typeof files !== 'object') { + return (logger.warn(`⚠️ Corrupted cache data`), clear(), null); + } + + if (!verifyChecksum(files, meta)) { + return (logger.warn(`⚠️ Checksum mismatch`), clear(), null); + } + + const map = new Map(Object.entries(files)); + logger.info(`📦 Using cached v${version} (age ${(ttl.age / 86_400_000).toFixed(1)} days)`); + return map; + } catch (err) { + logger.warn(`⚠️ Failed to load cache: ${err.message}`); + clear(); + return null; + } + }; + + return { save, load, clear }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js new file mode 100644 index 000000000000..8afb79f7481e --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js @@ -0,0 +1,61 @@ +import { Buffer } from 'node:buffer'; +import https from 'node:https'; +import { createGunzip } from 'node:zlib'; +import tar from 'tar-stream'; + +/** + * Stream and extract a remote `.tar.gz` file, calling a handler for each matched file. + * Handles redirects transparently. + * + * @param {string} url - Remote tarball URL. + * @param {(entryPath: string) => boolean} filter - Predicate selecting entries to extract. + * @param {(entryPath: string, content: Buffer) => Promise} onFile - Async handler for file contents. + * @returns {Promise} Resolves when extraction completes. + */ +export async function streamTarGz(url, filter, onFile) { + await new Promise((resolve, reject) => { + https + .get(url, { headers: { 'User-Agent': 'manager-muk-cli' } }, (res) => { + // Handle HTTP redirects + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + res.resume(); + streamTarGz(res.headers.location, filter, onFile).then(resolve).catch(reject); + return; + } + + if (res.statusCode !== 200) { + reject(new Error(`Download failed: ${res.statusCode}`)); + return; + } + + const gunzip = createGunzip(); + const extract = tar.extract(); + + extract.on('entry', (header, stream, next) => { + const entryPath = header.name; + if (header.type === 'file' && filter(entryPath)) { + const chunks = []; + stream.on('data', (c) => chunks.push(c)); + stream.on('end', async () => { + try { + await onFile(entryPath, Buffer.concat(chunks)); + next(); + } catch (e) { + reject(e); + } + }); + } else { + stream.resume(); + stream.on('end', next); + } + }); + + extract.on('finish', resolve); + extract.on('error', reject); + gunzip.on('error', reject); + + res.pipe(gunzip).pipe(extract); + }) + .on('error', reject); + }); +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js new file mode 100644 index 000000000000..94589f7a6d75 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js @@ -0,0 +1,179 @@ +import { execSync } from 'node:child_process'; +import path from 'node:path'; + +import { EMOJIS, MUK_COMPONENTS_PATH } from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * Executes a shell command synchronously and logs progress. + * + * Wraps Node’s `execSync()` to provide: + * - Contextual logging (start/success/error). + * - Project-aware working directory control. + * - Clean CLI output inherited from the child process. + * + * @private + * @param {string} cmd - The command to execute (e.g., `yarn lint:modern:fix`). + * @param {string} cwd - Working directory for the command. + * @param {string} desc - Human-readable description for logs. + */ +function runCommand(cmd, cwd, desc) { + try { + logger.info(`🔧 Running ${desc}...`); + execSync(cmd, { stdio: 'inherit', cwd }); + logger.success(`✅ ${desc} completed successfully.`); + } catch (err) { + logger.error(`❌ ${desc} failed: ${err.message}`); + } +} + +/** + * Run all post-update validation steps. + * + * Used after automated updates (e.g., component documentation sync) + * to ensure the repository remains consistent and buildable. + * + * Steps performed: + * 1. **Install dependencies** from the project root. + * 2. **Run ESLint (modern)** to auto-fix lint issues. + * 3. **Run unit tests** to validate component behavior. + * + * @example + * await runPostUpdateChecks(); + * + * @returns {void} + */ +export function runPostUpdateChecks() { + const componentDir = MUK_COMPONENTS_PATH; + const rootDir = path.resolve('.'); + + runCommand('yarn install', rootDir, 'Dependency installation (root)'); + runCommand('yarn lint:modern:fix', componentDir, 'ESLint (modern mode)'); + runCommand('yarn test', componentDir, 'Unit tests'); +} + +/** + * Creates an asynchronous, iterable queue that enables + * communication between producers and consumers in streaming workflows. + * + * This is a core utility for bridging **callback-based producers** + * (e.g., tarball extraction events) with **`for await...of` consumers** + * (e.g., file writers or loggers). + * + * It ensures: + * - ✅ Backpressure-safe processing (consumer controls flow). + * - ✅ Constant memory footprint (only holds unconsumed items). + * - ✅ Simple API: `push()`, `end()`, and async iteration. + * + * --- + * + * ## Example Usage + * + * ```js + * const queue = createAsyncQueue(); + * + * // Producer + * (async () => { + * for (const item of data) await queue.push(item); + * queue.end(); + * })(); + * + * // Consumer + * for await (const item of queue) { + * console.log('Processing', item); + * } + * ``` + * + * @returns {{ + * push(item: any): Promise, + * end(): void, + * [Symbol.asyncIterator](): AsyncGenerator + * }} + * Queue interface with three operations: + * - `push(item)` → adds a new item. + * - `end()` → signals that no more items will be added. + * - async iteration (`for await`) → consumes items as they arrive. + */ +export function createAsyncQueue() { + const items = []; + let resolve; + let done = false; + + return { + /** + * Push an item into the queue. + * If a consumer is waiting, it resolves immediately. + * @param {*} item - Any data or object to enqueue. + */ + async push(item) { + if (done) return; + if (resolve) { + resolve({ value: item, done: false }); + resolve = null; + } else { + items.push(item); + } + }, + + /** + * Mark the queue as complete. + * Signals to the consumer that iteration should end. + */ + end() { + done = true; + if (resolve) resolve({ value: undefined, done: true }); + }, + + /** + * Async iterator interface implementation. + * Enables `for await...of` consumption. + */ + [Symbol.asyncIterator]() { + return { + next() { + if (items.length) return Promise.resolve({ value: items.shift(), done: false }); + if (done) return Promise.resolve({ value: undefined, done: true }); + return new Promise((res) => (resolve = res)); + }, + }; + }, + }; +} + +/** + * Aggregates statistics from multiple synchronization operations. + * + * @param {Array<{created:number, updated:number, total:number}>} results + * @returns {{created:number, updated:number, total:number}} Combined totals. + */ +export function aggregateOperationsStats(results) { + return results.reduce( + (acc, curr) => ({ + created: acc.created + (curr.created || 0), + updated: acc.updated + (curr.updated || 0), + total: acc.total + (curr.total || 0), + }), + { created: 0, updated: 0, total: 0 }, + ); +} + +/** + * Executes a sync operation safely, with clear contextual logging. + * + * @async + * @param {string} label - Descriptive name for the operation (e.g., "component base-docs"). + * @param {Function} syncFn - The synchronization function to execute. + * @returns {Promise<{created:number, updated:number, total:number}>} + * Returns counts even if the operation fails. + */ +export async function safeSync(label, syncFn) { + try { + logger.info(`${EMOJIS.info} Syncing ${label}...`); + const result = (await syncFn()) || { created: 0, updated: 0, total: 0 }; + logger.info(`${EMOJIS.disk} ${label} → ${result.total} files processed.`); + return result; + } catch (error) { + logger.error(`${EMOJIS.error} Failed to sync ${label}: ${error.message}`); + return { created: 0, updated: 0, total: 0 }; + } +} diff --git a/packages/manager-tools/manager-muk-cli/src/index.js b/packages/manager-tools/manager-muk-cli/src/index.js new file mode 100755 index 000000000000..658a18f29bac --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/index.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import process from 'node:process'; + +import { addComponentsDocumentation } from './commands/add-components-documentation.js'; +import { addComponents } from './commands/add-components.js'; +import { checkComponents } from './commands/check-components.js'; +import { checkVersions } from './commands/check-versions.js'; +import { updateOdsVersions } from './commands/update-versions.js'; +import { logger } from './utils/log-manager.js'; + +const args = process.argv.slice(2); + +async function main() { + if (args.includes('--check-versions')) { + await checkVersions(); + } else if (args.includes('--check-components')) { + await checkComponents(); + } else if (args.includes('--update-versions')) { + await updateOdsVersions(); + } else if (args.includes('--add-components')) { + await addComponents(); + } else if (args.includes('--add-components-documentation')) { + await addComponentsDocumentation(); + } else { + logger.warn( + 'Usage: manager-muk-cli --check-versions | --update-versions | --check-components | --add-components | --add-components-documentation', + ); + } +} + +main().catch((err) => { + logger.error(`CLI error: ${err.message}`); + process.exit(1); +}); diff --git a/packages/manager-tools/manager-muk-cli/src/utils/json-utils.js b/packages/manager-tools/manager-muk-cli/src/utils/json-utils.js new file mode 100644 index 000000000000..a13aa149be66 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/utils/json-utils.js @@ -0,0 +1,86 @@ +import { promises as fs } from 'node:fs'; + +import { logger } from './log-manager.js'; + +/** + * Load and parse a JSON file into a typed object. + * + * @template T - The expected type of the parsed JSON object. + * @param {string} filePath - Absolute or relative path to the JSON file. + * @returns {Promise} A promise resolving to the parsed object, typed as `T`. + * + * @throws Will throw if the file cannot be read or contains invalid JSON. + * + * @example + * ```ts + * interface Config { + * port: number; + * debug: boolean; + * } + * + * const config = await loadJson('./config.json'); + * console.log(config.port); + * ``` + */ +export async function loadJson(filePath) { + logger.debug(`loadJson(file="${filePath}")`); + try { + const raw = await fs.readFile(filePath, 'utf-8'); + const parsed = JSON.parse(raw); + logger.info(`✅ Successfully loaded JSON from ${filePath}`); + logger.debug( + `Sample keys: ${Object.keys(parsed).slice(0, 5).join(', ')}${ + Object.keys(parsed).length > 5 ? ' ...' : '' + }`, + ); + return parsed; + } catch (err) { + logger.error(`❌ Failed to load JSON file ${filePath}: ${err.message}`); + logger.debug(`Stack trace: ${err.stack}`); + throw err; + } +} + +/** + * Safely write a JavaScript object to a JSON file. + * + * - Pretty-prints with 2 spaces. + * - Ensures atomic write: first writes to a temp file, then renames. + * - Logs summary including file size and sample keys. + * + * @template T + * @param {string} filePath - Path to the target JSON file. + * @param {T} data - Serializable object to write. + * @returns {Promise} + * + * @throws Will throw if serialization or file write fails. + * + * @example + * ```ts + * await writeJson('./config.json', { port: 3000, debug: true }); + * ``` + */ +export async function writeJson(filePath, data) { + logger.debug(`writeJson(file="${filePath}")`); + + try { + const serialized = JSON.stringify(data, null, 2); + const tempPath = `${filePath}.tmp`; + + // Write to a temp file first + await fs.writeFile(tempPath, serialized, 'utf8'); + + // Atomically replace the original + await fs.rename(tempPath, filePath); + + logger.info(`✅ JSON written successfully to ${filePath}`); + if (typeof data === 'object' && data !== null) { + const keys = Object.keys(data); + logger.debug(`Sample keys: ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? ' ...' : ''}`); + } + } catch (err) { + logger.error(`❌ Failed to write JSON to ${filePath}: ${err.message}`); + logger.debug(`Stack trace: ${err.stack}`); + throw err; + } +} diff --git a/packages/manager-tools/manager-muk-cli/src/utils/log-manager.js b/packages/manager-tools/manager-muk-cli/src/utils/log-manager.js new file mode 100644 index 000000000000..ce9b8900ad37 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/utils/log-manager.js @@ -0,0 +1,153 @@ +/* eslint-disable no-undef */ + +/** + * ANSI color escape codes for styled console output. + */ +const COLORS = { + reset: '\x1b[0m', + gray: '\x1b[90m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', +}; + +/** + * Symbols used for different log levels. + */ +const SYMBOLS = { + info: 'ℹ', + success: '✔', + warn: '⚠', + error: '✖', + debug: '•', +}; + +/** + * Supported logger modes. + * + * - `"default"`: Logs go to stdout/stderr as usual + * - `"stderr"`: All logs (info/success/warn/debug) are redirected to stderr + * - `"silent"`: Suppress **all** logs (info/success/warn/debug/error) + * + * @typedef {"default"|"stderr"|"silent"} LoggerMode + */ + +/** @type {LoggerMode} */ +let mode = 'stderr'; // default: stderr, safe for jq pipes + +/** + * Change the logging mode at runtime. + * + * Example: + * ```js + * setLoggerMode("silent"); // suppress all logs + * setLoggerMode("default"); // stdout/stderr normal + * setLoggerMode("stderr"); // stderr-only (default) + * ``` + * + * @param {LoggerMode} newMode - New mode ("default" | "stderr" | "silent"). + */ +export function setLoggerMode(newMode) { + mode = newMode; +} + +/** + * Get the current logger mode. + * + * @returns {LoggerMode} The current logger mode. + */ +export function getLoggerMode() { + return mode; +} + +/** + * Format a log message with color, symbol, and optional extra arguments. + * + * @param {string} color - ANSI escape code for the color to apply. + * @param {string} symbol - Symbol prefix for the log (e.g., ✔, ℹ, ⚠, ✖). + * @param {string} msg - Main log message. + * @param {any[]} args - Additional values to append after the message. + * @returns {string} A formatted string with colors and symbol. + */ +function formatMessage(color, symbol, msg, args) { + const extra = args.length ? ' ' + args.map(String).join(' ') : ''; + return `${color}${symbol} ${msg}${extra}${COLORS.reset}`; +} + +/** + * Dispatch a log message depending on the current logger mode. + * + * @param {(msg: string) => void} stream - Console stream function (console.log, console.error, etc.). + * @param {string} color - ANSI color code to apply. + * @param {string} symbol - Symbol prefix for the log. + * @param {string} msg - Main log message. + * @param {any[]} args - Additional arguments to append. + */ +// eslint-disable-next-line max-params +function output(stream, color, symbol, msg, args) { + if (mode === 'silent') return; // disable everything + const formatted = formatMessage(color, symbol, msg, args); + if (mode === 'stderr') { + console.error(formatted); + } else { + stream(formatted); + } +} + +/** + * Minimal, dependency-free logger with colored output and symbols. + * Honors the current mode set via {@link setLoggerMode}. + */ +export const logger = { + /** + * Log an informational message (blue ℹ). + * + * @param {string} msg - The main log message. + * @param {...any} args - Additional values to log. + */ + info(msg, ...args) { + output(console.log, COLORS.blue, SYMBOLS.info, msg, args); + }, + + /** + * Log a success message (green ✔). + * + * @param {string} msg - The main log message. + * @param {...any} args - Additional values to log. + */ + success(msg, ...args) { + output(console.log, COLORS.green, SYMBOLS.success, msg, args); + }, + + /** + * Log a warning message (yellow ⚠). + * + * @param {string} msg - The main log message. + * @param {...any} args - Additional values to log. + */ + warn(msg, ...args) { + output(console.warn, COLORS.yellow, SYMBOLS.warn, msg, args); + }, + + /** + * Log an error message (red ✖). + * Suppressed if mode is `"silent"`. + * + * @param {string} msg - The main log message. + * @param {...any} args - Additional values to log. + */ + error(msg, ...args) { + output(console.error, COLORS.red, SYMBOLS.error, msg, args); + }, + + /** + * Log a debug message (gray •). + * + * @param {string} msg - The main log message. + * @param {...any} args - Additional values to log. + */ + debug(msg, ...args) { + output(console.debug, COLORS.gray, SYMBOLS.debug, msg, args); + }, +}; diff --git a/packages/manager-tools/manager-vite-config/src/config.js b/packages/manager-tools/manager-vite-config/src/config.js index e7cdf5fe5dbb..7b684c38e8f1 100644 --- a/packages/manager-tools/manager-vite-config/src/config.js +++ b/packages/manager-tools/manager-vite-config/src/config.js @@ -18,7 +18,9 @@ const getBaseConfig = (config) => { if (envConfig.isLABEU || process.env.LABEU) { const labeuHost = process.env.LABEU_HOST; if (!labeuHost) { - throw new Error('Please define the environment variable "LABEU_HOST=host" to use LABEU env'); + throw new Error( + `'Please define the environment variable "LABEU_HOST=host" to use LABEU env'`, + ); } envConfig.host = labeuHost; } @@ -52,7 +54,7 @@ const getBaseConfig = (config) => { '@vitest', 'typescript', 'date-fns', - '@ovh-ux/manager-react-components', + '@ovh-ux/muk', ], }, define: { @@ -73,7 +75,7 @@ const getBaseConfig = (config) => { preprocessorOptions: { scss: { includePaths: [ - resolve(dirname(fileURLToPath(import.meta.url)), '../../../../../node_modules'), + resolve(`${dirname(fileURLToPath(import.meta.url))}`, '../../../../../node_modules'), ], }, }, diff --git a/packages/manager-react-components/.gitignore b/packages/manager-ui-kit/.gitignore similarity index 100% rename from packages/manager-react-components/.gitignore rename to packages/manager-ui-kit/.gitignore diff --git a/packages/manager-react-components/.storybook/i18n.ts b/packages/manager-ui-kit/.storybook/i18n.ts similarity index 100% rename from packages/manager-react-components/.storybook/i18n.ts rename to packages/manager-ui-kit/.storybook/i18n.ts diff --git a/packages/manager-react-components/.storybook/vite.config.ts b/packages/manager-ui-kit/.storybook/vite.config.ts similarity index 100% rename from packages/manager-react-components/.storybook/vite.config.ts rename to packages/manager-ui-kit/.storybook/vite.config.ts diff --git a/packages/manager-react-components/CHANGELOG.md b/packages/manager-ui-kit/CHANGELOG.md similarity index 100% rename from packages/manager-react-components/CHANGELOG.md rename to packages/manager-ui-kit/CHANGELOG.md diff --git a/packages/manager-react-components/README.md b/packages/manager-ui-kit/README.md similarity index 100% rename from packages/manager-react-components/README.md rename to packages/manager-ui-kit/README.md diff --git a/packages/manager-ui-kit/eslint.config.mjs b/packages/manager-ui-kit/eslint.config.mjs new file mode 100644 index 000000000000..832e129ac8f6 --- /dev/null +++ b/packages/manager-ui-kit/eslint.config.mjs @@ -0,0 +1,71 @@ +// Full adoption +/* import { eslintSharedConfig } from '@ovh-ux/manager-static-analysis-kit'; + +export default eslintSharedConfig; +*/ + +// Progressive adoption +/* import { javascriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/javascript'; +import { typescriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/typescript'; +import { reactEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/react'; +import { prettierEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/prettier'; +import { a11yEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/a11y'; +import { + complexityJsxTsxConfig, + complexityTsJsConfig, +} from '@ovh-ux/manager-static-analysis-kit/eslint/complexity'; +import { htmlEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/html'; +import { cssEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/css'; +import { tailwindJsxConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tailwind-jsx'; +import { tanStackQueryEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tanstack'; +import { importEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/imports'; +import { checkFileEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/naming-conventions'; +import { vitestEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/tests'; +import { storybookEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/storybook'; + +export default [ + javascriptEslintConfig, + typescriptEslintConfig, + reactEslintConfig, + a11yEslintConfig, + htmlEslintConfig, + tailwindJsxConfig, + tanStackQueryEslintConfig, + ...importEslintConfig, + ...checkFileEslintConfig, + vitestEslintConfig, + prettierEslintConfig, + complexityJsxTsxConfig, + complexityTsJsConfig, + { + ...cssEslintConfig, + files: ['**\/*.css', '**\/*.scss'], + }, + ...storybookEslintConfig, +]; */ + +// Progressive and disable some rules +/* import { typescriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/typescript'; + +export default [ + { + ...typescriptEslintConfig, + rules: { + ...typescriptEslintConfig.rules, + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/await-thenable': 'off' + }, + }, +]; */ + +// Progressive and disable full rules +import { typescriptEslintConfig } from '@ovh-ux/manager-static-analysis-kit/eslint/typescript'; + +export default [ + { + ...typescriptEslintConfig, + rules: {}, + }, +]; diff --git a/packages/manager-react-components/global.d.ts b/packages/manager-ui-kit/global.d.ts similarity index 100% rename from packages/manager-react-components/global.d.ts rename to packages/manager-ui-kit/global.d.ts diff --git a/packages/manager-ui-kit/package.json b/packages/manager-ui-kit/package.json new file mode 100644 index 000000000000..3eeef2a46cfa --- /dev/null +++ b/packages/manager-ui-kit/package.json @@ -0,0 +1,117 @@ +{ + "name": "@ovh-ux/muk", + "version": "0.0.1", + "license": "BSD-3-Clause", + "author": "OVH SAS", + "description": "MUK:Manager UI Kit", + "types": "dist/types/src/lib.d.ts", + "main": "dist/src/lib.js", + "browser": "dist/manager-ui-kit-lib.es.ts", + "homepage": "https://github.com/ovh/manager/blob/master/packages/manager-ui-kit/README.md", + "bugs": { + "url": "https://github.com/ovh/manager/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ovh/manager.git", + "directory": "packages/manager-ui-kit" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && vite build", + "build:strict": "tsc --project tsconfig.strict.json && vite build", + "lint:modern": "manager-lint --config eslint.config.mjs ./src", + "lint:modern:fix": "manager-lint --fix --config eslint.config.mjs ./src", + "prepare": "tsc && vite build", + "test": "TZ=UTC manager-test run", + "test:ci": "TZ=UTC manager-test run --coverage" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx,json,css,md}": [ + "prettier -w" + ] + }, + "dependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@tanstack/react-query": "^5.51.21", + "@tanstack/react-table": "^8.20.1", + "@tanstack/react-virtual": "^3.10.9", + "clsx": "^2.1.1", + "dompurify": "^3.2.6", + "lodash.isdate": "^4.0.1", + "lodash.isequal": "^4.5.0", + "react-i18next": "^14.0.5", + "react-use": "^17.5.0", + "sass": "1.56.1", + "tailwindcss": "^3.4.4", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@babel/core": "7.22.10", + "@mdx-js/react": "^3.0.1", + "@ovh-ux/manager-common-translations": "^0.19.2", + "@ovh-ux/manager-core-api": "^0.18.6", + "@ovh-ux/manager-core-utils": "^0.4.4", + "@ovh-ux/manager-react-shell-client": "^0.9.3", + "@ovh-ux/manager-tailwind-config": "^0.5.4", + "@ovh-ux/manager-vite-config": "^0.13.3", + "@ovh-ux/manager-static-analysis-kit": "*", + "@ovh-ux/manager-tests-setup": "^0.2.0", + "@ovhcloud/ods-react": "^19.0.1", + "@ovhcloud/ods-themes": "^19.0.1", + "@types/lodash.isdate": "^4.0.9", + "@types/lodash.isequal": "^4.5.0", + "@types/node": "^24.0.4", + "@types/react": "18.2.45", + "@types/react-dom": "18.2.7", + "autoprefixer": "10.4.14", + "axios-mock-adapter": "2.1.0", + "babel-loader": "9.1.3", + "babel-preset-react-app": "^10.0.1", + "date-fns": "~4.1.0", + "element-internals-polyfill": "^3.0.2", + "eslint": "^9.29.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-n": "^17.16.2", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-react": "^7.37.5", + "esrecurse": "^4.3.0", + "file-loader": "^6.2.0", + "husky": "8.0.3", + "i18next": "^23.8.2", + "jsdom": "26.0.0", + "json": "11.0.0", + "lint-staged": "13.2.3", + "msw": "2.3.1", + "postcss": "8.4.31", + "prettier": "^3.6.2", + "prop-types": "15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.5", + "react-router-dom": "^6.3.0", + "undici": "5.29.0", + "vite": "^6.0.7", + "vitest": "^2.1.9", + "vite-plugin-dts": "^4.5.3", + "vite-plugin-static-copy": "^2.3.0", + "zustand": "^4.5.5" + }, + "peerDependencies": { + "@ovh-ux/manager-common-translations": "*", + "@ovh-ux/manager-core-api": "^0.10.0", + "@ovh-ux/manager-core-utils": "*", + "@ovh-ux/manager-react-shell-client": "^0.9.1", + "@ovhcloud/ods-react": "^19.1.0", + "@ovhcloud/ods-themes": "^19.0.1", + "date-fns": "~4.1.0", + "i18next": "^23.8.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.3.0", + "zustand": "^4.5.5" + } +} diff --git a/packages/manager-react-components/postcss.config.js b/packages/manager-ui-kit/postcss.config.js similarity index 100% rename from packages/manager-react-components/postcss.config.js rename to packages/manager-ui-kit/postcss.config.js diff --git a/packages/manager-react-components/public/assets/css/storybook.css b/packages/manager-ui-kit/public/assets/css/storybook.css similarity index 100% rename from packages/manager-react-components/public/assets/css/storybook.css rename to packages/manager-ui-kit/public/assets/css/storybook.css diff --git a/packages/manager-react-components/public/assets/error-banner-oops.png b/packages/manager-ui-kit/public/assets/error-banner-oops.png similarity index 100% rename from packages/manager-react-components/public/assets/error-banner-oops.png rename to packages/manager-ui-kit/public/assets/error-banner-oops.png diff --git a/packages/manager-react-components/public/assets/img/favicon.ico b/packages/manager-ui-kit/public/assets/img/favicon.ico similarity index 100% rename from packages/manager-react-components/public/assets/img/favicon.ico rename to packages/manager-ui-kit/public/assets/img/favicon.ico diff --git a/packages/manager-react-components/public/assets/img/favicon.png b/packages/manager-ui-kit/public/assets/img/favicon.png similarity index 100% rename from packages/manager-react-components/public/assets/img/favicon.png rename to packages/manager-ui-kit/public/assets/img/favicon.png diff --git a/packages/manager-react-components/public/assets/placeholder.png b/packages/manager-ui-kit/public/assets/placeholder.png similarity index 100% rename from packages/manager-react-components/public/assets/placeholder.png rename to packages/manager-ui-kit/public/assets/placeholder.png diff --git a/packages/manager-ui-kit/setupTest.tsx b/packages/manager-ui-kit/setupTest.tsx new file mode 100644 index 000000000000..c1d54e2fc8f3 --- /dev/null +++ b/packages/manager-ui-kit/setupTest.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; +import { ComponentType } from 'react'; +import { I18nextProvider, initReactI18next } from 'react-i18next'; +import i18n from 'i18next'; +import { + RenderOptions, + RenderResult, + render, + cleanup, +} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import 'element-internals-polyfill'; +import { vi } from 'vitest'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import ActionMenuTransFR from './src/components/action-menu/translations/Messages_fr_FR.json'; +import DatagridTransFR from './src/components/datagrid/translations/Messages_fr_FR.json'; +import FiltersTransFR from './src/components/filters/translations/Messages_fr_FR.json'; + +i18n.use(initReactI18next).init({ + fallbackLng: 'fr_FR', + interpolation: { + escapeValue: false, + }, + resources: { + fr: { + 'action-menu': { + ...ActionMenuTransFR, + }, + datagrid: { + ...DatagridTransFR, + }, + filterAdd: { + ...FiltersTransFR, + }, + }, + }, +}); + +export const queryClient = new QueryClient({ + defaultOptions: { + mutations: { + retry: false, + }, + queries: { + retry: false, + }, + }, +}); + +const Wrappers = ({ children }: { children: React.ReactElement }) => { + return ( + + {children} + + ); +}; + +const customRender = ( + ui: React.JSX.Element, + options?: Omit, +): RenderResult => + render(ui, { wrapper: Wrappers as ComponentType, ...options }); + +export { customRender as render, cleanup }; + +// This polyfill exists because of an issue with jsdom and the EventTarget class +// when testing a component with an OdsDatepicker (addEventListener crashes at component initialization). +// Fix from issue: https://github.com/jsdom/jsdom/issues/2156 +global.EventTarget = class { + listeners = {}; + + addEventListener(type, listener) { + this.listeners = this.listeners || {}; + (this.listeners[type] || (this.listeners[type] = new Set())).add(listener); + } + + removeEventListener(type, listener) { + if (this.listeners && this.listeners[type]) { + this.listeners[type].delete(listener); + } + } + + dispatchEvent(event) { + this.listeners[event.type].forEach((listener) => listener(event)); + return !event.defaultPrevented; + } +}; + +const ResizeObserverMock = vi.fn((callback) => { + // Create a mock ResizeObserverEntry with the expected structure + const mockEntry = { + target: document.createElement('div'), + contentRect: { + width: 100, + height: 100, + top: 0, + left: 0, + bottom: 100, + right: 100, + x: 0, + y: 0, + }, + borderBoxSize: [{ width: 100, height: 100 }], + contentBoxSize: [{ width: 100, height: 100 }], + devicePixelContentBoxSize: [{ width: 100, height: 100 }], + }; + + // Call the callback with an array of entries as the real ResizeObserver does + callback([mockEntry]); + + return { + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + }; +}); + +vi.stubGlobal('ResizeObserver', ResizeObserverMock); diff --git a/packages/manager-ui-kit/src/__mocks__/@tanstack/react-virtual.ts b/packages/manager-ui-kit/src/__mocks__/@tanstack/react-virtual.ts new file mode 100644 index 000000000000..0deb657ce947 --- /dev/null +++ b/packages/manager-ui-kit/src/__mocks__/@tanstack/react-virtual.ts @@ -0,0 +1,19 @@ +import { vi } from 'vitest'; + +export const useVirtualizer = vi.fn((options) => { + const { count, estimateSize } = options; + const virtualItems = Array.from({ length: count }, (_, index) => ({ + index, + start: index * (estimateSize?.() || 50), + size: estimateSize?.() || 50, + end: (index + 1) * (estimateSize?.() || 50), + key: index, + })); + + return { + getVirtualItems: () => virtualItems, + getTotalSize: () => count * (estimateSize?.() || 50), + scrollToIndex: vi.fn(), + measure: vi.fn(), + }; +}); diff --git a/packages/manager-react-components/src/__mocks__/images.tsx b/packages/manager-ui-kit/src/__mocks__/images.tsx similarity index 100% rename from packages/manager-react-components/src/__mocks__/images.tsx rename to packages/manager-ui-kit/src/__mocks__/images.tsx diff --git a/packages/manager-react-components/src/__mocks__/stepper.ts b/packages/manager-ui-kit/src/__mocks__/stepper.ts similarity index 100% rename from packages/manager-react-components/src/__mocks__/stepper.ts rename to packages/manager-ui-kit/src/__mocks__/stepper.ts diff --git a/packages/manager-react-components/src/__mocks__/tiles-input.ts b/packages/manager-ui-kit/src/__mocks__/tiles-input.ts similarity index 100% rename from packages/manager-react-components/src/__mocks__/tiles-input.ts rename to packages/manager-ui-kit/src/__mocks__/tiles-input.ts diff --git a/packages/manager-ui-kit/src/components/Link/Link.component.tsx b/packages/manager-ui-kit/src/components/Link/Link.component.tsx new file mode 100644 index 000000000000..633fda5c092a --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/Link.component.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { + Link as OdsLink, + Icon, + Tooltip, + TooltipTrigger, + TooltipContent, + TOOLTIP_POSITION, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { LinkType, LinkProps, LinkIconsProps } from './Link.props'; +import { useAuthorizationIam } from '../../hooks'; + +const LinkIcons: React.FC = ({ type, children }) => ( + <> + {type === LinkType.back && } + {children} + {type === LinkType.external && } + {type === LinkType.next && } + +); + +export const Link: React.FC = ({ + children, + type, + href, + className = '', + iamActions, + urn, + disableIamCheck = false, + displayTooltip = true, + isIamTrigger = true, + ...props +}) => { + const { t } = useTranslation(NAMESPACES.IAM); + const { isAuthorized } = useAuthorizationIam( + iamActions || [], + urn || '', + !disableIamCheck, + ); + + const getLinkProps = (isDisabled = false) => ({ + className, + href, + disabled: isDisabled, + ...props, + }); + + if (isAuthorized || iamActions === undefined) { + return ( + + {children} + + ); + } + + if (!displayTooltip) { + return ( + + {children} + + ); + } + + return ( + + + + {children} + + + {t('iam_actions_message')} + + ); +}; + +export default Link; diff --git a/packages/manager-ui-kit/src/components/Link/Link.props.ts b/packages/manager-ui-kit/src/components/Link/Link.props.ts new file mode 100644 index 000000000000..8a184843b0ce --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/Link.props.ts @@ -0,0 +1,30 @@ +import React, { DOMAttributes } from 'react'; +import { LinkProp } from '@ovhcloud/ods-react'; + +export enum LinkType { + back = 'back', + next = 'next', + external = 'external', +} + +export interface LinkProps extends LinkProp, DOMAttributes { + className?: string; + download?: string; + label?: string; + children?: string | JSX.Element; + href?: string; + rel?: string; + target?: string; + type?: LinkType; + // Iam trigger + iamActions?: string[]; + urn?: string; + displayTooltip?: boolean; + isIamTrigger?: boolean; + disableIamCheck?: boolean; +} + +export interface LinkIconsProps { + type?: LinkType; + children: React.ReactNode; +} diff --git a/packages/manager-ui-kit/src/components/Link/__tests__/Link.snapshot.test.tsx b/packages/manager-ui-kit/src/components/Link/__tests__/Link.snapshot.test.tsx new file mode 100644 index 000000000000..8b41542a5279 --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/__tests__/Link.snapshot.test.tsx @@ -0,0 +1,120 @@ +import { Link } from '../Link.component'; +import { LinkType } from '../Link.props'; +import { render } from '@/setupTest'; + +describe('Link component', () => { + it('renders a simple link correctly', () => { + const props = { + children: 'test link', + href: 'https://www.example.com', + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a back link correctly', () => { + const props = { + children: 'Back to the list', + href: 'https://www.example.com', + type: LinkType.back, + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a next link correctly', () => { + const props = { + children: 'Next Page', + href: 'https://www.example.com', + type: LinkType.next, + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a external link correctly with label as prop', () => { + const props = { + href: 'https://www.ovhcloud.com/', + target: '_blank', + children: 'External Page', + type: LinkType.external, + }; + + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a link with urn and iamActions correctly', () => { + const props = { + children: 'Protected Link', + href: 'https://www.example.com', + urn: 'urn:v1:eu:resource:unknown:unknown-id', + iamActions: ['unknown:apiovh:resource/edit'], + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a disabled link with urn and iamActions correctly', () => { + const props = { + children: 'Disabled Protected Link', + href: 'https://www.example.com', + urn: 'urn:v1:eu:resource:test:test-resource-id', + iamActions: ['test:apiovh:resource/edit'], + disabled: true, + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a link with urn and iamActions without tooltip', () => { + const props = { + children: 'Protected Link No Tooltip', + href: 'https://www.example.com', + urn: 'urn:v1:eu:resource:demo:demo-resource-id', + iamActions: ['demo:apiovh:resource/edit'], + displayTooltip: false, + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a link with disabled IAM check', () => { + const props = { + children: 'Link with Disabled IAM Check', + href: 'https://www.example.com', + urn: 'urn:v1:eu:resource:mock:mock-resource-id', + iamActions: ['mock:apiovh:resource/edit'], + disableIamCheck: true, + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a link with multiple iamActions', () => { + const props = { + children: 'Link with Multiple IAM Actions', + href: 'https://www.example.com', + urn: 'urn:v1:eu:resource:example:example-resource-id', + iamActions: [ + 'example:apiovh:resource/edit', + 'example:apiovh:resource/delete', + 'example:apiovh:resource/view', + ], + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('renders a link with custom className and IAM props', () => { + const props = { + children: 'Custom Styled Protected Link', + href: 'https://www.example.com', + className: 'custom-link-class', + urn: 'urn:v1:eu:resource:generic:generic-resource-id', + iamActions: ['generic:apiovh:resource/edit'], + }; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/Link/__tests__/Link.spec.tsx b/packages/manager-ui-kit/src/components/Link/__tests__/Link.spec.tsx new file mode 100644 index 000000000000..5df5a079a59c --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/__tests__/Link.spec.tsx @@ -0,0 +1,127 @@ +import AxiosMockAdapter from 'axios-mock-adapter'; +import { screen, waitFor } from '@testing-library/react'; +import apiClient from '@ovh-ux/manager-core-api'; +import { Link } from '../Link.component'; +import { LinkType } from '../Link.props'; +import { render } from '@/setupTest'; + +const PROPS_LINK = { + children: 'Link', + href: 'https://www.example.com', +}; + +describe('Link component', () => { + it('renders a simple link correctly', () => { + const props = { + children: 'test link', + href: 'https://www.example.com', + }; + render(); + const linkElement = screen.getByText('test link'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', 'https://www.example.com'); + }); + + it('renders a back link correctly', () => { + const props = { + children: 'Back to the list', + href: 'https://www.example.com', + type: LinkType.back, + }; + render(); + const linkElement = screen.getByText('Back to the list'); + expect(linkElement).toBeInTheDocument(); + + const iconElement = linkElement + .closest('a') + ?.querySelector('[class*="arrow-left"]'); + expect(iconElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', 'https://www.example.com'); + }); + + it('renders a next link correctly', () => { + const props = { + children: 'Next Page', + href: 'https://www.example.com', + type: LinkType.next, + }; + render(); + const linkElement = screen.getByText('Next Page'); + expect(linkElement).toBeInTheDocument(); + + const iconElement = linkElement + .closest('a') + ?.querySelector('[class*="arrow-right"]'); + expect(iconElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', 'https://www.example.com'); + }); + + it('renders a external link correctly with label as prop', () => { + const props = { + href: 'https://www.ovhcloud.com/', + target: '_blank', + children: 'External Page', + type: LinkType.external, + }; + + render(); + const linkElement = screen.getByText('External Page'); + expect(linkElement).toBeInTheDocument(); + + const iconElement = linkElement + .closest('a') + ?.querySelector('[class*="external-link"]'); + expect(iconElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', 'https://www.ovhcloud.com/'); + }); + + const mockAdapter = new AxiosMockAdapter(apiClient.v2); + + it('not renders a link when we have not the autorization', () => { + mockAdapter.onPost('/iam/resource/test/authorization/check').reply(200, []); + + render(); + + const link = screen.getByText('Link'); + expect(link).toBeInTheDocument(); + + waitFor(() => { + expect(link).toBeDisabled(); + }); + }); + + it('renders a link when we have the autorization', () => { + mockAdapter + .onPost('/iam/resource/test/authorization/check') + .reply(200, { authorizedActions: ['subscribe'] }); + + render(); + const linkElement = screen.getByText('Link'); + expect(linkElement).toBeInTheDocument(); + + waitFor(() => { + expect(linkElement).not.toBeDisabled(); + }); + }); + + it('renders a disabled link when we have the autorization but forced disabled', () => { + mockAdapter + .onPost('/iam/resource/test/authorization/check') + .reply(200, { authorizedActions: ['subscribe'] }); + + render( + , + ); + const linkElement = screen.getByText('Link'); + expect(linkElement).toBeInTheDocument(); + + waitFor(() => { + expect(linkElement).toBeDisabled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/Link/__tests__/__snapshots__/Link.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/Link/__tests__/__snapshots__/Link.snapshot.test.tsx.snap new file mode 100644 index 000000000000..f54d8d4e8f61 --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/__tests__/__snapshots__/Link.snapshot.test.tsx.snap @@ -0,0 +1,152 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Link component > renders a back link correctly 1`] = ` + + + + Back to the list + + +`; + +exports[`Link component > renders a disabled link with urn and iamActions correctly 1`] = ` + + + Disabled Protected Link + + +`; + +exports[`Link component > renders a external link correctly with label as prop 1`] = ` + + + External Page + + + +`; + +exports[`Link component > renders a link with custom className and IAM props 1`] = ` + + + Custom Styled Protected Link + + +`; + +exports[`Link component > renders a link with disabled IAM check 1`] = ` + + + Link with Disabled IAM Check + + +`; + +exports[`Link component > renders a link with multiple iamActions 1`] = ` + + + Link with Multiple IAM Actions + + +`; + +exports[`Link component > renders a link with urn and iamActions correctly 1`] = ` + + + Protected Link + + +`; + +exports[`Link component > renders a link with urn and iamActions without tooltip 1`] = ` + + + Protected Link No Tooltip + + +`; + +exports[`Link component > renders a next link correctly 1`] = ` + + + Next Page + + + +`; + +exports[`Link component > renders a simple link correctly 1`] = ` + + + test link + + +`; diff --git a/packages/manager-ui-kit/src/components/Link/index.ts b/packages/manager-ui-kit/src/components/Link/index.ts new file mode 100644 index 000000000000..8e2734b38e4e --- /dev/null +++ b/packages/manager-ui-kit/src/components/Link/index.ts @@ -0,0 +1,3 @@ +export { Link } from './Link.component'; +export type { LinkProps } from './Link.props'; +export { LinkType } from './Link.props'; diff --git a/packages/manager-ui-kit/src/components/action-banner/ActionBanner.component.tsx b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.component.tsx new file mode 100644 index 000000000000..236c62fa702f --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.component.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import DOMPurify from 'dompurify'; +import { Button, Link, Message, Text, BUTTON_SIZE } from '@ovhcloud/ods-react'; +import { ActionBannerProps } from './ActionBanner.props'; + +export function ActionBanner({ + message, + label, + color, + onClick, + href, + variant, + dismissible = false, +}: Readonly) { + return ( + +
+ + + + {href && ( + + {label} + + )} + {onClick && !href && ( + + )} +
+
+ ); +} diff --git a/packages/manager-ui-kit/src/components/action-banner/ActionBanner.props.ts b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.props.ts new file mode 100644 index 000000000000..d78029b02743 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.props.ts @@ -0,0 +1,12 @@ +import { MESSAGE_VARIANT, MESSAGE_COLOR } from '@ovhcloud/ods-react'; + +export type ActionBannerProps = { + message: string; + label?: string; + variant?: MESSAGE_VARIANT; + color?: MESSAGE_COLOR; + onClick?: () => void; + href?: string; + className?: string; + dismissible?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/action-banner/ActionBanner.scss b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.scss new file mode 100644 index 000000000000..755885b6e8fa --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/ActionBanner.scss @@ -0,0 +1,23 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.action-banner.action-banner—-success .action-banner__text { + color: var(--ods-color-success-700); +} + +.action-banner.action-banner—-warning .action-banner__text { + color: var(--ods-color-warning-700); +} + +.action-banner.action-banner—-information .action-banner__text { + color: var(--ods-color-information-700); +} + +.action-banner.action-banner—-danger .action-banner__text { + color: var(--ods-color-critical-700); +} + +.action-banner.action-banner—-critical .action-banner__text { + color: var(--ods-color-critical-700); +} diff --git a/packages/manager-ui-kit/src/components/action-banner/__tests__/ActionBanner.spec.tsx b/packages/manager-ui-kit/src/components/action-banner/__tests__/ActionBanner.spec.tsx new file mode 100644 index 000000000000..def5fb9db679 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/__tests__/ActionBanner.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { fireEvent, screen, render, act } from '@testing-library/react'; +import { ActionBannerProps } from '../ActionBanner.props'; +import { ActionBanner } from '../ActionBanner.component'; + +const renderComponent = (props: ActionBannerProps) => + render(); + +describe('ActionBanner tests', () => { + it('should display message', () => { + renderComponent({ + message: 'hello world', + label: 'custom action', + onClick: () => {}, + dismissible: true, + }); + + expect(screen.getAllByText('hello world')).not.toBeNull(); + }); + + it('should have a working call to action button', () => { + const mockOnClick = vitest.fn(); + + renderComponent({ + message: 'hello world', + label: 'custom action', + onClick: mockOnClick, + }); + expect(screen.getByTestId('action-banner-button')).not.toBeNull(); + const cta = screen.queryByTestId('action-banner-button'); + expect(mockOnClick).not.toHaveBeenCalled(); + act(() => fireEvent.click(cta)); + expect(mockOnClick).toHaveBeenCalled(); + }); + + it('should have a link action', () => { + const href = 'www.ovhcloud.com'; + renderComponent({ + message: 'hello world', + label: 'custom action', + href, + }); + const link = screen.queryByTestId('action-banner-link'); + expect(link).toBeDefined(); + expect(link.getAttribute('href')).toBe(href); + expect(link.getAttribute('target')).toBe('_blank'); + }); +}); diff --git a/packages/manager-ui-kit/src/components/action-banner/__tests__/__snapshots__/ActionBanner.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/action-banner/__tests__/__snapshots__/ActionBanner.snapshot.test.tsx.snap new file mode 100644 index 000000000000..cc0d2d000fbb --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/__tests__/__snapshots__/ActionBanner.snapshot.test.tsx.snap @@ -0,0 +1,206 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ActionBanner Snapshot Tests > Displays ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays ActionBanner with Button 1`] = ` +
+
+
+

+ + Test Message + +

+ +
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays ActionBanner with Link 1`] = ` +
+
+
+

+ + Test Message + +

+ + Link Label + +
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays Critical ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays Information ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays Neutral ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays Primary ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays Warning ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; + +exports[`ActionBanner Snapshot Tests > Displays success ActionBanner 1`] = ` +
+
+
+

+ + Test Message + +

+
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/action-banner/index.ts b/packages/manager-ui-kit/src/components/action-banner/index.ts new file mode 100644 index 000000000000..0c79d2188cb5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-banner/index.ts @@ -0,0 +1,3 @@ +export { ActionBanner } from './ActionBanner.component'; + +export type { ActionBannerProps } from './ActionBanner.props'; diff --git a/packages/manager-ui-kit/src/components/action-menu/ActionMenu.component.tsx b/packages/manager-ui-kit/src/components/action-menu/ActionMenu.component.tsx new file mode 100644 index 000000000000..7e5daa0b0e6b --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/ActionMenu.component.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { + Button, + Popover, + PopoverTrigger, + PopoverContent, + POPOVER_POSITION, + BUTTON_VARIANT, + BUTTON_SIZE, + Icon, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import './translations/translation'; + +import { MenuItem } from './menu-item'; +import { ActionMenuProps } from './ActionMenu.props'; +import './action-menu.scss'; + +export const ActionMenu: React.FC = ({ + items, + isCompact = false, + icon, + variant = BUTTON_VARIANT.outline, + isDisabled = false, + isLoading = false, + displayIcon = true, + popoverPosition = POPOVER_POSITION.bottom, + label, + size = BUTTON_SIZE.sm, +}) => { + const { t } = useTranslation('action-menu'); + const [isTrigger, setIsTrigger] = React.useState(false); + + return ( + + + + + +
    + {items.map(({ id, ...item }) => ( +
  • + +
  • + ))} +
+
+
+ ); +}; + +export default ActionMenu; diff --git a/packages/manager-ui-kit/src/components/action-menu/ActionMenu.props.ts b/packages/manager-ui-kit/src/components/action-menu/ActionMenu.props.ts new file mode 100644 index 000000000000..bfaa5e6e69c2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/ActionMenu.props.ts @@ -0,0 +1,41 @@ +import { + POPOVER_POSITION, + BUTTON_VARIANT, + BUTTON_COLOR, + BUTTON_SIZE, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import { LinkType } from '../Link'; + +export interface ActionMenuItem { + id: number; + rel?: string; + href?: string; + download?: string; + target?: string; + onClick?: () => void; + label: string; + variant?: BUTTON_VARIANT; + iamActions?: string[]; + urn?: string; + className?: string; + linktype?: LinkType; + isDisabled?: boolean; + isLoading?: boolean; + color?: BUTTON_COLOR; + 'data-testid'?: string; +} + +export interface ActionMenuProps { + items: ActionMenuItem[]; + isCompact?: boolean; + icon?: ICON_NAME; + variant?: BUTTON_VARIANT; + displayIcon?: boolean; + id: string; + isDisabled?: boolean; + isLoading?: boolean; + popoverPosition?: POPOVER_POSITION; + label?: string; + size?: BUTTON_SIZE; +} diff --git a/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.snapshot.test.tsx b/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.snapshot.test.tsx new file mode 100644 index 000000000000..7c2621f17555 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.snapshot.test.tsx @@ -0,0 +1,511 @@ +import { + describe, + expect, + it, + vi, + beforeEach, + type MockInstance, +} from 'vitest'; +import { act, screen, fireEvent } from '@testing-library/react'; +import { + POPOVER_POSITION, + ICON_NAME, + BUTTON_VARIANT, +} from '@ovhcloud/ods-react'; +import { ActionMenu } from '../index'; +import { render } from '@/setupTest'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +// Mock the IAM hook +vi.mock('../../../hooks/iam'); + +const mockUseAuthorizationIam = useAuthorizationIam as unknown as MockInstance; + +describe('ActionMenu Snapshot Tests', () => { + const baseItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'Action 1', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }, + { + id: 2, + onClick: vi.fn(), + label: 'Action 2', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }, + { + id: 3, + href: 'https://www.ovhcloud.com', + target: '_blank', + label: 'External Link', + }, + { + id: 4, + href: `data:text/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ name: 'john' }), + )}`, + download: 'test.json', + target: '_blank', + label: 'Download File', + }, + { + id: 5, + href: 'https://ovhcloud.com', + target: '_blank', + label: 'Disabled Link', + isDisabled: true, + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseAuthorizationIam.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + describe('Default rendering', () => { + it('should match snapshot with default props', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('default-props'); + }); + + it('should match snapshot with minimal props', () => { + const minimalItems = [ + { + id: 1, + label: 'Simple Action', + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('minimal-props'); + }); + }); + + describe('Compact mode', () => { + it('should match snapshot in compact mode', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('compact-mode'); + }); + + it('should match snapshot in compact mode with custom icon', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('compact-mode-custom-icon'); + }); + }); + + describe('Button variants', () => { + it('should match snapshot with outline variant', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('outline-variant'); + }); + + it('should match snapshot with ghost variant', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('ghost-variant'); + }); + + it('should match snapshot with default variant', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('default-variant'); + }); + }); + + describe('States', () => { + it('should match snapshot when disabled', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('disabled-state'); + }); + + it('should match snapshot when loading', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('loading-state'); + }); + + it('should match snapshot when disabled and loading', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('disabled-loading-state'); + }); + }); + + describe('Custom labels and icons', () => { + it('should match snapshot with custom label', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('custom-label'); + }); + + it('should match snapshot with custom icon', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('custom-icon'); + }); + + it('should match snapshot with custom label and icon', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('custom-label-and-icon'); + }); + }); + + describe('Popover positions', () => { + it('should match snapshot with top position', () => { + const { container } = render( +
+ +
, + ); + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + expect(container.firstChild).toMatchSnapshot('top-position'); + }); + + it('should match snapshot with right position', () => { + const { container } = render( +
+ +
, + ); + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + expect(container.firstChild).toMatchSnapshot('right-position'); + }); + + it('should match snapshot with left position', () => { + const { container } = render( +
+ +
, + ); + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + expect(container.firstChild).toMatchSnapshot('left-position'); + }); + }); + + describe('Item variations', () => { + it('should match snapshot with only link items', () => { + const linkOnlyItems = [ + { + id: 1, + href: 'https://example.com', + label: 'External Link', + target: '_blank', + }, + { + id: 2, + href: '/internal-link', + label: 'Internal Link', + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('link-only-items'); + }); + + it('should match snapshot with only button items', () => { + const buttonOnlyItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'Button Action 1', + }, + { + id: 2, + onClick: vi.fn(), + label: 'Button Action 2', + isDisabled: true, + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('button-only-items'); + }); + + it('should match snapshot with IAM protected items', () => { + const iamItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'IAM Protected Action', + urn: 'urn:v18:eu:resource:m--components:test', + iamActions: ['test:action:read'], + }, + { + id: 2, + onClick: vi.fn(), + label: 'Another IAM Action', + urn: 'urn:v18:eu:resource:m--components:test', + iamActions: ['test:action:write'], + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('iam-protected-items'); + }); + + it('should match snapshot with mixed item types', () => { + const mixedItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'Button Action', + }, + { + id: 2, + href: 'https://example.com', + label: 'External Link', + target: '_blank', + }, + { + id: 3, + onClick: vi.fn(), + label: 'IAM Protected', + urn: 'urn:v18:eu:resource:m--components:test', + iamActions: ['test:action:read'], + }, + { + id: 4, + href: '/download', + download: 'file.pdf', + label: 'Download', + }, + ]; + + const { container } = render( +
+ +
, + ); + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + expect(container.firstChild).toMatchSnapshot('mixed-item-types'); + }); + }); + + describe('Edge cases', () => { + it('should match snapshot with empty items array', () => { + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('empty-items'); + }); + + it('should match snapshot with single item', () => { + const singleItem = [ + { + id: 1, + label: 'Single Action', + onClick: vi.fn(), + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('single-item'); + }); + + it('should match snapshot with many items', () => { + const manyItems = Array.from({ length: 10 }, (_, index) => ({ + id: index + 1, + label: `Action ${index + 1}`, + onClick: vi.fn(), + })); + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('many-items'); + }); + }); + + describe('IAM authorization states', () => { + it('should match snapshot when IAM is loading', () => { + mockUseAuthorizationIam.mockReturnValue({ + isAuthorized: false, + isLoading: true, + isFetched: false, + }); + + const iamItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'IAM Action', + urn: 'urn:v18:eu:resource:m--components:test', + iamActions: ['test:action:read'], + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('iam-loading-state'); + }); + + it('should match snapshot when IAM is not authorized', () => { + mockUseAuthorizationIam.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + + const iamItems = [ + { + id: 1, + onClick: vi.fn(), + label: 'Unauthorized Action', + urn: 'urn:v18:eu:resource:m--components:test', + iamActions: ['test:action:read'], + }, + ]; + + const { container } = render( +
+ +
, + ); + expect(container.firstChild).toMatchSnapshot('iam-unauthorized-state'); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.spec.tsx b/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.spec.tsx new file mode 100644 index 000000000000..6997d8e3991d --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/__tests__/ActionMenu.spec.tsx @@ -0,0 +1,125 @@ +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { act, waitFor, screen, fireEvent } from '@testing-library/react'; +import { POPOVER_POSITION, ICON_NAME } from '@ovhcloud/ods-react'; +import { ActionMenu, ActionMenuProps } from '../index'; +import { render } from '@/setupTest'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +vitest.mock('../../../hooks/iam'); + +const actionItems: ActionMenuProps = { + id: 'action-menu-test-id', + items: [ + { + id: 1, + onClick: () => window.open('/'), + label: 'Action 1', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }, + { + id: 2, + onClick: () => window.open('/'), + label: 'Action 2', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }, + { + id: 3, + href: 'https://www.ovhcloud.com', + target: '_blank', + label: 'external link', + }, + { + id: 4, + href: `data:text/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ name: 'john' }), + )}`, + download: 'test.json', + target: '_blank', + label: 'download', + }, + { + id: 5, + href: 'https://ovhcloud.com', + target: '_blank', + label: 'disabled link', + isDisabled: true, + }, + ], +}; + +const setupSpecTest = (customProps?: Partial) => + render( +
+ +
, + ); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe('ActionMenu', () => { + beforeEach(() => { + vitest.clearAllMocks(); + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: true, + isFetched: true, + }); + }); + + it('renders menu actions correctly', () => { + const { container } = setupSpecTest(); + + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + + const action1 = screen.getAllByTestId('manager-button')[0]; + const action2 = screen.getAllByTestId('manager-button')[1]; + expect(action1).toBeInTheDocument(); + expect(action2).toBeInTheDocument(); + expect(screen.getAllByTestId('manager-button').length).toBe(2); + const icon = container.querySelector('span[class*="chevron-down"]'); + expect(icon).toBeInTheDocument(); + }); + + it('renders compact menu with classic ellipsis correctly', () => { + const { container } = setupSpecTest({ isCompact: true }); + const icon = container.querySelector('span[class*="ellipsis-vertical"]'); + expect(icon).toBeInTheDocument(); + }); + + it('renders compact menu with custom icon menu correctly', async () => { + const { container } = setupSpecTest({ + icon: ICON_NAME.ellipsisHorizontal, + }); + const icon = container.querySelector('span[class*="ellipsis-horizontal"]'); + expect(icon).toBeInTheDocument(); + }); + + it('renders compact menu with popover position right', () => { + const { container } = setupSpecTest({ + popoverPosition: POPOVER_POSITION.right, + }); + + act(() => { + const actionMenuIcon = screen.getByTestId( + 'navigation-action-trigger-action', + ); + fireEvent.click(actionMenuIcon); + }); + waitFor(() => { + const popoverPosition = container.parentElement?.querySelector( + 'div[data-scope="popover"]', + ); + expect(popoverPosition?.children[0].getAttribute('data-placement')).toBe( + 'right', + ); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/action-menu/__tests__/__snapshots__/ActionMenu.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/action-menu/__tests__/__snapshots__/ActionMenu.snapshot.test.tsx.snap new file mode 100644 index 000000000000..e50166297ad1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/__tests__/__snapshots__/ActionMenu.snapshot.test.tsx.snap @@ -0,0 +1,681 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ActionMenu Snapshot Tests > Button variants > should match snapshot with default variant > default-variant 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Button variants > should match snapshot with ghost variant > ghost-variant 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Button variants > should match snapshot with outline variant > outline-variant 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Compact mode > should match snapshot in compact mode > compact-mode 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Compact mode > should match snapshot in compact mode with custom icon > compact-mode-custom-icon 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Custom labels and icons > should match snapshot with custom icon > custom-icon 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Custom labels and icons > should match snapshot with custom label > custom-label 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Custom labels and icons > should match snapshot with custom label and icon > custom-label-and-icon 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Default rendering > should match snapshot with default props > default-props 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Default rendering > should match snapshot with minimal props > minimal-props 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Edge cases > should match snapshot with empty items array > empty-items 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Edge cases > should match snapshot with many items > many-items 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Edge cases > should match snapshot with single item > single-item 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > IAM authorization states > should match snapshot when IAM is loading > iam-loading-state 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > IAM authorization states > should match snapshot when IAM is not authorized > iam-unauthorized-state 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Item variations > should match snapshot with IAM protected items > iam-protected-items 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Item variations > should match snapshot with mixed item types > mixed-item-types 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Item variations > should match snapshot with only button items > button-only-items 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Item variations > should match snapshot with only link items > link-only-items 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Popover positions > should match snapshot with left position > left-position 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Popover positions > should match snapshot with right position > right-position 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > Popover positions > should match snapshot with top position > top-position 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > States > should match snapshot when disabled > disabled-state 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > States > should match snapshot when disabled and loading > disabled-loading-state 1`] = ` +
+ +
+`; + +exports[`ActionMenu Snapshot Tests > States > should match snapshot when loading > loading-state 1`] = ` +
+ +
+`; diff --git a/packages/manager-ui-kit/src/components/action-menu/action-menu.scss b/packages/manager-ui-kit/src/components/action-menu/action-menu.scss new file mode 100644 index 000000000000..d19e0d31186b --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/action-menu.scss @@ -0,0 +1,37 @@ +.menu-item-ul { + list-style: none; + margin: 0px; + padding: 0px; + text-align: left; + + li { + a, + button { + height: 32px; + border-radius: 0; + width: 100%; + justify-content: left; + } + + a { + padding-left: 5px; + } + + a:focus-visible, + a:hover { + background-size: none; + text-decoration: none; + background-color: var(--ods-color-primary-100); + color: var(--ods-color-primary-700); + border: none; + background: none; + } + a::after { + background: none; + } + + &:hover { + background-color: var(--ods-color-primary-100); + } + } +} diff --git a/packages/manager-ui-kit/src/components/action-menu/index.ts b/packages/manager-ui-kit/src/components/action-menu/index.ts new file mode 100644 index 000000000000..bd7ec44f1936 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/index.ts @@ -0,0 +1,2 @@ +export { ActionMenu } from './ActionMenu.component'; +export type { ActionMenuItem, ActionMenuProps } from './ActionMenu.props'; diff --git a/packages/manager-ui-kit/src/components/action-menu/menu-item/MenuItem.component.tsx b/packages/manager-ui-kit/src/components/action-menu/menu-item/MenuItem.component.tsx new file mode 100644 index 000000000000..16b0cb4f704f --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/menu-item/MenuItem.component.tsx @@ -0,0 +1,44 @@ +import { BUTTON_SIZE, BUTTON_VARIANT } from '@ovhcloud/ods-react'; +import { Link } from '../../Link'; +import { ActionMenuItem } from '../ActionMenu.props'; +import { Button } from '../../button'; + +export const MenuItem = ({ + item, + isTrigger, + id, +}: { + item: Omit; + isTrigger: boolean; + id: number; +}) => { + const buttonProps = { + size: BUTTON_SIZE.sm, + variant: BUTTON_VARIANT.ghost, + displayTooltip: false, + className: 'menu-item-button w-full', + ...item, + }; + + if (item.href) { + return ( + + {item.label} + + ); + } + + return ( + + ); +}; diff --git a/packages/manager-ui-kit/src/components/action-menu/menu-item/__tests__/MenuItem.spec.tsx b/packages/manager-ui-kit/src/components/action-menu/menu-item/__tests__/MenuItem.spec.tsx new file mode 100644 index 000000000000..b4e91e5cc12f --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/menu-item/__tests__/MenuItem.spec.tsx @@ -0,0 +1,172 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { MenuItem } from '../MenuItem.component'; +import { useAuthorizationIam } from '../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; +import { ActionMenuItem } from '../../ActionMenu.props'; + +vi.mock('../../../../hooks/iam'); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +describe('MenuItem', () => { + const mockItem: Omit = { + label: 'Test Menu Item', + onClick: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Menu item rendering', () => { + it('should render a link when href is provided', () => { + const itemWithHref = { + ...mockItem, + href: 'https://example.com', + target: '_blank', + download: 'test-file.pdf', + }; + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + render(); + + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); + expect(link).toHaveAttribute('download', 'test-file.pdf'); + expect(link).toHaveTextContent('Test Menu Item'); + }); + + it('should render a link with href and IAM actions', () => { + const itemWithHrefIamActions = { + ...mockItem, + href: 'https://example.com', + label: 'Action 1', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }; + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + render( + , + ); + + expect(screen.getByText('Action 1')).toBeInTheDocument(); + expect(screen.getByText('Action 1')).not.toBeDisabled(); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', 'https://example.com'); + }); + + it('should render a link with no IAM actions', () => { + const itemWithHrefIamActions = { + ...mockItem, + href: 'https://example.com', + label: 'Action 1', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }; + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + render( + , + ); + expect(screen.getByText('Action 1')).toBeInTheDocument(); + }); + }); + + describe('Button rendering', () => { + it('should render a regular button when no href and no IAM actions', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Test Menu Item'); + expect(button).toHaveClass('menu-item-button', 'w-full'); + }); + + it('should render a regular button with custom props', () => { + const itemWithCustomProps = { + ...mockItem, + variant: 'primary' as any, + color: 'success' as any, + isDisabled: true, + isLoading: true, + 'data-testid': 'custom-button', + }; + + render(); + + const button = screen.getByTestId('custom-button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Test Menu Item'); + }); + + it('should call onClick when button is clicked', () => { + const onClickMock = vi.fn(); + const itemWithOnClick = { + ...mockItem, + onClick: onClickMock, + }; + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + expect(onClickMock).toHaveBeenCalledTimes(1); + }); + + it('should render a Button with href and IAM actions', () => { + const itemWithHrefIamActions = { + ...mockItem, + label: 'Action 2', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }; + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + render( + , + ); + + // Remove comment after merging Button Component + // const button = screen.getByRole('Button'); + // expect(button).toHaveTextContent('Action 2'); + // expect(button).not.toBeDisabled(); + }); + + it('should render a Button with no IAM actions', () => { + const itemWithHrefIamActions = { + ...mockItem, + label: 'Action 2', + urn: 'urn:v18:eu:resource:m--components:vrz-a878-dsflkds-fdsfdsfdsf', + iamActions: ['vrackServices:apiovh:iam/resource/tag/remove'], + }; + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + render( + , + ); + // Remove comment after merging Button Component + // const button = screen.getByRole('button'); + // expect(button).toHaveTextContent('Action 2'); + // expect(button).toBeDisabled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/action-menu/menu-item/index.ts b/packages/manager-ui-kit/src/components/action-menu/menu-item/index.ts new file mode 100644 index 000000000000..e90e0b555ada --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/menu-item/index.ts @@ -0,0 +1 @@ +export { MenuItem } from './MenuItem.component'; diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_de_DE.json new file mode 100644 index 000000000000..c634f5414de7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_de_DE.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Aktionen" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_en_GB.json new file mode 100644 index 000000000000..5518a6c418e3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_en_GB.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Actions" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_es_ES.json new file mode 100644 index 000000000000..66b0b687868a --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_es_ES.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Acciones" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_CA.json new file mode 100644 index 000000000000..5518a6c418e3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_CA.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Actions" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..5518a6c418e3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_fr_FR.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Actions" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_it_IT.json new file mode 100644 index 000000000000..cb7764f502ae --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_it_IT.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Azioni" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pl_PL.json new file mode 100644 index 000000000000..e722f7629195 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pl_PL.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Operacje" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pt_PT.json new file mode 100644 index 000000000000..4e6747ea560d --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/Messages_pt_PT.json @@ -0,0 +1,3 @@ +{ + "common_actions": "Ações" +} diff --git a/packages/manager-ui-kit/src/components/action-menu/translations/translation.ts b/packages/manager-ui-kit/src/components/action-menu/translations/translation.ts new file mode 100644 index 000000000000..db6fb27eb6b4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/action-menu/translations/translation.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'action-menu'); diff --git a/packages/manager-ui-kit/src/components/badge/__tests__/__snapshots__/badge.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/badge/__tests__/__snapshots__/badge.snapshot.test.tsx.snap new file mode 100644 index 000000000000..031d6ef42438 --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/__tests__/__snapshots__/badge.snapshot.test.tsx.snap @@ -0,0 +1,20 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Badge Snapshot Tests > Displays Badge 1`] = ` +
+ + Active + +
+`; + +exports[`Badge Snapshot Tests > Displays Loader 1`] = ` +
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/badge/__tests__/badge.snapshot.test.tsx b/packages/manager-ui-kit/src/components/badge/__tests__/badge.snapshot.test.tsx new file mode 100644 index 000000000000..2d1a4a895338 --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/__tests__/badge.snapshot.test.tsx @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import { BADGE_COLOR, BADGE_SIZE } from '@ovhcloud/ods-react'; +import { Badge } from '../badge.component'; + +describe('Badge Snapshot Tests', () => { + it('Displays Badge', () => { + const { container } = render( + + Active + , + ); + expect(container).toMatchSnapshot(); + }); + + it('Displays Loader', () => { + const { container } = render( + + Active + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/badge/__tests__/badge.spec.tsx b/packages/manager-ui-kit/src/components/badge/__tests__/badge.spec.tsx new file mode 100644 index 000000000000..e0abd34c8232 --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/__tests__/badge.spec.tsx @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { waitFor, screen } from '@testing-library/react'; +import { BADGE_COLOR, BADGE_SIZE } from '@ovhcloud/ods-react'; +import { render } from '@/setupTest'; +import { Badge } from '../badge.component'; + +describe('Badge component', () => { + it('should render badge with correct props', async () => { + render( + + Active + , + ); + await waitFor(() => { + const component = screen.getByTestId('test'); + expect(component.hasAttribute(BADGE_COLOR.information)); + expect(component.hasAttribute('active')); + }); + }); + it('should render OdsSkeleton when isLoading is true', async () => { + render(); + await waitFor(() => { + const skeleton = screen.getByTestId('skeleton'); + expect(skeleton).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/badge/badge.component.tsx b/packages/manager-ui-kit/src/components/badge/badge.component.tsx new file mode 100644 index 000000000000..cfc4403f5077 --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/badge.component.tsx @@ -0,0 +1,10 @@ +import { Badge as OdsBadge, Skeleton } from '@ovhcloud/ods-react'; +import { BadgeProps } from './badge.props'; + +export const Badge = ({ children, isLoading, ...props }: BadgeProps) => { + return isLoading ? ( + + ) : ( + {children} + ); +}; diff --git a/packages/manager-ui-kit/src/components/badge/badge.props.ts b/packages/manager-ui-kit/src/components/badge/badge.props.ts new file mode 100644 index 000000000000..5d5042073052 --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/badge.props.ts @@ -0,0 +1,7 @@ +import { PropsWithChildren } from 'react'; +import { type BadgeProp } from '@ovhcloud/ods-react'; + +export type BadgeProps = BadgeProp & { + isLoading?: boolean; + 'data-testid'?: string; +}; diff --git a/packages/manager-ui-kit/src/components/badge/index.ts b/packages/manager-ui-kit/src/components/badge/index.ts new file mode 100644 index 000000000000..260a0c1b650a --- /dev/null +++ b/packages/manager-ui-kit/src/components/badge/index.ts @@ -0,0 +1,3 @@ +export { Badge } from './badge.component'; + +export type { BadgeProps } from './badge.props'; diff --git a/packages/manager-ui-kit/src/components/base-layout/BaseLayout.component.tsx b/packages/manager-ui-kit/src/components/base-layout/BaseLayout.component.tsx new file mode 100644 index 000000000000..88374517a4f4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/BaseLayout.component.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { Header } from './header/Header.component'; +import { LinkType, Link } from '../Link'; +import { BaseLayoutProps } from './BaseLayout.props'; + +export const BaseLayout = ({ + backLink, + breadcrumb, + description, + subtitle, + message, + children, + header, + tabs, +}: BaseLayoutProps) => ( +
+
{breadcrumb}
+ {header && ( +
+
+
+ )} + {backLink && (backLink.onClick || backLink.previousPageLink) && ( +
+ + {backLink.label} + +
+ )} + {description && ( + + {description} + + )} + {message &&
{message}
} + {subtitle && ( + + {subtitle} + + )} + {tabs &&
{tabs}
} + {children} +
+); diff --git a/packages/manager-ui-kit/src/components/base-layout/BaseLayout.props.ts b/packages/manager-ui-kit/src/components/base-layout/BaseLayout.props.ts new file mode 100644 index 000000000000..9decd2dc4fec --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/BaseLayout.props.ts @@ -0,0 +1,16 @@ +import { PropsWithChildren, ReactElement } from 'react'; +import { HeaderProps } from './header/Header.props'; + +export type BaseLayoutProps = PropsWithChildren<{ + breadcrumb?: ReactElement; + header?: HeaderProps; + message?: ReactElement; + description?: string; + subtitle?: string; + backLink?: { + label: string; + onClick?: () => void; + previousPageLink?: string; + }; + tabs?: ReactElement; +}>; diff --git a/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.snapshot.test.tsx b/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.snapshot.test.tsx new file mode 100644 index 000000000000..4f3b04eaee4d --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.snapshot.test.tsx @@ -0,0 +1,88 @@ +import { vitest, describe, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { BaseLayout } from '..'; +import { GuideMenu, GuideMenuItem } from '../../guide-menu'; +import { ChangelogMenu, ChangelogMenuLinks } from '../../changelog-menu'; +import { Breadcrumb } from '../../breadcrumb'; +import { TabsComponent } from '../../tabs'; +import { Notifications } from '../../notifications'; + +vitest.mock('react-router-dom', async () => ({ + ...(await vitest.importActual('react-router-dom')), + useLocation: () => ({ + pathname: '/foo', + }), +})); + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +vitest.mock('@ovh-ux/manager-react-shell-client', async (importActual) => ({ + ...(await importActual()), + useOvhTracking: () => ({ + trackClick: vitest.fn(), + }), +})); + +const guideMenu = ( + +); + +const ChangelogMenuComponent = ( + +); + +const breadcrumb = ; + +const tabs = ; + +const message = ; + +const renderBaseLayout = ({ children, ...rest }) => + render({children}); + +describe('BaseLayout component - Snapshot tests', () => { + it('renders the complete Base Layout', () => { + const { container } = renderBaseLayout({ + children:
Main Content of the Page.
, + header: { + title: 'Title of the page', + guideMenu, + ChangelogMenuComponent, + }, + breadcrumb, + tabs, + backLink: { + label: 'back Link Label', + onClick: vitest.fn(), + }, + description: 'Sample description of the page.', + message, + subtitle: 'Sample Sub-title of the page', + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.spec.tsx b/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.spec.tsx new file mode 100644 index 000000000000..354af614ad73 --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/__tests__/BaseLayout.spec.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; +import { BaseLayout } from '..'; +import { GuideMenu, GuideMenuItem } from '../../guide-menu'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + } as IamAuthorizationResponse), +})); + +describe('BaseLayout component', () => { + it('renders with header', () => { + const header = { + title: 'Test Header', + guideMenu: ( + + ), + }; + render(); + const titleElement = screen.getByText('Test Header'); + expect(titleElement).toBeInTheDocument(); + expect(titleElement.tagName).toBe('H1'); + }); + + it('renders with breadcrumb', () => { + const breadcrumb =
; + render(); + expect(screen.getByTestId('breadcrumb')).toBeInTheDocument(); + }); + + it('renders with description', () => { + const description = 'Test Description'; + render(); + const descriptionElement = screen.getByText(description); + expect(descriptionElement).toBeInTheDocument(); + expect(descriptionElement.tagName).toBe('SPAN'); + }); + + it('renders with message', () => { + const message =
; + render(); + expect(screen.getByTestId('messages')).toBeInTheDocument(); + }); + + it('renders with sub-title', () => { + const subtitle = 'Test Sub-title'; + render(); + const subtitleElement = screen.getByText(subtitle); + expect(subtitleElement).toBeInTheDocument(); + expect(subtitleElement.tagName).toBe('H3'); + }); + + it('renders with tabs', () => { + const tabs =
; + render(); + expect(screen.getByTestId('tabs')).toBeInTheDocument(); + }); + + it('renders children', () => { + render( + +
+
, + ); + expect(screen.getByTestId('children')).toBeInTheDocument(); + }); + + it('renders back link', () => { + const backLink = { + label: 'Back Link Label', + onClick: vitest.fn(), + }; + render(); + const linkElement = screen.getByTestId('manager-back-link'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.textContent).toBe(backLink.label); + + fireEvent.click(linkElement); + expect(backLink.onClick).toHaveBeenCalled(); + }); + + it('renders back link with previous page href', () => { + const backLink = { + label: 'Back Link Label', + previousPageLink: 'https://ovhcloud.com/', + }; + render(); + const linkElement = screen.getByTestId('manager-back-link'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.textContent).toBe(backLink.label); + }); + + it('does not render backLink when "onClick" or previousPageLink is not provided', () => { + render(); + expect(screen.queryByText(/Back Link Label/i)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/base-layout/__tests__/__snapshots__/BaseLayout.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/base-layout/__tests__/__snapshots__/BaseLayout.snapshot.test.tsx.snap new file mode 100644 index 000000000000..7d3ae8135ca2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/__tests__/__snapshots__/BaseLayout.snapshot.test.tsx.snap @@ -0,0 +1,164 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`BaseLayout component - Snapshot tests > renders the complete Base Layout 1`] = ` +
+
+
+ +
+
+
+

+ Title of the page +

+
+ +
+
+
+ + + Sample description of the page. + +
+

+ Sample Sub-title of the page +

+
+
+
+
+ + +
+
+
+ tab1 +
+
+
+
+ Main Content of the Page. +
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/base-layout/header/Header.component.tsx b/packages/manager-ui-kit/src/components/base-layout/header/Header.component.tsx new file mode 100644 index 000000000000..24c00a1811da --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/header/Header.component.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { HeaderProps } from './Header.props'; + +export const Header: React.FC = ({ + title, + guideMenu, + changelogButton, +}) => { + return ( +
+ {title && {title}} + {(guideMenu || changelogButton) && ( +
+ {changelogButton} + {guideMenu} +
+ )} +
+ ); +}; + +export default Header; diff --git a/packages/manager-ui-kit/src/components/base-layout/header/Header.props.ts b/packages/manager-ui-kit/src/components/base-layout/header/Header.props.ts new file mode 100644 index 000000000000..10c8ebdd9b57 --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/header/Header.props.ts @@ -0,0 +1,7 @@ +import React from 'react'; + +export type HeaderProps = { + title?: string; + guideMenu?: React.ReactElement; + changelogButton?: React.ReactElement; +}; diff --git a/packages/manager-ui-kit/src/components/base-layout/header/__tests__/Header.spec.tsx b/packages/manager-ui-kit/src/components/base-layout/header/__tests__/Header.spec.tsx new file mode 100644 index 000000000000..e7bd0d56149b --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/header/__tests__/Header.spec.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import Header from '../Header.component'; + +describe('Header Component', () => { + const defaultProps = { + title: 'Page Title', + guideMenu: , + changelogButton: , + }; + + it('renders title correctly', () => { + render(
); + + expect(screen.getByText('Page Title')).toBeInTheDocument(); + }); + + it('does not render title if not provided', () => { + render(
); + expect(screen.queryByTestId('title')).not.toBeInTheDocument(); + }); + + it('renders both buttons when provided', () => { + render(
); + expect(screen.getByText('Guide')).toBeInTheDocument(); + expect(screen.getByText('Changelog')).toBeInTheDocument(); + }); + + it('does not render buttons when both are not provided', () => { + render( +
, + ); + expect( + screen.queryByRole('button', { name: /Guide/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /Changelog/i }), + ).not.toBeInTheDocument(); + }); + + it('renders only guide button when only guideMenu is provided', () => { + render(
); + expect(screen.getByText('Guide')).toBeInTheDocument(); + expect(screen.queryByText('Changelog')).not.toBeInTheDocument(); + }); + + it('renders only changelog button when only changelogButton is provided', () => { + render(
); + expect(screen.getByText('Changelog')).toBeInTheDocument(); + expect(screen.queryByText('Guide')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/base-layout/index.ts b/packages/manager-ui-kit/src/components/base-layout/index.ts new file mode 100644 index 000000000000..53c83b7215c7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/base-layout/index.ts @@ -0,0 +1,5 @@ +export { BaseLayout } from './BaseLayout.component'; + +export type { BaseLayoutProps } from './BaseLayout.props'; + +export type { HeaderProps } from './header/Header.props'; diff --git a/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.component.tsx b/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.component.tsx new file mode 100644 index 000000000000..f417c5acddc1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.component.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { + Breadcrumb as OdsBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, +} from '@ovhcloud/ods-react'; +import { BreadcrumbProps } from './Breadcrumb.props'; +import { useBreadcrumb } from '../../hooks/breadcrumb/useBreadcrumb'; + +export const Breadcrumb: React.FC = ({ + appName, + hideRootLabel = false, + rootLabel, +}) => { + const breadcrumbItems = useBreadcrumb({ + appName, + hideRootLabel, + rootLabel, + }); + return ( + + {breadcrumbItems + ?.filter((item) => !item.hideLabel) + ?.map((item) => ( + + {item.label} + + ))} + + ); +}; diff --git a/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.props.ts b/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.props.ts new file mode 100644 index 000000000000..a0f2be9639d2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/Breadcrumb.props.ts @@ -0,0 +1,8 @@ +export interface BreadcrumbProps { + /** application name define in the shell */ + appName: string; + /** root label step */ + rootLabel: string; + /** hides app name from breadcrumb */ + hideRootLabel?: boolean; +} diff --git a/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.snapshot.test.tsx b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.snapshot.test.tsx new file mode 100644 index 000000000000..45f8f411e490 --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.snapshot.test.tsx @@ -0,0 +1,33 @@ +import { vitest } from 'vitest'; +import { render } from '@/setupTest'; +import { Breadcrumb } from '../Breadcrumb.component'; + +vitest.mock('../../../hooks/breadcrumb/useBreadcrumb', () => ({ + useBreadcrumb: vitest.fn(({ hideRootLabel }) => [ + { label: 'vRack services', href: '/', hideLabel: hideRootLabel }, + ]), +})); + +describe('breadcrumb component snapshot', () => { + it('should render 3 breadcrumb items when hideRootLabel is false', () => { + const { asFragment } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + }); + + it('should hide root label when hideRootLabel is true', () => { + const { asFragment } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.spec.tsx b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.spec.tsx new file mode 100644 index 000000000000..63afc3a9f7cb --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/Breadcrumb.spec.tsx @@ -0,0 +1,44 @@ +import { vitest } from 'vitest'; +import { render } from '@/setupTest'; +import { Breadcrumb } from '../Breadcrumb.component'; + +vitest.mock('../../../hooks/breadcrumb/useBreadcrumb', () => ({ + useBreadcrumb: vitest.fn(({ hideRootLabel }) => [ + { label: 'vRack services', href: '/', hideLabel: hideRootLabel }, + { label: 'vRack service', href: '/:id', hideLabel: false }, + { + label: 'vRack service listing', + href: '/:id/listing', + hideLabel: false, + }, + ]), +})); + +describe('breadcrumb component', () => { + it('should render 3 breadcrumb items when hideRootLabel is false', () => { + const { container } = render( + , + ); + const items = container.querySelectorAll('li'); + expect(items.length).toBe(3); + expect(items[0]).toBeVisible(); + expect(items[1]).toBeVisible(); + expect(items[2]).toBeVisible(); + }); + + it('should hide root label when hideRootLabel is true', () => { + const { container } = render( + , + ); + const items = container.querySelectorAll('li'); + expect(items.length).toBe(2); + }); +}); diff --git a/packages/manager-ui-kit/src/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.snapshot.test.tsx.snap new file mode 100644 index 000000000000..9d4bd68ac34c --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/__tests__/__snapshots__/Breadcrumb.snapshot.test.tsx.snap @@ -0,0 +1,36 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`breadcrumb component snapshot > should hide root label when hideRootLabel is true 1`] = ` + + + +`; + +exports[`breadcrumb component snapshot > should render 3 breadcrumb items when hideRootLabel is false 1`] = ` + + + +`; diff --git a/packages/manager-ui-kit/src/components/breadcrumb/index.ts b/packages/manager-ui-kit/src/components/breadcrumb/index.ts new file mode 100644 index 000000000000..acf870a365de --- /dev/null +++ b/packages/manager-ui-kit/src/components/breadcrumb/index.ts @@ -0,0 +1,2 @@ +export { Breadcrumb } from './Breadcrumb.component'; +export type { BreadcrumbProps } from './Breadcrumb.props'; diff --git a/packages/manager-ui-kit/src/components/button/Button.component.tsx b/packages/manager-ui-kit/src/components/button/Button.component.tsx new file mode 100644 index 000000000000..7758af3888c4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/button/Button.component.tsx @@ -0,0 +1,57 @@ +import { + Button as OdsButton, + ButtonProp, + Tooltip, + TooltipContent, + TooltipTrigger, + TOOLTIP_POSITION, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { ButtonProps } from './Button.props'; +import './translations'; + +import { useAuthorizationIam } from '../../hooks/iam'; + +export const Button = ({ + children, + iamActions, + urn, + displayTooltip = true, + tooltipPosition = TOOLTIP_POSITION.bottom, + isIamTrigger = true, + ...restProps +}: ButtonProps & ButtonProp) => { + const { t } = useTranslation('iam'); + const { isAuthorized } = useAuthorizationIam(iamActions || [], urn || ''); + + if (isAuthorized || !(iamActions && urn)) { + return ( + + {children} + + ); + } + + return displayTooltip ? ( + + + + {children} + + + {t('common_iam_actions_message')} + + ) : ( + + {children} + + ); +}; diff --git a/packages/manager-ui-kit/src/components/button/Button.props.ts b/packages/manager-ui-kit/src/components/button/Button.props.ts new file mode 100644 index 000000000000..d65b365450bf --- /dev/null +++ b/packages/manager-ui-kit/src/components/button/Button.props.ts @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; +import { TOOLTIP_POSITION, ButtonProp } from '@ovhcloud/ods-react'; + +export type ButtonProps = PropsWithChildren<{ + iamActions?: string[]; + urn?: string; + displayTooltip?: boolean; + isIamTrigger?: boolean; + children: JSX.Element | string; + tooltipPosition?: TOOLTIP_POSITION; +}> & + ButtonProp; diff --git a/packages/manager-ui-kit/src/components/button/__tests__/Button.snapshot.test.tsx b/packages/manager-ui-kit/src/components/button/__tests__/Button.snapshot.test.tsx new file mode 100644 index 000000000000..14b3709901f7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/button/__tests__/Button.snapshot.test.tsx @@ -0,0 +1,275 @@ +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { TOOLTIP_POSITION } from '@ovhcloud/ods-react'; +import { render } from '@/setupTest'; +import { Button } from '../index'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe('Button Snapshot Tests', () => { + afterEach(() => { + vitest.resetAllMocks(); + }); + + describe('Authorized Button States', () => { + it('should render basic button when authorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('should render button with IAM props when authorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render button with ODS button props when authorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Unauthorized Button States', () => { + it('should render disabled button with tooltip when unauthorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render disabled button without tooltip when unauthorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render disabled button with custom tooltip position', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render disabled button with all ODS props when unauthorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Edge Cases', () => { + it('should render button when no IAM props provided', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('should render button when only iamActions provided', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render button when only urn provided', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render button with JSX children', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render button with isIamTrigger set to false', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Loading States', () => { + it('should render button when IAM is loading', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: true, + isFetched: false, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render button when IAM is loading and authorized', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: true, + isFetched: false, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/button/__tests__/Button.spec.tsx b/packages/manager-ui-kit/src/components/button/__tests__/Button.spec.tsx new file mode 100644 index 000000000000..2e0aed6e19eb --- /dev/null +++ b/packages/manager-ui-kit/src/components/button/__tests__/Button.spec.tsx @@ -0,0 +1,91 @@ +import { vitest } from 'vitest'; +import { fireEvent, screen } from '@testing-library/react'; +import { Button, ButtonProps } from '../index'; +import { render } from '@/setupTest'; +import fr_FR from '../translations/Messages_fr_FR.json'; +import { useAuthorizationIam } from '../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const renderComponent = (props: ButtonProps) => { + return render( +
+`; + +exports[`Button Snapshot Tests > Authorized Button States > should render button with IAM props when authorized 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Authorized Button States > should render button with ODS button props when authorized 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Edge Cases > should render button when no IAM props provided 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Edge Cases > should render button when only iamActions provided 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Edge Cases > should render button when only urn provided 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Edge Cases > should render button with JSX children 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Edge Cases > should render button with isIamTrigger set to false 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Loading States > should render button when IAM is loading 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Loading States > should render button when IAM is loading and authorized 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Unauthorized Button States > should render disabled button with all ODS props when unauthorized 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Unauthorized Button States > should render disabled button with custom tooltip position 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Unauthorized Button States > should render disabled button with tooltip when unauthorized 1`] = ` +
+ +
+`; + +exports[`Button Snapshot Tests > Unauthorized Button States > should render disabled button without tooltip when unauthorized 1`] = ` +
+ +
+`; diff --git a/packages/manager-ui-kit/src/components/button/index.ts b/packages/manager-ui-kit/src/components/button/index.ts new file mode 100644 index 000000000000..02e042b56992 --- /dev/null +++ b/packages/manager-ui-kit/src/components/button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button.component'; +export type { ButtonProps } from './Button.props'; diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/button/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/button/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/button/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/button/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/button/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/button/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/button/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/button/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/button/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/ManagerButton/translations/index.ts b/packages/manager-ui-kit/src/components/button/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/ManagerButton/translations/index.ts rename to packages/manager-ui-kit/src/components/button/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.component.tsx b/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.component.tsx new file mode 100644 index 000000000000..83df0f1fa3ad --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.component.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react'; +import { BUTTON_VARIANT } from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { ActionMenu } from '../action-menu'; +import { LinkType } from '../Link'; +import { ChangelogMenuProps, ChangelogMenuLinks } from './ChangelogMenu.props'; +import './translations/translation'; + +export const CHANGELOG_PREFIXES = ['tile-changelog-roadmap', 'external-link']; +const GO_TO = (link: string) => `go-to-${link}`; + +const LinksTrad = { + changelog: 'mrc_changelog_changelog', + roadmap: 'mrc_changelog_roadmap', + 'feature-request': 'mrc_changelog_feature-request', +}; + +export const ChangelogMenu: React.FC = ({ + links, + chapters = [], + prefixes, +}) => { + const { t } = useTranslation('changelog-menu'); + const { trackClick } = useOvhTracking(); + const [linksArray, setLinksArray] = useState< + { + id: number; + label: string; + href: string; + linktype: LinkType; + onClick: () => void; + }[] + >([]); + + const sendTrackClick = (key: string) => { + trackClick({ + actionType: 'navigation', + actions: [...chapters, ...(prefixes || CHANGELOG_PREFIXES), GO_TO(key)], + }); + }; + + useEffect(() => { + const linksTab: { + id: number; + label: string; + href: string; + linktype: LinkType; + onClick: () => void; + }[] = []; + Object.keys(links).forEach((key, index) => { + linksTab.push({ + id: index, + label: t(LinksTrad[key as keyof typeof LinksTrad]), + href: links[key as keyof ChangelogMenuLinks], + linktype: LinkType.external, + onClick: () => sendTrackClick(key), + }); + }); + setLinksArray(linksTab); + }, [links]); + + return ( + + ); +}; diff --git a/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.props.ts b/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.props.ts new file mode 100644 index 000000000000..849f11442c12 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/ChangelogMenu.props.ts @@ -0,0 +1,11 @@ +export interface ChangelogMenuLinks { + changelog: string; + roadmap: string; + 'feature-request': string; +} + +export interface ChangelogMenuProps { + links: ChangelogMenuLinks; + chapters?: string[]; + prefixes?: string[]; +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.snapshot.test.tsx b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.snapshot.test.tsx new file mode 100644 index 000000000000..ef17910d9089 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.snapshot.test.tsx @@ -0,0 +1,70 @@ +import { vitest } from 'vitest'; +import { ChangelogMenu } from '../ChangelogMenu.component'; +import { render } from '@/setupTest'; +import { Links, chapters } from './ChangelogMenu.utils'; + +vitest.mock('@ovh-ux/manager-react-shell-client', () => ({ + useOvhTracking: vitest.fn(() => ({ + trackClick: vitest.fn(), + })), +})); + +describe('ChangelogMenu Snapshot Tests', () => { + it('should match snapshot with default props', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with chapters prop', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with custom prefixes', () => { + const customPrefixes = ['custom-prefix-1', 'custom-prefix-2']; + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with different link URLs', () => { + const customLinks = { + changelog: 'https://custom-changelog.com', + roadmap: 'https://custom-roadmap.com', + 'feature-request': 'https://custom-feature-request.com', + }; + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with single chapter', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with long URLs', () => { + const longUrls = { + changelog: + 'https://very-long-url-that-might-affect-rendering.com/path/to/changelog?param1=value1¶m2=value2¶m3=value3', + roadmap: + 'https://very-long-url-that-might-affect-rendering.com/path/to/roadmap?param1=value1¶m2=value2¶m3=value3', + 'feature-request': + 'https://very-long-url-that-might-affect-rendering.com/path/to/feature-request?param1=value1¶m2=value2¶m3=value3', + }; + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.spec.tsx b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.spec.tsx new file mode 100644 index 000000000000..e362a7be094c --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.spec.tsx @@ -0,0 +1,162 @@ +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { act, screen, fireEvent } from '@testing-library/react'; +import { useOvhTracking } from '@ovh-ux/manager-react-shell-client'; +import { ChangelogMenu, CHANGELOG_PREFIXES } from '../ChangelogMenu.component'; +import { render } from '@/setupTest'; +import TradFr from '../translations/Messages_fr_FR.json'; +import { Links, chapters } from './ChangelogMenu.utils'; + +vitest.mock('@ovh-ux/manager-react-shell-client', () => ({ + useOvhTracking: vitest.fn(), +})); + +const GO_TO = (link: string) => `go-to-${link}`; + +const defaultProps = { + links: Links, + chapters, +}; + +const setupSpecTest = (customProps = {}) => + render(); + +const mockedTracking = useOvhTracking as unknown as MockInstance; + +describe('ChangelogMenu', () => { + beforeEach(() => { + mockedTracking.mockReturnValue({ + trackClick: vitest.fn(), + }); + }); + + describe('Rendering', () => { + it('renders the changelog menu button correctly', () => { + setupSpecTest(); + + const changelogActionButton = screen.getByText( + TradFr.mrc_changelog_header, + ); + expect(changelogActionButton).toBeInTheDocument(); + + const changelogButton = screen.getByText(TradFr.mrc_changelog_changelog); + expect(changelogButton).toBeInTheDocument(); + + const roadmapButton = screen.getByText(TradFr.mrc_changelog_roadmap); + expect(roadmapButton).toBeInTheDocument(); + + const featureRequest = screen.getByText( + TradFr['mrc_changelog_feature-request'], + ); + expect(featureRequest).toBeInTheDocument(); + }); + }); + + describe('Interactions', () => { + it('tracks click when changelog link is clicked', async () => { + const mockTrackClick = vitest.fn(); + mockedTracking.mockReturnValue({ + trackClick: mockTrackClick, + }); + + setupSpecTest(); + + const changelogButton = screen.getByText(TradFr.mrc_changelog_changelog); + + await act(async () => { + fireEvent.click(changelogButton); + }); + + expect(mockTrackClick).toHaveBeenCalledWith({ + actionType: 'navigation', + actions: [...chapters, ...CHANGELOG_PREFIXES, GO_TO('changelog')], + }); + }); + + it('tracks click when roadmap link is clicked', async () => { + const mockTrackClick = vitest.fn(); + mockedTracking.mockReturnValue({ + trackClick: mockTrackClick, + }); + + setupSpecTest(); + + const roadmapButton = screen.getByText(TradFr.mrc_changelog_roadmap); + + await act(async () => { + fireEvent.click(roadmapButton); + }); + + expect(mockTrackClick).toHaveBeenCalledWith({ + actionType: 'navigation', + actions: [...chapters, ...CHANGELOG_PREFIXES, GO_TO('roadmap')], + }); + }); + + it('tracks click when feature request link is clicked', async () => { + const mockTrackClick = vitest.fn(); + mockedTracking.mockReturnValue({ + trackClick: mockTrackClick, + }); + + setupSpecTest(); + + const featureRequestButton = screen.getByText( + TradFr['mrc_changelog_feature-request'], + ); + + await act(async () => { + fireEvent.click(featureRequestButton); + }); + + expect(mockTrackClick).toHaveBeenCalledWith({ + actionType: 'navigation', + actions: [...chapters, ...CHANGELOG_PREFIXES, GO_TO('feature-request')], + }); + }); + + it('uses custom prefixes when provided', async () => { + const customPrefixes = ['custom-prefix-1', 'custom-prefix-2']; + const mockTrackClick = vitest.fn(); + mockedTracking.mockReturnValue({ + trackClick: mockTrackClick, + }); + + setupSpecTest({ prefixes: customPrefixes }); + + const changelogButton = screen.getByText(TradFr.mrc_changelog_changelog); + + await act(async () => { + fireEvent.click(changelogButton); + }); + + expect(mockTrackClick).toHaveBeenCalledWith({ + actionType: 'navigation', + actions: [...chapters, ...customPrefixes, GO_TO('changelog')], + }); + }); + }); + + describe('Accessibility', () => { + it('has external links with correct href attributes', () => { + setupSpecTest(); + + const changelogLink = screen + .getByText(TradFr.mrc_changelog_changelog) + .closest('a'); + const roadmapLink = screen + .getByText(TradFr.mrc_changelog_roadmap) + .closest('a'); + const featureRequestLink = screen + .getByText(TradFr['mrc_changelog_feature-request']) + .closest('a'); + + expect(changelogLink).toHaveAttribute('href', Links.changelog); + expect(roadmapLink).toHaveAttribute('href', Links.roadmap); + expect(featureRequestLink).toHaveAttribute( + 'href', + Links['feature-request'], + ); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.utils.ts b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.utils.ts new file mode 100644 index 000000000000..0ebcb9f412ab --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/ChangelogMenu.utils.ts @@ -0,0 +1,10 @@ +export const Links = { + roadmap: + 'https://github.com/orgs/ovh/projects/16/views/1?pane=info&sliceBy%5Bvalue%5D=Baremetal', + changelog: + 'https://github.com/orgs/ovh/projects/16/views/1?pane=info&sliceBy%5Bvalue%5D=Baremetal', + 'feature-request': + 'https://github.com/orgs/ovh/projects/16/views/16/views/1?pane=info&sliceBy%5Bvalue%5D=Baremetal', +}; + +export const chapters = ['baremetal', 'server', 'dedicated']; diff --git a/packages/manager-ui-kit/src/components/changelog-menu/__tests__/__snapshots__/ChangelogMenu.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/__snapshots__/ChangelogMenu.snapshot.test.tsx.snap new file mode 100644 index 000000000000..13cf02ca0d4e --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/__tests__/__snapshots__/ChangelogMenu.snapshot.test.tsx.snap @@ -0,0 +1,103 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with chapters prop 1`] = ` + +`; + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with custom prefixes 1`] = ` + +`; + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with default props 1`] = ` + +`; + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with different link URLs 1`] = ` + +`; + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with long URLs 1`] = ` + +`; + +exports[`ChangelogMenu Snapshot Tests > should match snapshot with single chapter 1`] = ` + +`; diff --git a/packages/manager-ui-kit/src/components/changelog-menu/index.ts b/packages/manager-ui-kit/src/components/changelog-menu/index.ts new file mode 100644 index 000000000000..209d7fd406a9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/index.ts @@ -0,0 +1,5 @@ +export { ChangelogMenu } from './ChangelogMenu.component'; +export type { + ChangelogMenuProps, + ChangelogMenuLinks, +} from './ChangelogMenu.props'; diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_de_DE.json new file mode 100644 index 000000000000..37111286f7e7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_de_DE.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap und Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature Request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_en_GB.json new file mode 100644 index 000000000000..065d273d7555 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_en_GB.json @@ -0,0 +1,7 @@ +{ + "user_account_guides_header": "Guides", + "mrc_changelog_header": "Roadmap & Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_es_ES.json new file mode 100644 index 000000000000..bd11bfff8ca6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_es_ES.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap & Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature Request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_CA.json new file mode 100644 index 000000000000..0dcad84fba08 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_CA.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap & Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..0dcad84fba08 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_fr_FR.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap & Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_it_IT.json new file mode 100644 index 000000000000..c98c03163740 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_it_IT.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap e Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature Request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pl_PL.json new file mode 100644 index 000000000000..c73fac699af2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pl_PL.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap & changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Propozycja wdrożenia nowej funkcji" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pt_PT.json new file mode 100644 index 000000000000..5a8caca3a4d0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/Messages_pt_PT.json @@ -0,0 +1,6 @@ +{ + "mrc_changelog_header": "Roadmap & Changelog", + "mrc_changelog_roadmap": "Roadmap", + "mrc_changelog_changelog": "Changelog", + "mrc_changelog_feature-request": "Feature request" +} diff --git a/packages/manager-ui-kit/src/components/changelog-menu/translations/translation.ts b/packages/manager-ui-kit/src/components/changelog-menu/translations/translation.ts new file mode 100644 index 000000000000..4d733d7b7ab8 --- /dev/null +++ b/packages/manager-ui-kit/src/components/changelog-menu/translations/translation.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'changelog-menu'); diff --git a/packages/manager-ui-kit/src/components/clipboard/Clipboard.component.tsx b/packages/manager-ui-kit/src/components/clipboard/Clipboard.component.tsx new file mode 100644 index 000000000000..ae108133e20d --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/Clipboard.component.tsx @@ -0,0 +1,31 @@ +import { + Clipboard as OdsClipboard, + ClipboardControl, + ClipboardTrigger, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import './translations'; +import { ClipboardProps } from './Clipboard.props'; + +export const Clipboard = ({ + loading = false, + masked = false, + ...others +}: ClipboardProps) => { + const { t } = useTranslation('clipboard'); + + return ( + + + + + ); +}; diff --git a/packages/manager-ui-kit/src/components/clipboard/Clipboard.props.tsx b/packages/manager-ui-kit/src/components/clipboard/Clipboard.props.tsx new file mode 100644 index 000000000000..605080e14567 --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/Clipboard.props.tsx @@ -0,0 +1,6 @@ +import { ClipboardProp } from '@ovhcloud/ods-react'; + +export type ClipboardProps = ClipboardProp & { + loading?: boolean; + masked?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.snapshot.test.tsx b/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.snapshot.test.tsx new file mode 100644 index 000000000000..03524eada63a --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.snapshot.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect, vitest } from 'vitest'; +import { render } from '@testing-library/react'; +import { Clipboard } from '../Clipboard.component'; + +describe('Clipboard Snapshot Tests', () => { + it('Displays Clipboard', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('Displays disabled Clipboard', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('Displays loading Clipboard', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('Displays masked Clipboard', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.spec.tsx b/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.spec.tsx new file mode 100644 index 000000000000..c4102a7e7053 --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/__tests__/Clipboard.spec.tsx @@ -0,0 +1,35 @@ +import { screen, render } from '@testing-library/react'; +import { Clipboard } from '../Clipboard.component'; + +const renderClipboardComponent = (props = {}) => + render(); + +describe('Clipboard', () => { + it('should render value', () => { + renderClipboardComponent(); + expect( + screen.getByTestId('clipboard').getElementsByTagName('input')[0], + ).toHaveValue('Value to copy to clipboard'); + }); + + it('should render disabled Clipboard', () => { + renderClipboardComponent({ disabled: true }); + expect( + screen.getByTestId('clipboard').getElementsByTagName('input')[0], + ).toBeDisabled(); + }); + + it('should render loading Clipboard', () => { + renderClipboardComponent({ loading: true }); + expect( + screen.getByTestId('clipboard').getElementsByTagName('span')[0], + ).toHaveRole('progressbar'); + }); + + it('should render masked Clipboard', () => { + renderClipboardComponent({ masked: true }); + expect( + screen.getByTestId('clipboard').getElementsByTagName('input')[0], + ).toHaveAttribute('type', 'password'); + }); +}); diff --git a/packages/manager-ui-kit/src/components/clipboard/__tests__/__snapshots__/Clipboard.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/clipboard/__tests__/__snapshots__/Clipboard.snapshot.test.tsx.snap new file mode 100644 index 000000000000..7158df8e3e2e --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/__tests__/__snapshots__/Clipboard.snapshot.test.tsx.snap @@ -0,0 +1,244 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Clipboard Snapshot Tests > Displays Clipboard 1`] = ` +
+
+
+ +
+
+ +
+
+
+`; + +exports[`Clipboard Snapshot Tests > Displays disabled Clipboard 1`] = ` +
+
+
+ +
+
+ +
+
+
+`; + +exports[`Clipboard Snapshot Tests > Displays loading Clipboard 1`] = ` +
+
+
+ +
+ + + + + + + + + + +
+
+
+ +
+
+
+`; + +exports[`Clipboard Snapshot Tests > Displays masked Clipboard 1`] = ` +
+
+
+ +
+ +
+
+
+ +
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/clipboard/index.ts b/packages/manager-ui-kit/src/components/clipboard/index.ts new file mode 100644 index 000000000000..4c7e7bd56100 --- /dev/null +++ b/packages/manager-ui-kit/src/components/clipboard/index.ts @@ -0,0 +1,3 @@ +export { Clipboard } from './Clipboard.component'; + +export type { ClipboardProps } from './Clipboard.props'; diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/clipboard/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/clipboard/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/clipboard/translations/index.ts b/packages/manager-ui-kit/src/components/clipboard/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/clipboard/translations/index.ts rename to packages/manager-ui-kit/src/components/clipboard/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/datagrid/Datagrid.component.tsx b/packages/manager-ui-kit/src/components/datagrid/Datagrid.component.tsx new file mode 100644 index 000000000000..ed0328c8fa6d --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/Datagrid.component.tsx @@ -0,0 +1,134 @@ +import { useRef } from 'react'; +import { Table } from '@ovhcloud/ods-react'; +import { TableHeaderContent } from './table/table-head/table-header-content/TableHeaderContent.component'; +import { TableFooter } from './table/table-footer/TableFooter.component'; +import { TableBody } from './table/table-body/TableBody.component'; +import { useDatagrid } from './useDatagrid'; +import { DatagridProps } from './Datagrid.props'; +import { Topbar } from './topbar/Topbar.component'; +import './translations'; + +const DEFAULT_ROW_HEIGHT = 50; +const DEFAULT_CONTAINER_HEIGHT = 570; + +export const Datagrid = >({ + autoScroll = true, + columns, + columnVisibility, + containerHeight, + contentAlignLeft = true, + data, + expandable, + filters, + hasNextPage, + isLoading, + maxRowHeight = DEFAULT_ROW_HEIGHT, + resourceType, + rowSelection, + search, + sorting, + subComponentHeight, + topbar, + totalCount, + onFetchAllPages, + onFetchNextPage, + renderSubComponent, +}: DatagridProps) => { + const { + features, + getHeaderGroups, + getRowModel, + getAllLeafColumns, + toggleAllColumnsVisible, + getIsAllColumnsVisible, + getIsSomeColumnsVisible, + } = useDatagrid({ + columns, + data, + sorting: sorting?.sorting, + onSortChange: sorting?.setSorting, + manualSorting: sorting?.manualSorting, + renderSubComponent, + columnVisibility: columnVisibility?.columnVisibility, + setColumnVisibility: columnVisibility?.setColumnVisibility, + rowSelection, + expandable, + }); + const { + hasSortingFeature, + hasSearchFeature, + hasColumnVisibilityFeature, + hasFilterFeature, + } = features; + const rowModel = getRowModel(); + const { rows } = rowModel; + const headerGroups = getHeaderGroups(); + const tableContainerRef = useRef(null); + const visibleColumns = getAllLeafColumns(); + const containerSize = + data?.length < 10 ? '100%' : `${DEFAULT_CONTAINER_HEIGHT}px`; + const containerStyle = { + maxHeight: containerHeight ? `${containerHeight}px` : containerSize, + height: containerHeight ? `${containerHeight}px` : containerSize, + }; + const shouldRenderTopbar = + topbar || + hasSearchFeature || + hasFilterFeature || + hasColumnVisibilityFeature; + return ( + <> + {shouldRenderTopbar && ( + + )} +
+ + + headerGroups={headerGroups} + onSortChange={sorting?.setSorting} + enableSorting={hasSortingFeature} + contentAlignLeft={contentAlignLeft} + /> + +
+
+ + + ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/Datagrid.props.ts b/packages/manager-ui-kit/src/components/datagrid/Datagrid.props.ts new file mode 100644 index 000000000000..726348a80dc0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/Datagrid.props.ts @@ -0,0 +1,117 @@ +import { MutableRefObject, ReactNode, Dispatch, SetStateAction } from 'react'; +import { + ColumnDef, + ColumnSort as TanstackColumnSort, + Row, + VisibilityState, + RowSelectionState, + SortingState, + ExpandedState, +} from '@tanstack/react-table'; +import { + FilterComparator, + FilterTypeCategories as DatagridColumnTypes, +} from '@ovh-ux/manager-core-api'; +import { Option } from '../filters'; +import { FilterWithLabel } from '../filters/Filter.props'; + +export interface RowSelectionProps { + onRowSelectionChange?: (selectedRows: T[]) => void; + rowSelection: RowSelectionState; + setRowSelection: Dispatch>; +} + +export interface SortingProps { + sorting: SortingState; + setSorting: (sorting: SortingState) => void; + manualSorting?: boolean; +} + +export interface ColumnVisibilityProps { + columnVisibility: VisibilityState; + setColumnVisibility: Dispatch>; +} + +export interface SearchProps { + onSearch: (search: string) => void; + placeholder?: string; + searchInput: string; + setSearchInput: Dispatch>; +} + +export type ColumnFilterProps = { + comparator: FilterComparator; + key: string; + label: string; + value: string | string[]; +}; + +export interface FilterProps { + add: (filters: ColumnFilterProps) => void; + filters: FilterWithLabel[]; + remove: (filter: FilterWithLabel) => void; +} + +export type ColumnSort = TanstackColumnSort; + +export interface ExpandedProps { + expanded: ExpandedState; + setExpanded: Dispatch>; +} + +export type DatagridProps> = { + autoScroll?: boolean; + columns: readonly DatagridColumn[]; + columnVisibility?: ColumnVisibilityProps; + containerHeight?: number; + contentAlignLeft?: boolean; + data: T[]; + expandable?: ExpandedProps; + filters?: FilterProps; + hasNextPage?: boolean; + isLoading?: boolean; + maxRowHeight?: number; + resourceType?: string; + rowSelection?: RowSelectionProps; + search?: SearchProps; + sorting?: SortingProps; + subComponentHeight?: number; + topbar?: ReactNode; + totalCount?: number; + onFetchAllPages?: () => void; + onFetchNextPage?: () => void; + renderSubComponent?: ( + row: Row, + headerRefs?: MutableRefObject>, + ) => JSX.Element; +}; + +export enum ColumnMetaType { + TEXT = 'text', + LINK = 'link', + BADGE = 'badge', +} + +// export type ManagerColumnDef = ColumnDef & { +export type DatagridColumn = ColumnDef & { + /** set column comparator for the filter */ + comparator?: FilterComparator[]; + /** Allows the column to be hidden or shown dynamically */ + enableHiding?: boolean; + /** filterOptions can be passed to have selector instead of input to choose value */ + filterOptions?: Option[]; + /** Trigger the column filter */ + isFilterable?: boolean; + /** Trigger the column search */ + isSearchable?: boolean; + /** Trigger the column sorting */ + isSortable?: boolean; + /** label displayed for the column in the table */ + label?: string; + meta?: { + type?: ColumnMetaType; + className?: string; + }; + /** Filters displayed for the column */ + type?: DatagridColumnTypes; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/__mocks__/index.tsx b/packages/manager-ui-kit/src/components/datagrid/__mocks__/index.tsx new file mode 100644 index 000000000000..828972ff662e --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/__mocks__/index.tsx @@ -0,0 +1,397 @@ +import { vi } from 'vitest'; +import { FilterTypeCategories } from '@ovh-ux/manager-core-api'; + +// Mock IAM hook response +export const mockIamResponse = { + isAuthorized: true, + isLoading: false, + isFetched: true, +}; + +// Common mock columns with different configurations +export const mockColumns = [ + { + id: 'name', + accessorKey: 'name', + label: 'Name', + cell: (props: any) =>
{props.getValue()}
, + isSearchable: true, + isSortable: true, + isFilterable: true, + enableHiding: true, + type: FilterTypeCategories.String, + }, + { + id: 'age', + accessorKey: 'age', + label: 'Age', + cell: (props: any) =>
{props.getValue()}
, + isSortable: true, + isFilterable: true, + enableHiding: true, + type: FilterTypeCategories.Numeric, + }, +]; + +// Basic mock columns (without search/filter features) +export const mockBasicColumns = [ + { + id: 'name', + accessorKey: 'name', + label: 'Name', + cell: (props: any) =>
{props.getValue()}
, + isSortable: true, + }, + { + id: 'age', + accessorKey: 'age', + label: 'Age', + cell: (props: any) =>
{props.getValue()}
, + isSortable: true, + }, +]; + +// Mock data +export const mockData = [ + { + name: 'John Doe', + age: 30, + subRows: [ + { + name: 'John Doe Jr.', + age: 10, + }, + { + name: 'John Doe Jr. 2', + age: 11, + }, + { + name: 'John Doe Jr. 3', + age: 12, + }, + ], + }, + { + name: 'Jane Smith', + age: 25, + }, +]; + +// Extended mock data for testing pagination +export const mockExtendedData = [ + { + name: 'Person 1', + age: 25, + }, + { + name: 'Person 2', + age: 26, + }, + { + name: 'Person 3', + age: 25, + }, + { + name: 'Person 4', + age: 27, + }, + { + name: 'Person 5', + age: 28, + }, +]; + +// Mock visible columns for topbar tests +export const mockVisibleColumns = [ + { + id: 'name', + columnDef: { header: 'Name', enableHiding: true }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, + { + id: 'age', + columnDef: { header: 'Age', enableHiding: true }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, +]; + +// Mock search props +export const mockSearch = { + onSearch: vi.fn(), + searchInput: 'test', + setSearchInput: vi.fn(), + placeholder: 'Search...', +}; + +// Mock search with empty input +export const mockEmptySearch = { + onSearch: vi.fn(), + searchInput: '', + setSearchInput: vi.fn(), + placeholder: 'Search users...', +}; + +// Mock filters +export const mockFilters = { + add: vi.fn(), + remove: vi.fn(), + filters: [], +}; + +// Mock filters with active data +export const mockFiltersWithData = { + add: vi.fn(), + remove: vi.fn(), + filters: [ + { + key: 'name', + label: 'Name', + value: 'John', + comparator: 'contains' as any, + }, + ], +}; + +// Mock filters with multiple active filters +export const mockFiltersWithMultipleData = { + add: vi.fn(), + remove: vi.fn(), + filters: [ + { + key: 'name', + label: 'Name', + value: 'John', + comparator: 'contains' as any, + }, + { + key: 'age', + label: 'Age', + value: '25', + comparator: 'equals' as any, + }, + ], +}; + +// Mock column visibility +export const mockColumnVisibility = { name: true, age: true }; +export const mockSetColumnVisibility = vi.fn(); + +// Mock sort change handler +export const mockOnSortChange = vi.fn(); + +// Mock render sub component +export const mockRenderSubComponent = vi.fn(() =>
Sub content
); + +// Mock row selection +export const mockRowSelection = { + rowSelection: {}, + setRowSelection: vi.fn(), + onRowSelectionChange: vi.fn(), +}; + +// Mock pagination handlers +export const mockOnFetchNextPage = vi.fn(); +export const mockOnFetchAllPages = vi.fn(); + +// Mock column visibility functions +export const mockGetIsAllColumnsVisible = vi.fn(() => true); +export const mockGetIsSomeColumnsVisible = vi.fn(() => false); +export const mockToggleAllColumnsVisible = vi.fn(); + +// Mock partial column visibility (some columns hidden) +export const mockPartialVisibleColumns = [ + { + id: 'name', + columnDef: { header: 'Name', enableHiding: true }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, + { + id: 'age', + columnDef: { header: 'Age', enableHiding: true }, + getIsVisible: vi.fn(() => false), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, +]; + +// Mock columns without search feature +export const mockColumnsWithoutSearch = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: false, + isFilterable: true, + enableHiding: true, + }, +]; + +// Mock columns without filter feature +export const mockColumnsWithoutFilter = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: true, + isFilterable: false, + enableHiding: true, + }, +]; + +// Mock columns without visibility feature +export const mockColumnsWithoutVisibility = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: true, + isFilterable: true, + enableHiding: false, + }, +]; + +// Mock visible columns without hiding capability +export const mockVisibleColumnsWithoutHiding = [ + { + id: 'name', + columnDef: { header: 'Name', enableHiding: false }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => false), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, +]; + +// Mock sorting states +export const mockSortingAsc = [{ id: 'name', desc: false }]; +export const mockSortingDesc = [{ id: 'age', desc: true }]; +export const mockEmptySorting: any[] = []; + +// Mock virtualizer for TableBody tests +export const mockVirtualizer = { + getTotalSize: () => 100, + getVirtualItems: () => [ + { index: 0, key: 0, start: 0, size: 50 }, + { index: 1, key: 1, start: 50, size: 50 }, + ], + measureElement: () => {}, + overscan: 40, + scrollToIndex: vi.fn(), +}; + +// Mock table container ref +export const mockTableContainerRef = { + current: document.createElement('div'), +}; + +// Mock row model +export const mockRowModel = { + rows: [ + { + id: '0', + getVisibleCells: () => [ + { + id: 'name-0', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto' }, + }, + }, + { + id: 'age-0', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto' }, + }, + }, + ], + getIsExpanded: () => false, + }, + { + id: '1', + getVisibleCells: () => [ + { + id: 'name-1', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto' }, + }, + }, + { + id: 'age-1', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto' }, + }, + }, + ], + getIsExpanded: () => false, + }, + ], +}; + +// Mock header groups +export const mockHeaderGroups: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, +]; + +// Mock virtual row for SubRow tests +export const mockVirtualRow = { + index: 0, + key: 0, + start: 0, + size: 50, +}; + +// Mock row for SubRow tests +export const mockRow = { + id: '0', + getVisibleCells: () => [], + getIsExpanded: () => true, +}; + +// Mock columns for LoadingRow tests +export const mockLoadingColumns = [ + { + id: 'name', + getIsVisible: () => true, + }, + { + id: 'age', + getIsVisible: () => true, + }, +] as any[]; diff --git a/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.snapshot.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.snapshot.spec.tsx new file mode 100644 index 000000000000..5d3ddfbef23a --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.snapshot.spec.tsx @@ -0,0 +1,189 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent, act } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { Datagrid } from '../Datagrid.component'; +import { useAuthorizationIam } from '../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; +import { + mockIamResponse, + mockBasicColumns, + mockData, + mockOnSortChange, + mockSearch, + mockFilters, + mockColumnVisibility, + mockSetColumnVisibility, + mockRenderSubComponent, + mockRowSelection, + mockOnFetchNextPage, + mockOnFetchAllPages, +} from '../__mocks__'; + +vi.mock('../../../hooks/iam'); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +describe('Datagrid Snapshot Tests', () => { + beforeEach(() => { + mockedHook.mockReturnValue(mockIamResponse); + }); + + it('should match snapshot with basic props', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with sorting enabled', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with search enabled', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with filters enabled', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with column visibility enabled', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with pagination', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with loading state', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with empty data', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with expandable rows', () => { + const { container } = render( + , + ); + const expandableButton = screen.getAllByTestId('manager-button'); + expect(expandableButton[0]).toBeInTheDocument(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with row selection', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with custom container height', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with all features enabled', () => { + const { container } = render( + , + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.spec.tsx new file mode 100644 index 000000000000..ae20d4143812 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/__tests__/Datagrid.spec.tsx @@ -0,0 +1,431 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { Datagrid } from '../Datagrid.component'; +import { useAuthorizationIam } from '../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; +import { + mockIamResponse, + mockBasicColumns, + mockColumns, + mockData, + mockOnSortChange, + mockSearch, + mockFilters, + mockColumnVisibility, + mockSetColumnVisibility, + mockRenderSubComponent, + mockRowSelection, + mockOnFetchNextPage, + mockOnFetchAllPages, +} from '../__mocks__'; + +vi.mock('../../../hooks/iam'); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +const virtualWindowStart = 0; +const virtualWindowSize = 20; + +const scrollToIndexMock = vi.fn(); +vi.mock('@tanstack/react-virtual', () => ({ + useVirtualizer: ({ count }: { count: number }) => ({ + getTotalSize: () => count * 50, + getVirtualItems: () => { + const endIndex = Math.min(virtualWindowStart + virtualWindowSize, count); + const actualStart = Math.max(0, virtualWindowStart); + const actualCount = Math.max(0, endIndex - actualStart); + + return Array.from({ length: actualCount }).map((_, i) => { + const index = actualStart + i; + return { + index, + key: index, + start: index * 50, + size: 50, + }; + }); + }, + measureElement: () => {}, + overscan: 40, + scrollToIndex: scrollToIndexMock, + }), +})); + +describe('Datagrid', () => { + beforeEach(() => { + mockedHook.mockReturnValue(mockIamResponse); + vi.clearAllMocks(); + }); + + describe('Basic Rendering', () => { + it('should render with data and headers', () => { + render(); + + expect(screen.getByText('name')).toBeInTheDocument(); + expect(screen.getByText('age')).toBeInTheDocument(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('30')).toBeInTheDocument(); + }); + + it('should render with empty data', () => { + render(); + + expect(screen.getByText('name')).toBeInTheDocument(); + expect(screen.getByText('Aucun résultat')).toBeInTheDocument(); + }); + + it('should apply container height styles', () => { + const { container } = render( + , + ); + const tableContainer = container.querySelector( + '.overflow-auto.relative.w-full', + ); + expect(tableContainer).toHaveStyle('height: 400px'); + }); + }); + + describe('Topbar Features', () => { + it('should render custom topbar', () => { + const customTopbar =
Custom Topbar
; + render( + , + ); + + expect(screen.getByTestId('custom-topbar')).toBeInTheDocument(); + }); + + it('should render search when enabled', () => { + render( + , + ); + + expect(screen.getByTestId('topbar-container')).toBeInTheDocument(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + }); + + it('should render filters when enabled', () => { + render( + , + ); + + expect(screen.getByTestId('topbar-container')).toBeInTheDocument(); + expect(screen.getByTestId('datagrid-topbar-filters')).toBeInTheDocument(); + }); + + it('should render column visibility when enabled', () => { + render( + , + ); + + expect(screen.getByTestId('topbar-container')).toBeInTheDocument(); + expect(screen.getByText('Colonnes')).toBeInTheDocument(); + }); + + it('should not render topbar when no features enabled', () => { + render(); + + expect(screen.queryByTestId('topbar-container')).not.toBeInTheDocument(); + }); + }); + + describe('Sorting', () => { + it('should handle sorting with setSorting', () => { + render( + , + ); + const NameHeaderButton = screen.getByText('name'); + expect(NameHeaderButton).toBeInTheDocument(); + fireEvent.click(NameHeaderButton); + expect(mockOnSortChange).toHaveBeenCalledWith([ + { id: 'name', desc: true }, + ]); + }); + + it('should handle manual sorting', () => { + render( + , + ); + const NameHeaderButton = screen.getByText('name'); + expect(NameHeaderButton).toBeInTheDocument(); + fireEvent.click(NameHeaderButton); + }); + }); + + describe('Search & Filter Interactions', () => { + it('should handle search input changes', () => { + render( + , + ); + const searchInput = screen.getByRole('searchbox'); + fireEvent.change(searchInput, { target: { value: 'new search' } }); + expect(mockSearch.setSearchInput).toHaveBeenCalledWith('new search'); + }); + + it('should handle search form submission', () => { + render( + , + ); + + const form = screen.getByRole('searchbox').closest('form'); + fireEvent.submit(form!); + + expect(mockSearch.onSearch).toHaveBeenCalledWith('test'); + }); + + it('should render filter list when filters are active', () => { + const filtersWithData = { + ...mockFilters, + filters: [ + { + key: 'name', + label: 'name', + value: 'John', + comparator: 'contains' as any, + }, + ], + }; + + render( + , + ); + expect(screen.getByTestId('datagrid-filter-list')).toBeInTheDocument(); + }); + }); + + describe('Pagination & Actions', () => { + it('should render pagination buttons when hasNextPage is true', () => { + render( + , + ); + + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + }); + + it('should call pagination handlers when buttons are clicked', () => { + render( + , + ); + + fireEvent.click(screen.getByText('Charger plus')); + fireEvent.click(screen.getByText('Charger tout')); + + expect(mockOnFetchNextPage).toHaveBeenCalledTimes(1); + expect(mockOnFetchAllPages).toHaveBeenCalledTimes(1); + }); + + it('should disable pagination buttons when loading', () => { + render( + , + ); + expect(screen.getByText('Charger plus')).toBeDisabled(); + expect(screen.getByText('Charger tout')).toBeDisabled(); + }); + }); + + describe('Footer & Results', () => { + it('should render footer with items count and total count', () => { + render( + , + ); + expect(screen.getByText('2 sur 100 résultats')).toBeInTheDocument(); + }); + + it('should render footer with only items count when totalCount is provided', () => { + render( + , + ); + + expect(screen.getByText('2 sur 100 résultats')).toBeInTheDocument(); + }); + + it('should render footer with zero items count', () => { + render( + , + ); + expect(screen.getByText('0 sur 100 résultats')).toBeInTheDocument(); + }); + }); + + describe('Advanced Features', () => { + it('should handle row selection', () => { + const { container } = render( + , + ); + const targetDiv = container.querySelector('thead tr th label'); + expect(targetDiv).toBeInTheDocument(); + expect(targetDiv).toHaveAttribute('id', 'checkbox:select-all'); + const targetDivBody = container.querySelector('tbody tr td label'); + expect(targetDivBody).toBeInTheDocument(); + expect(targetDivBody).toHaveAttribute('id', 'checkbox:0'); + }); + + it('should handle expandable rows', () => { + const { container } = render( + , + ); + const targetDiv = container.querySelector('tbody tr td div button span'); + expect(targetDiv).toBeInTheDocument(); + expect(targetDiv?.className).toContain('chevron-right'); + }); + + it('should handle loading state', () => { + const { container } = render( + , + ); + // expect td that contains skeleton in the class + const targetDiv = container.querySelector('tbody tr td div div'); + expect(targetDiv).toBeInTheDocument(); + expect(targetDiv?.className).toContain('skeleton'); + }); + + it('should handle content alignment left', () => { + render( + , + ); + expect(screen.getByText('name')).toHaveClass('pl-4'); + }); + + it('should handle content alignment center', () => { + render( + , + ); + expect(screen.getByText('name')).toHaveClass('text-center'); + }); + }); + + describe('All Features Combined', () => { + it('should render with all features enabled', () => { + const customTopbar =
Custom Topbar
; + + render( + , + ); + + // Check all features are rendered + expect(screen.getByTestId('custom-topbar')).toBeInTheDocument(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + expect(screen.getByTestId('datagrid-topbar-filters')).toBeInTheDocument(); + expect(screen.getByText('Colonnes')).toBeInTheDocument(); + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + expect(screen.getByText('2 sur 100 résultats')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/__tests__/__snapshots__/Datagrid.snapshot.spec.tsx.snap b/packages/manager-ui-kit/src/components/datagrid/__tests__/__snapshots__/Datagrid.snapshot.spec.tsx.snap new file mode 100644 index 000000000000..b8e3d0e110b1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/__tests__/__snapshots__/Datagrid.snapshot.spec.tsx.snap @@ -0,0 +1,2055 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Datagrid Snapshot Tests > should match snapshot with all features enabled 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+ + + + + +
+
+
+ + name + + + + +
+
+
+ + age + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with basic props 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with column visibility enabled 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with custom container height 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with empty data 1`] = ` +
+ + + + + + + + + + + + +
+ name + + age +
+

+ Aucun résultat +

+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with expandable rows 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + name + + age +
+
+
+ +
+
+
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+
+
+
+
+ John Doe Jr. +
+
+
+
+
+ 10 +
+
+
+
+
+
+
+
+
+ John Doe Jr. 2 +
+
+
+
+
+ 11 +
+
+
+
+
+
+
+
+
+ John Doe Jr. 3 +
+
+
+
+
+ 12 +
+
+
+
+
+ +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with filters enabled 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with loading state 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with pagination 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with row selection 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + +
+ + + name + + age +
+
+ +
+
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+ +
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with search enabled 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+ name + + age +
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+`; + +exports[`Datagrid Snapshot Tests > should match snapshot with sorting enabled 1`] = ` +
+ + + + + + + + + + + + + + + + + +
+
+ + name + + + + +
+
+
+ + age + + + +
+
+
+
+ Jane Smith +
+
+
+
+
+ 25 +
+
+
+
+
+ John Doe +
+
+
+
+
+ 30 +
+
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/datagrid/builder/TableBuilderProps.props.ts b/packages/manager-ui-kit/src/components/datagrid/builder/TableBuilderProps.props.ts new file mode 100644 index 000000000000..f35efe47b7e7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/builder/TableBuilderProps.props.ts @@ -0,0 +1,27 @@ +import { MutableRefObject } from 'react'; +import { + ColumnDef, + ColumnSort, + Row, + VisibilityState, +} from '@tanstack/react-table'; +import { ExpandedProps, RowSelectionProps } from '../Datagrid.props'; +import { ExpandableRow } from '../useDatagrid.props'; + +export type TableBuilderProps> = { + columns: readonly ColumnDef[]; + columnVisibility: VisibilityState; + data: T[]; + expandable: ExpandedProps; + hasExpandableFeature: boolean; + hasSortingFeature: boolean; + manualSorting: boolean; + onSortChange: (sorting: ColumnSort[]) => void; + renderSubComponent: ( + row: Row, + headerRefs?: MutableRefObject>, + ) => JSX.Element; + rowSelection: RowSelectionProps; + setColumnVisibility: (columnVisibility: VisibilityState) => void; + sorting: ColumnSort[]; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/builder/TableHeaderBuilder.tsx b/packages/manager-ui-kit/src/components/datagrid/builder/TableHeaderBuilder.tsx new file mode 100644 index 000000000000..401bfe77fe6b --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/builder/TableHeaderBuilder.tsx @@ -0,0 +1,74 @@ +import { Row } from '@tanstack/react-table'; +import { + BUTTON_SIZE, + BUTTON_VARIANT, + Checkbox, + CheckboxControl, + Icon, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import { Button } from '../../button'; +import { ExpandedProps } from '../Datagrid.props'; + +export const getExpandable = (expandable: ExpandedProps) => ({ + cell: ({ row }: { row: Row }) => { + return row.getCanExpand() ? ( +
+ {expandable && row.depth ? null : ( + + )} +
+ ) : null; + }, + enableHiding: false, + enableResizing: true, + id: 'expander', + maxSize: 20, +}); + +export const getRowSelection = () => ({ + cell: ({ row }: { row: Row }) => ( + row.toggleSelected()} + checked={row.getIsSelected()} + disabled={!row.getCanSelect()} + > + + + ), + enableHiding: true, + enableResizing: true, + header: ({ table: tableHeader }) => ( + { + tableHeader.toggleAllRowsSelected(); + }} + checked={tableHeader.getIsAllRowsSelected()} + > + + + ), + id: 'select', + maxSize: 20, +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/builder/index.ts b/packages/manager-ui-kit/src/components/datagrid/builder/index.ts new file mode 100644 index 000000000000..0dd14cf7b7fc --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/builder/index.ts @@ -0,0 +1,2 @@ +export { useTableBuilder } from './useTableBuilder'; +export type { TableBuilderProps } from './TableBuilderProps.props'; diff --git a/packages/manager-ui-kit/src/components/datagrid/builder/useTableBuilder.ts b/packages/manager-ui-kit/src/components/datagrid/builder/useTableBuilder.ts new file mode 100644 index 000000000000..4c52f3139e74 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/builder/useTableBuilder.ts @@ -0,0 +1,108 @@ +import { + getCoreRowModel, + getExpandedRowModel, + getSortedRowModel, + TableOptions, +} from '@tanstack/react-table'; +import { getExpandable, getRowSelection } from './TableHeaderBuilder'; +import { TableBuilderProps } from './TableBuilderProps.props'; +import { ExpandableRow } from '../useDatagrid.props'; + +export const useTableBuilder = >({ + columns, + columnVisibility, + data, + expandable, + hasExpandableFeature, + hasSortingFeature, + manualSorting, + onSortChange, + renderSubComponent, + rowSelection, + setColumnVisibility, + sorting, +}: TableBuilderProps) => { + const params: Partial> = {}; + const builder = { + build: () => params as TableOptions, + setColumns: () => { + let cols = [...columns]; + if (rowSelection) { + cols = [getRowSelection(), ...cols]; + } + if ((hasExpandableFeature && expandable) || renderSubComponent) { + cols = [getExpandable(expandable), ...cols]; + } + params.columns = cols; + return builder; + }, + setColumnsVisibility: () => { + if (columnVisibility && setColumnVisibility) { + params.onColumnVisibilityChange = (updaterOrValue) => { + if (typeof updaterOrValue === 'function') { + setColumnVisibility(updaterOrValue(columnVisibility)); + } else { + setColumnVisibility(updaterOrValue); + } + }; + } + return builder; + }, + setCoreRowModel: () => { + params.getCoreRowModel = getCoreRowModel(); + return builder; + }, + setData: () => { + params.data = data; + return builder; + }, + setExpanded: () => { + if (hasExpandableFeature && expandable?.setExpanded) { + params.onExpandedChange = expandable.setExpanded; + } + return builder; + }, + setExpandedRowModel: () => { + if (hasExpandableFeature || renderSubComponent) { + params.getRowCanExpand = () => true; + params.getExpandedRowModel = getExpandedRowModel(); + } + return builder; + }, + setRowSelection: () => { + if (rowSelection) { + params.enableRowSelection = true; + params.onRowSelectionChange = rowSelection.setRowSelection; + } + return builder; + }, + setSorting: () => { + if (hasSortingFeature && onSortChange) { + params.onSortingChange = (updaterOrValue) => { + if (typeof updaterOrValue === 'function') { + onSortChange(updaterOrValue(sorting || [])); + } else { + onSortChange(updaterOrValue); + } + }; + params.getSortedRowModel = getSortedRowModel(); + params.manualSorting = manualSorting; + } + return builder; + }, + setState: () => { + params.state = { + ...(hasSortingFeature && onSortChange && { sorting }), + ...(columnVisibility && setColumnVisibility && { columnVisibility }), + ...(rowSelection && { rowSelection: rowSelection.rowSelection }), + ...(hasExpandableFeature && { expanded: expandable?.expanded ?? {} }), + }; + return builder; + }, + setSubRows: () => { + params.getSubRows = (row: T) => row?.subRows; + return builder; + }, + }; + return builder; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/index.ts b/packages/manager-ui-kit/src/components/datagrid/index.ts new file mode 100644 index 000000000000..43803c0dc4df --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/index.ts @@ -0,0 +1,8 @@ +export { Datagrid } from './Datagrid.component'; +export { CellRow } from './table/table-body/cell-row/CellRow.component'; +export { ColumnMetaType } from './Datagrid.props'; +export type { + DatagridProps, + DatagridColumn, + ColumnSort, +} from './Datagrid.props'; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.component.tsx new file mode 100644 index 000000000000..8dcf9cd09d1a --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.component.tsx @@ -0,0 +1,132 @@ +import { useEffect, useCallback, Fragment, useMemo } from 'react'; +import { Row, flexRender } from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { TableBodyProps } from './TableBody.props'; +import { EmptyRow } from './empty-row/EmptyRow.component'; +import { usePrevious } from './usePrevious'; +import { SubRowMemo } from './sub-row/SubRow.component'; +import { LoadingRow } from './loading-row/LoadingRow.component'; + +export const TableBody = ({ + autoScroll = true, + columns, + isLoading, + maxRowHeight, + expanded, + pageSize = 10, + renderSubComponent, + rowModel, + subComponentHeight = 50, + tableContainerRef, + contentAlignLeft = true, +}: TableBodyProps) => { + const { rows } = rowModel; + const previousRowsLength = usePrevious(rows?.length); + const rowVirtualizer = useVirtualizer({ + count: rows.length, + estimateSize: useCallback(() => maxRowHeight, [maxRowHeight]), + getScrollElement: () => tableContainerRef.current, + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? (el) => el?.getBoundingClientRect().height + : undefined, + overscan: 15, + }); + + useEffect(() => { + if ( + autoScroll && + previousRowsLength !== undefined && + rows?.length > previousRowsLength + ) { + rowVirtualizer.scrollToIndex(previousRowsLength, { align: 'start' }); + } + }, [rows?.length]); + + const EXPANDED_SIZE = subComponentHeight; + const getOffset = useCallback( + (index: number) => { + let count = 0; + for (let i = 0; i < index; i += 1) { + if (rows[i]?.getIsExpanded()) count += 1; + } + return count * EXPANDED_SIZE; + }, + [rows], + ); + + const totalHeight = useMemo(() => { + const total = rows.reduce((acc, row) => { + return ( + acc + maxRowHeight + (row.getIsExpanded() ? subComponentHeight : 0) + ); + }, 0); + return isLoading + ? total + (pageSize - 1) * maxRowHeight + : total + rows.length; + }, [rows, maxRowHeight, subComponentHeight, isLoading, expanded]); + + if (rows?.length === 0 && !isLoading) { + return ; + } + + return ( + + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow?.index] as Row; + const offset = renderSubComponent ? getOffset(virtualRow?.index) : 0; + return ( + + + {row.getVisibleCells().map((cell) => ( + +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ + ))} + + {row.getIsExpanded() && renderSubComponent && ( + + )} +
+ ); + })} + {isLoading && } + + ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.props.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.props.ts new file mode 100644 index 000000000000..087afdeab277 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/TableBody.props.ts @@ -0,0 +1,19 @@ +import { RefObject, MutableRefObject } from 'react'; +import { RowModel, Row, ExpandedState, Column } from '@tanstack/react-table'; + +export type TableBodyProps = { + autoScroll?: boolean; + isLoading: boolean; + maxRowHeight: number; + pageSize?: number; + renderSubComponent?: ( + row: Row, + headerRefs?: MutableRefObject>, + ) => JSX.Element; + rowModel: RowModel; + subComponentHeight?: number; + tableContainerRef: RefObject; + contentAlignLeft?: boolean; + expanded?: ExpandedState; + columns: Column[]; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/__tests__/TableBody.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/__tests__/TableBody.spec.tsx new file mode 100644 index 000000000000..a1b2b176b5fd --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/__tests__/TableBody.spec.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { Datagrid } from '../../../Datagrid.component'; +import { useAuthorizationIam } from '../../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../../hooks/iam/iam.interface'; +import { + mockIamResponse, + mockBasicColumns, + mockExtendedData, +} from '../../../__mocks__'; + +vi.mock('../../../../../hooks/iam'); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +let virtualWindowStart = 0; +let virtualWindowSize = 20; + +const setVirtualWindow = (start: number, size = 20) => { + virtualWindowStart = start; + virtualWindowSize = size; +}; + +const scrollToIndexMock = vi.fn(); + +vi.mock('@tanstack/react-virtual', () => ({ + useVirtualizer: ({ count }: { count: number }) => ({ + getTotalSize: () => count * 50, + getVirtualItems: () => { + const endIndex = Math.min(virtualWindowStart + virtualWindowSize, count); + const actualStart = Math.max(0, virtualWindowStart); + const actualCount = Math.max(0, endIndex - actualStart); + + return Array.from({ length: actualCount }).map((_, i) => { + const index = actualStart + i; + return { + index, + key: index, + start: index * 50, + size: 50, + }; + }); + }, + measureElement: () => {}, + overscan: 40, + scrollToIndex: scrollToIndexMock, + }), +})); + +const Wrapper = () => { + const [items, setItems] = useState(mockExtendedData); + const [isFetchAllPages, setIsFetchAllPages] = useState(false); + const onFetchNextPage = () => { + setItems([ + ...items, + { + name: `Person ${items.length + 1}`, + age: items.length + 1, + }, + ]); + }; + const onFetchAllPages = () => { + const newData = Array.from({ length: 200 }, (_, index) => ({ + name: `Person ${index}`, + age: index + 1, + })); + setItems(newData); + setIsFetchAllPages(true); + }; + + return ( + + ); +}; + +describe('TableBodyComponent', () => { + beforeEach(() => { + mockedHook.mockReturnValue(mockIamResponse); + }); + it('should render the table body', () => { + render( + , + ); + expect(screen.getByText('Person 1')).toBeInTheDocument(); + expect(screen.getByText('Person 2')).toBeInTheDocument(); + expect(screen.getByText('Person 3')).toBeInTheDocument(); + expect(screen.getByText('Person 4')).toBeInTheDocument(); + expect(screen.getByText('Person 5')).toBeInTheDocument(); + }); + + it('should render the table body, Charger plus and display next items', () => { + render(); + expect(screen.getByText('Person 1')).toBeInTheDocument(); + expect(screen.getByText('Person 2')).toBeInTheDocument(); + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + const loadMoreButton = screen.getByText('Charger plus'); + fireEvent.click(loadMoreButton); + expect(screen.getByText('Person 6')).toBeInTheDocument(); + fireEvent.click(loadMoreButton); + fireEvent.click(loadMoreButton); + expect(screen.getByText('Person 7')).toBeInTheDocument(); + expect(screen.getByText('Person 8')).toBeInTheDocument(); + }); + + it('should test virtualization with scrollable window', () => { + // Start with first 20 items visible + setVirtualWindow(0, 20); + const { container } = render(); + + // Charger tout data (200 items) + const loadAllButton = screen.getByText('Charger tout'); + fireEvent.click(loadAllButton); + + // Should see first 20 items (Person 0-19) + expect(screen.getByText('Person 0')).toBeInTheDocument(); + expect(screen.getByText('Person 19')).toBeInTheDocument(); + // Should NOT see items beyond the virtual window + expect(screen.queryByText('Person 20')).not.toBeInTheDocument(); + expect(screen.queryByText('Person 199')).not.toBeInTheDocument(); + + // Verify only 20 rows are rendered + expect(container.querySelectorAll('tr').length).toBe(21); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/cell-row/CellRow.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/cell-row/CellRow.component.tsx new file mode 100644 index 000000000000..b00f17233598 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/cell-row/CellRow.component.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { BADGE_COLOR } from '@ovhcloud/ods-react'; +import { Badge } from '../../../../badge'; +import { Link } from '../../../../Link'; +import { Text } from '../../../../text'; +import { ColumnMetaType } from '../../../Datagrid.props'; + +export const CellRow = ({ + badgeColor, + children, + className, + href, + onClick, + type, +}: { + badgeColor?: BADGE_COLOR; + children?: React.ReactNode; + className?: string; + href?: string; + onClick?: () => void; + type: ColumnMetaType; +}) => { + if (type === 'text') { + return {children}; + } + if (type === 'link') { + return ( + + {children as string} + + ); + } + if (type === 'badge') { + return ( + + {children} + + ); + } + return className ? ( +
{children}
+ ) : ( + <>{children} + ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/empty-row/EmptyRow.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/empty-row/EmptyRow.component.tsx new file mode 100644 index 000000000000..a59ae3e2ed90 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/empty-row/EmptyRow.component.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from 'react-i18next'; +import { Text } from '../../../../text'; + +export const EmptyRow = () => { + const { t } = useTranslation(['datagrid']); + return ( + + + + {t('common_pagination_no_results')} + + + + ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.component.tsx new file mode 100644 index 000000000000..86a1e50fc436 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.component.tsx @@ -0,0 +1,32 @@ +import { memo } from 'react'; +import { Skeleton } from '@ovhcloud/ods-react'; +import { LoadingRowProps } from './LoadingRow.props'; + +const LoadingRowComponent = ({ + columns, + pageSize = 10, +}: LoadingRowProps) => ( + <> + {Array.from({ length: pageSize }).map((_, index) => ( + + {columns?.map( + (col, idx) => + col.getIsVisible() && ( + +
+ +
+ + ), + )} + + ))} + +); + +export const LoadingRow = memo(LoadingRowComponent) as ( + props: LoadingRowProps, +) => JSX.Element | null; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.props.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.props.ts new file mode 100644 index 000000000000..617743774f5c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/loading-row/LoadingRow.props.ts @@ -0,0 +1,6 @@ +import { Column } from '@tanstack/react-table'; + +export type LoadingRowProps = { + columns: Column[]; + pageSize?: number; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.component.tsx new file mode 100644 index 000000000000..7ba293bdfbc6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.component.tsx @@ -0,0 +1,32 @@ +import { memo } from 'react'; +import { SubRowProps } from './SubRow.props'; + +export const SubRow = ({ + maxRowHeight, + offset, + renderSubComponent, + row, + subComponentHeight, + virtualRow, +}: SubRowProps) => ( + +
+ {renderSubComponent(row)} +
+ +); + +export const SubRowMemo = memo(SubRow); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.props.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.props.ts new file mode 100644 index 000000000000..eb6c1edf1cd6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/sub-row/SubRow.props.ts @@ -0,0 +1,10 @@ +import { VirtualItem } from '@tanstack/react-virtual'; + +export type SubRowProps = { + maxRowHeight: number; + offset: number; + renderSubComponent?: any; + row: any; + subComponentHeight: number; + virtualRow: VirtualItem; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-body/usePrevious.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-body/usePrevious.ts new file mode 100644 index 000000000000..4dd054f27065 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-body/usePrevious.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +export const usePrevious = (value: number) => { + const ref = useRef(0); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-footer/TableFooter.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/TableFooter.component.tsx new file mode 100644 index 000000000000..6f1ed871b34c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/TableFooter.component.tsx @@ -0,0 +1,42 @@ +import { FooterInfos } from './footer-infos/FooterInfos.component'; +import { FooterActions } from './footer-actions/FooterActions.component'; + +export const TableFooter = ({ + hasNextPage, + isLoading, + itemsCount, + onFetchAllPages, + onFetchNextPage, + totalCount, +}: { + hasNextPage?: boolean; + isLoading?: boolean; + itemsCount?: number; + onFetchAllPages?: () => void; + onFetchNextPage?: () => void; + totalCount?: number; +}) => { + if (!onFetchAllPages && !onFetchNextPage && !totalCount) { + return null; + } + return ( +
+
+
+ {(onFetchAllPages || onFetchNextPage) && ( + + )} +
+
+ {totalCount && ( + + )} +
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-footer/__tests__/TableFooter.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/__tests__/TableFooter.spec.tsx new file mode 100644 index 000000000000..62d573d19281 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/__tests__/TableFooter.spec.tsx @@ -0,0 +1,264 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { TableFooter } from '../TableFooter.component'; +import { useAuthorizationIam } from '../../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../../hooks/iam/iam.interface'; + +vi.mock('../../../../../hooks/iam'); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +describe('TableFooter', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + it('should render the footer with basic props', () => { + render( + , + ); + + // Should render footer info + expect(screen.getByText('10 sur 100 résultats')).toBeInTheDocument(); + + // Should not render pagination buttons when hasNextPage is false + expect(screen.queryByText('Charger plus')).not.toBeInTheDocument(); + expect(screen.queryByText('Charger tout')).not.toBeInTheDocument(); + }); + + it('should render pagination buttons when hasNextPage is true', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + }); + + it('should render only Charger plus button when onFetchAllPages is not provided', () => { + const mockOnFetchNextPage = vi.fn(); + + render( + , + ); + + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.queryByText('Charger tout')).not.toBeInTheDocument(); + }); + + it('should render only Charger tout button when onFetchNextPage is not provided', () => { + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + expect(screen.queryByText('Charger plus')).not.toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + }); + + it('should call onFetchNextPage when Charger plus button is clicked', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + const loadMoreButton = screen.getByText('Charger plus'); + fireEvent.click(loadMoreButton); + + expect(mockOnFetchNextPage).toHaveBeenCalledTimes(1); + }); + + it('should call onFetchAllPages when Charger tout button is clicked', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + const loadAllButton = screen.getByText('Charger tout'); + fireEvent.click(loadAllButton); + + expect(mockOnFetchAllPages).toHaveBeenCalledTimes(1); + }); + + it('should disable buttons when isLoading is true', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + const loadMoreButton = screen.getByText('Charger plus'); + const loadAllButton = screen.getByText('Charger tout'); + + expect(loadMoreButton).toBeDisabled(); + expect(loadAllButton).toBeDisabled(); + }); + + it('should render footer info with only itemsCount when totalCount is not provided', () => { + render( + , + ); + + expect(screen.getByText('5 sur 5 résultats')).toBeInTheDocument(); + }); + + it('should render footer info with itemsCount and totalCount', () => { + render( + , + ); + + expect(screen.getByText('25 sur 150 résultats')).toBeInTheDocument(); + }); + + it('should render footer info with zero itemsCount', () => { + render( + , + ); + + expect(screen.getByText('0 sur 100 résultats')).toBeInTheDocument(); + }); + + it('should not render pagination buttons when hasNextPage is false', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + expect(screen.queryByText('Charger plus')).not.toBeInTheDocument(); + expect(screen.queryByText('Charger tout')).not.toBeInTheDocument(); + }); + + it('should render with undefined props', () => { + render(); + + // Should not render pagination buttons + expect(screen.queryByText('Charger plus')).not.toBeInTheDocument(); + expect(screen.queryByText('Charger tout')).not.toBeInTheDocument(); + }); + + it('should handle multiple clicks on pagination buttons', () => { + const mockOnFetchNextPage = vi.fn(); + const mockOnFetchAllPages = vi.fn(); + + render( + , + ); + + const loadMoreButton = screen.getByText('Charger plus'); + const loadAllButton = screen.getByText('Charger tout'); + + // Click multiple times + fireEvent.click(loadMoreButton); + fireEvent.click(loadMoreButton); + fireEvent.click(loadAllButton); + fireEvent.click(loadAllButton); + + expect(mockOnFetchNextPage).toHaveBeenCalledTimes(2); + expect(mockOnFetchAllPages).toHaveBeenCalledTimes(2); + }); + + it('should render with large numbers', () => { + render( + , + ); + + // ODS 19.2.0 may format numbers, so use flexible matcher + expect(screen.getByText(/9999.*99999.*résultats/i)).toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.component.tsx new file mode 100644 index 000000000000..1a740e74774c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.component.tsx @@ -0,0 +1,50 @@ +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BUTTON_VARIANT, BUTTON_SIZE } from '@ovhcloud/ods-react'; +import { Button } from '../../../../button'; + +const FooterActionsComponent = ({ + hasNextPage, + isLoading, + onFetchAllPages, + onFetchNextPage, +}: { + hasNextPage?: boolean; + isLoading?: boolean; + onFetchAllPages?: () => void; + onFetchNextPage?: () => void; +}) => { + const { t } = useTranslation('datagrid'); + return ( +
+ {hasNextPage && ( +
+ {onFetchNextPage && ( + + )} + {onFetchAllPages && ( + + )} +
+ )} +
+ ); +}; + +export const FooterActions = memo(FooterActionsComponent); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.spec.tsx new file mode 100644 index 000000000000..7067cbbbf5b1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-actions/FooterActions.spec.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { FooterActions } from './FooterActions.component'; +import { mockOnFetchNextPage, mockOnFetchAllPages } from '../../../__mocks__'; + +describe('FooterActions', () => { + it('should render the footer actions', () => { + render( + , + ); + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + }); + + it('should render only Charger plus actions', () => { + render( + , + ); + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + expect(screen.queryByText('Charger tout')).not.toBeInTheDocument(); + }); + + it('should not render Charger plus action when hasNextPage is false', () => { + render( + , + ); + expect(screen.queryByText('Charger plus')).not.toBeInTheDocument(); + }); + + it('should call onFetchNext page when click on Charger plus', () => { + render( + , + ); + expect(screen.getByText('Charger plus')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Charger plus')); + expect(mockOnFetchNextPage).toHaveBeenCalled(); + }); + + it('should call onFetchAllPages when click on Charger tout', () => { + render( + , + ); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Charger tout')); + expect(mockOnFetchAllPages).toHaveBeenCalled(); + }); + + it('should button Charger tout be disabled when isLoading is true', () => { + render( + , + ); + expect(screen.getByText('Charger tout')).toBeInTheDocument(); + expect(screen.getByText('Charger tout')).toBeDisabled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-infos/FooterInfos.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-infos/FooterInfos.component.tsx new file mode 100644 index 000000000000..5650eda567b8 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-footer/footer-infos/FooterInfos.component.tsx @@ -0,0 +1,24 @@ +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Text } from '../../../../text'; + +const FooterInformationsComponent = ({ + itemsCount, + totalCount, +}: { + itemsCount?: number; + totalCount?: number; +}) => { + const { t } = useTranslation('datagrid'); + return ( +
+ + {itemsCount}{' '} + {totalCount && `${t('common_pagination_of')} ${totalCount}`}{' '} + {`${t('common_pagination_results')}`} + +
+ ); +}; + +export const FooterInfos = memo(FooterInformationsComponent); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/TableHeaderContent.props.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-head/TableHeaderContent.props.ts new file mode 100644 index 000000000000..810312ca1ec6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/TableHeaderContent.props.ts @@ -0,0 +1,8 @@ +import { HeaderGroup, ColumnSort, Column } from '@tanstack/react-table'; + +export type TableHeaderContentProps = { + contentAlignLeft?: boolean; + headerGroups: HeaderGroup[]; + onSortChange?: (sorting: ColumnSort[]) => void; + enableSorting?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/__test__/TableHead.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-head/__test__/TableHead.spec.tsx new file mode 100644 index 000000000000..56f441402dba --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/__test__/TableHead.spec.tsx @@ -0,0 +1,379 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { TableHeaderContent } from '../table-header-content/TableHeaderContent.component'; +import { mockHeaderGroups, mockOnSortChange } from '../../../__mocks__'; + +describe('TableHead', () => { + it('should render all headers correctly', () => { + render( + + +
, + ); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + it('should render headers with sorting enabled', () => { + render( + + +
, + ); + + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + it('should call onSortChange when header is clicked', () => { + const mockToggleSorting = vi.fn(); + const mockHeaderGroupsWithSorting: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSorting, + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const nameHeader = screen.getByTestId('header-name'); + fireEvent.click(nameHeader); + expect(mockToggleSorting).toHaveBeenCalled(); + }); + + it('should toggle sort direction on second click', () => { + const mockToggleSorting = vi.fn(); + const mockHeaderGroupsWithAscSort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSorting, + getIsSorted: () => 'asc', + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const nameHeader = screen.getByTestId('header-name'); + fireEvent.click(nameHeader); + expect(mockToggleSorting).toHaveBeenCalled(); + }); + + it('should handle multiple column sorting', () => { + const mockToggleSortingAge = vi.fn(); + const mockHeaderGroupsMultiSort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => 'asc', + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSortingAge, + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const ageHeader = screen.getByTestId('header-age'); + fireEvent.click(ageHeader); + expect(mockToggleSortingAge).toHaveBeenCalled(); + }); + + it('should render with initial sorting state', () => { + const mockHeaderGroupsWithDescSort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => 'desc', + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + it('should handle empty sorting array', () => { + const mockToggleSorting = vi.fn(); + const mockHeaderGroupsEmptySort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSorting, + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const nameHeader = screen.getByTestId('header-name'); + fireEvent.click(nameHeader); + expect(mockToggleSorting).toHaveBeenCalled(); + }); + + it('should handle undefined sorting prop', () => { + const mockToggleSorting = vi.fn(); + const mockHeaderGroupsNoSort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSorting, + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const nameHeader = screen.getByTestId('header-name'); + fireEvent.click(nameHeader); + expect(mockToggleSorting).toHaveBeenCalled(); + }); + + it('should handle manual sorting mode', () => { + const mockToggleSorting = vi.fn(); + const mockHeaderGroupsManualSort: any[] = [ + { + id: 'header-group-0', + depth: 0, + headers: [ + { + id: 'name', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Name' }, + getCanSort: () => true, + getToggleSortingHandler: () => mockToggleSorting, + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + { + id: 'age', + column: { + getSize: () => 150, + columnDef: { minSize: 20, maxSize: 'auto', header: 'Age' }, + getCanSort: () => true, + getToggleSortingHandler: () => vi.fn(), + getIsSorted: () => false, + }, + isPlaceholder: false, + getContext: () => ({}), + }, + ], + }, + ]; + + render( + + +
, + ); + + const nameHeader = screen.getByTestId('header-name'); + fireEvent.click(nameHeader); + expect(mockToggleSorting).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/index.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-head/index.ts new file mode 100644 index 000000000000..a5bcb1c00276 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/index.ts @@ -0,0 +1 @@ +export { TableHeaderContent } from './table-header-content/TableHeaderContent.component'; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-content/TableHeaderContent.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-content/TableHeaderContent.component.tsx new file mode 100644 index 000000000000..db683694dd0c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-content/TableHeaderContent.component.tsx @@ -0,0 +1,52 @@ +import { memo } from 'react'; +import { flexRender } from '@tanstack/react-table'; +import { TableHeaderContentProps } from '../TableHeaderContent.props'; +import { TableHeaderSorting } from '../table-header-sorting/TableHeaderSorting.component'; + +const TableHeaderContentComponent = ({ + contentAlignLeft = true, + headerGroups, + onSortChange, + enableSorting, +}: TableHeaderContentProps) => ( + + {headerGroups?.map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {!header.isPlaceholder && + (enableSorting && onSortChange ? ( + + ) : ( + <> + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + ))} + +); + +export const TableHeaderContent = memo(TableHeaderContentComponent) as < + T = unknown, +>( + props: TableHeaderContentProps, +) => JSX.Element; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.component.tsx b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.component.tsx new file mode 100644 index 000000000000..4d80e70cac0a --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.component.tsx @@ -0,0 +1,36 @@ +import { Icon, ICON_NAME } from '@ovhcloud/ods-react'; +import { flexRender } from '@tanstack/react-table'; +import { TableHeaderSortingProps } from './TableHeaderSorting.props'; + +export const TableHeaderSorting = ({ + header, + onSortChange, +}: TableHeaderSortingProps) => { + const canSort = header.column.getCanSort(); + const handleClick = onSortChange + ? header.column.getToggleSortingHandler() + : undefined; + const containerClassName = `${canSort ? 'cursor-pointer select-none' : ''} h-[20px]`; + + return ( +
+ + {flexRender(header.column.columnDef.header, header.getContext())} + + + + +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.props.ts b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.props.ts new file mode 100644 index 000000000000..9c573bc63e4c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/table/table-head/table-header-sorting/TableHeaderSorting.props.ts @@ -0,0 +1,6 @@ +import { Column, Header } from '@tanstack/react-table'; + +export type TableHeaderSortingProps = { + header: Header; + onSortChange: any; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/Topbar.component.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/Topbar.component.tsx new file mode 100644 index 000000000000..da0e80b9a751 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/Topbar.component.tsx @@ -0,0 +1,90 @@ +import { memo } from 'react'; +import { ColumnVisibility } from './columns-visibility/ColumnsVisibility.component'; +import { ColumnsVisibilityProps } from './columns-visibility/ColumnsVisiblity.props'; +import { ColumnsFilteringComponent } from './columns-filtering/ColumnsFiltering.component'; +import { FilterList } from '../../filters'; +import { ColumnsSearch } from './columns-search/ColumnsSearch.component'; +import { useDatagridTopbar } from './useDatagridTopbar'; + +const TopbarComponent = ({ + columns, + enableColumnvisibility, + enableFilter, + enableSearch, + filters, + getIsAllColumnsVisible, + getIsSomeColumnsVisible, + resourceType, + search, + setColumnVisibility, + toggleAllColumnsVisible, + topbar, + visibleColumns, +}: ColumnsVisibilityProps) => { + const { filtersColumns, hasVisibilityFeature } = useDatagridTopbar({ + columns, + visibleColumns, + }); + + return ( + <> +
+
+ {topbar && <>{topbar}} +
+
+
+ {enableSearch && } + {enableFilter && filtersColumns?.length > 0 && ( +
+ +
+ )} + + {enableColumnvisibility && + setColumnVisibility && + hasVisibilityFeature && ( +
0 ? 'ml-[10px]' : ''}> + {visibleColumns && visibleColumns.length > 0 && ( + + visibleColumns={visibleColumns} + toggleAllColumnsVisible={toggleAllColumnsVisible} + getIsAllColumnsVisible={getIsAllColumnsVisible} + getIsSomeColumnsVisible={getIsSomeColumnsVisible} + /> + )} +
+ )} +
+
+
+ {enableFilter && filters && filters.filters.length > 0 && ( +
+ +
+ )} + + ); +}; + +export const Topbar = memo(TopbarComponent) as ( + props: ColumnsVisibilityProps, +) => JSX.Element; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/__tests__/Topbar.spec.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/__tests__/Topbar.spec.tsx new file mode 100644 index 000000000000..a962bd74ddbe --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/__tests__/Topbar.spec.tsx @@ -0,0 +1,406 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { FilterTypeCategories } from '@ovh-ux/manager-core-api'; +import { render } from '@/setupTest'; +import { Topbar } from '../Topbar.component'; + +// Mock the IAM hook +vi.mock('../../../../../hooks/iam', () => ({ + useAuthorizationIam: vi.fn(() => ({ + isAuthorized: true, + isLoading: false, + isFetched: true, + })), +})); + +const mockColumns = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: true, + isFilterable: true, + enableHiding: true, + type: FilterTypeCategories.String, + }, + { + id: 'age', + header: 'Age', + accessorKey: 'age', + isSearchable: true, + isFilterable: true, + enableHiding: true, + type: FilterTypeCategories.Numeric, + }, +]; + +const mockVisibleColumns = [ + { + id: 'name', + columnDef: { header: 'Name', enableHiding: true }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, + { + id: 'age', + columnDef: { header: 'Age', enableHiding: true }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => true), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, +]; + +const mockSearch = { + onSearch: vi.fn(), + searchInput: 'test', + setSearchInput: vi.fn(), + placeholder: 'Search...', +}; + +const mockFilters = { + add: vi.fn(), + remove: vi.fn(), + filters: [], +}; + +const mockColumnVisibility = { name: true, age: true }; +const mockSetColumnVisibility = vi.fn(); + +describe('Topbar', () => { + it('should render the topbar with basic props', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should render the container + expect(screen.getByTestId('topbar-container')).toBeInTheDocument(); + }); + + it('should render custom topbar content', () => { + const customTopbar =
Custom topbar content
; + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + expect(screen.getByText('Custom topbar content')).toBeInTheDocument(); + }); + + it('should render search component when enabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should render search input + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + }); + + it('should not render search when disabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); + }); + + it('should render filter component when enabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect(screen.getByTestId('datagrid-topbar-filters')).toBeInTheDocument(); + }); + + it('should not render filter when disabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect( + screen.queryByTestId('datagrid-topbar-filters'), + ).not.toBeInTheDocument(); + }); + + it('should render column visibility when enabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should render columns button + expect(screen.getByText('Colonnes')).toBeInTheDocument(); + }); + + it('should not render column visibility when disabled', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + expect(screen.queryByText('Columns')).not.toBeInTheDocument(); + }); + + it('should render filter list when filters are active', () => { + const filtersWithData = { + add: vi.fn(), + remove: vi.fn(), + filters: [ + { + key: 'name', + label: 'Name', + value: 'John', + comparator: 'contains' as any, + }, + ], + }; + + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect(screen.getByTestId('datagrid-filter-list')).toBeInTheDocument(); + }); + + it('should not render filter list when no active filters', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect( + screen.queryByTestId('datagrid-filter-list'), + ).not.toBeInTheDocument(); + }); + + it('should render all features when enabled', () => { + const customTopbar =
Custom topbar content
; + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect(screen.getByText('Custom topbar content')).toBeInTheDocument(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + expect(screen.getByTestId('datagrid-topbar-filters')).toBeInTheDocument(); + expect(screen.getByText('Colonnes')).toBeInTheDocument(); + }); + + it('should render with resource type', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + expect(screen.getByTestId('datagrid-topbar-filters')).toBeInTheDocument(); + }); + + it('should render with empty visible columns', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should not render columns button when no visible columns + expect(screen.queryByText('Columns')).not.toBeInTheDocument(); + }); + + it('should handle search input changes', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + const searchInput = screen.getByRole('searchbox'); + fireEvent.change(searchInput, { target: { value: 'new search' } }); + + expect(mockSearch.setSearchInput).toHaveBeenCalledWith('new search'); + }); + + it('should handle search form submission', () => { + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + const form = screen.getByRole('searchbox').closest('form'); + fireEvent.submit(form!); + + expect(mockSearch.onSearch).toHaveBeenCalledWith('test'); + }); + + it('should render with columns that have no search feature', () => { + const columnsWithoutSearch = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: false, + isFilterable: true, + enableHiding: true, + }, + ]; + + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should not render search when no columns are searchable + expect(screen.queryByRole('searchbox')).not.toBeInTheDocument(); + }); + + it('should render with columns that have no visibility feature', () => { + const columnsWithoutVisibility = [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + isSearchable: true, + isFilterable: true, + enableHiding: false, + }, + ]; + + const visibleColumnsWithoutHiding = [ + { + id: 'name', + columnDef: { header: 'Name', enableHiding: false }, + getIsVisible: vi.fn(() => true), + getCanHide: vi.fn(() => false), + getToggleVisibilityHandler: vi.fn(() => vi.fn()), + } as any, + ]; + + render( + true)} + getIsSomeColumnsVisible={vi.fn(() => false)} + toggleAllColumnsVisible={vi.fn()} + />, + ); + + // Should not render column visibility when no columns can be hidden + expect(screen.queryByText('Columns')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/columns-filtering/ColumnsFiltering.component.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-filtering/ColumnsFiltering.component.tsx new file mode 100644 index 000000000000..27c502d9ea09 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-filtering/ColumnsFiltering.component.tsx @@ -0,0 +1,59 @@ +import { + Popover, + PopoverTrigger, + PopoverContent, + Icon, + ICON_NAME, + BUTTON_SIZE, + BUTTON_VARIANT, + POPOVER_POSITION, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '../../../button/Button.component'; +import { ColumnFilter, FilterAdd } from '../../../filters'; +import { FilterProps } from '../../Datagrid.props'; + +interface ColumnsFilteringComponentProps { + columns: ColumnFilter[]; + filters?: FilterProps; + resourceType?: string; +} + +export const ColumnsFilteringComponent = ({ + columns, + filters, + resourceType, +}: ColumnsFilteringComponentProps) => { + const { t } = useTranslation('filters'); + return ( + + + + + + { + if (filters && addedFilter.value !== undefined) { + filters.add({ + ...addedFilter, + value: addedFilter.value, + label: column.label, + }); + } + }} + /> + + + ); +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/columns-search/ColumnsSearch.component.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-search/ColumnsSearch.component.tsx new file mode 100644 index 000000000000..837ed891842f --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-search/ColumnsSearch.component.tsx @@ -0,0 +1,35 @@ +import { memo } from 'react'; +import { Input } from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { SearchProps } from '../../Datagrid.props'; + +const ColumnsSearchComponent = ({ search }: { search: SearchProps }) => { + const { t } = useTranslation('datagrid'); + return ( +
{ + search?.onSearch(search?.searchInput); + e.preventDefault(); + }} + > + { + search?.onSearch(''); + search?.setSearchInput(''); + }} + onChange={(e) => { + search?.setSearchInput(e.target.value); + e.preventDefault(); + }} + placeholder={search?.placeholder} + clearable + type="search" + value={search?.searchInput} + /> +
+ ); +}; + +export const ColumnsSearch = memo(ColumnsSearchComponent); diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisibility.component.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisibility.component.tsx new file mode 100644 index 000000000000..c74ce0ac278c --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisibility.component.tsx @@ -0,0 +1,96 @@ +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Popover, + PopoverTrigger, + PopoverContent, + Icon, + ICON_NAME, + Checkbox, + CheckboxControl, + CheckboxLabel, + FormField, + BUTTON_SIZE, + BUTTON_VARIANT, + POPOVER_POSITION, +} from '@ovhcloud/ods-react'; +import { ColumnsVisibilityProps } from './ColumnsVisiblity.props'; +import { Button } from '../../../button/Button.component'; +import { Text } from '../../../text/Text.component'; + +export const INTERNAL_COLUMNS = ['expander', 'actions']; + +const ColumnsVisibilityComponent = ({ + getIsAllColumnsVisible, + toggleAllColumnsVisible, + visibleColumns, +}: ColumnsVisibilityProps) => { + const { t } = useTranslation('datagrid'); + const eligibleColumns = + visibleColumns?.filter((column) => !INTERNAL_COLUMNS.includes(column.id)) || + []; + const visibleColumnsCount = eligibleColumns.filter((column) => + column.getIsVisible(), + ).length; + const isAllColumnsVisible = getIsAllColumnsVisible?.() || false; + return ( + + + + + +
+ +
    +
  • + toggleAllColumnsVisible?.()} + > + + + {t('common_topbar_columns_select_all')} + + +
  • + {eligibleColumns.map((column) => ( +
  • + + + + {column.columnDef.header as string} + + +
  • + ))} +
+
+
+
+
+ ); +}; + +export const ColumnVisibility = memo(ColumnsVisibilityComponent) as ( + props: ColumnsVisibilityProps, +) => JSX.Element; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisiblity.props.ts b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisiblity.props.ts new file mode 100644 index 000000000000..91d2d6bc863b --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/columns-visibility/ColumnsVisiblity.props.ts @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; +import { Column, VisibilityState } from '@tanstack/react-table'; +import { DatagridColumn, FilterProps, SearchProps } from '../../Datagrid.props'; + +export type ColumnsVisibilityProps = { + columns?: readonly DatagridColumn[]; + columnVisibility?: VisibilityState; + enableColumnvisibility?: boolean; + enableFilter?: boolean; + enableSearch?: boolean; + filters?: FilterProps; + getIsAllColumnsVisible?: () => boolean; + getIsSomeColumnsVisible?: () => boolean; + resourceType?: string; + search?: SearchProps; + setColumnVisibility?: (columnVisibility: VisibilityState) => void; + topbar?: ReactNode; + toggleAllColumnsVisible?: () => void; + visibleColumns?: Column[]; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.props.ts b/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.props.ts new file mode 100644 index 000000000000..7119e2cfd5b5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.props.ts @@ -0,0 +1,7 @@ +import { Column } from '@tanstack/react-table'; +import { DatagridColumn } from '../Datagrid.props'; + +export type UseDatagridTopbarProps = { + columns?: readonly DatagridColumn[]; + visibleColumns?: Column[]; +}; diff --git a/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.tsx b/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.tsx new file mode 100644 index 000000000000..bd81c253f340 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/topbar/useDatagridTopbar.tsx @@ -0,0 +1,39 @@ +import { useMemo } from 'react'; +import { FilterCategories } from '@ovh-ux/manager-core-api'; +import { ColumnFilter } from '../../filters'; +import { UseDatagridTopbarProps } from './useDatagridTopbar.props'; + +export const useDatagridTopbar = ({ + columns, + visibleColumns, +}: UseDatagridTopbarProps) => { + const hasVisibilityFeature = useMemo( + () => visibleColumns?.some((col) => col.columnDef?.enableHiding), + [visibleColumns], + ); + const filtersColumns = useMemo( + () => + columns + ?.filter( + (item) => + ('comparator' in item || 'type' in item) && + 'isFilterable' in item && + item.isFilterable, + ) + .map((column) => ({ + id: column.id || '', + label: column.label || '', + comparators: + column?.comparator || + FilterCategories[column?.type as keyof typeof FilterCategories] || + [], + ...(column?.type && { type: column.type }), + ...(column?.filterOptions && { options: column.filterOptions }), + })) || [], + [columns], + ); + return { + filtersColumns, + hasVisibilityFeature, + }; +}; diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_de_DE.json diff --git a/packages/manager-ui-kit/src/components/datagrid/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_en_GB.json new file mode 100644 index 000000000000..dc6f55c225c1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_en_GB.json @@ -0,0 +1,12 @@ +{ + "common_pagination_of": "of", + "common_pagination_results": "results", + "common_pagination_no_results": "Aucun résultat", + "common_clipboard_success_label": "Copied!", + "common_clipboard_error_label": "Copy error.", + "common_pagination_load_more": "Charger plus", + "common_empty_text_cell": "None", + "common_topbar_columns": "Columns", + "common_pagination_load_all": "Charger tout", + "common_topbar_columns_select_all": "Select all" +} diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_fr_CA.json diff --git a/packages/manager-ui-kit/src/components/datagrid/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..4890d8e8544d --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_fr_FR.json @@ -0,0 +1,14 @@ +{ + "common_pagination_of": "sur", + "common_pagination_results": "résultats", + "common_pagination_no_results": "Aucun résultat", + "common_clipboard_success_label": "Copié !", + "common_clipboard_error_label": "Erreur de copie.", + "common_pagination_load_more": "Charger plus", + "common_pagination_load_all": "Charger tout", + "common_empty_text_cell": "Aucun", + "common_search_placeholder": "Rechercher", + "common_topbar_columns": "Colonnes", + "common_topbar_columns_select_all": "Sélectionner tout", + "datagrid_error_context_not_found": "Erreur de chargement, veuillez essayer plus tard." +} diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/datagrid/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/datagrid/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/datagrid/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/datagrid/translations/index.ts b/packages/manager-ui-kit/src/components/datagrid/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/datagrid/translations/index.ts rename to packages/manager-ui-kit/src/components/datagrid/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/datagrid/useDatagrid.props.ts b/packages/manager-ui-kit/src/components/datagrid/useDatagrid.props.ts new file mode 100644 index 000000000000..54f344744ed7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/useDatagrid.props.ts @@ -0,0 +1,27 @@ +import { MutableRefObject } from 'react'; +import { ColumnSort, Row, VisibilityState } from '@tanstack/react-table'; +import { + DatagridColumn, + RowSelectionProps, + ExpandedProps, +} from './Datagrid.props'; + +export type UseDatagridTableProps = { + columns: readonly DatagridColumn[]; + columnVisibility?: VisibilityState; + data: T[]; + expandable?: ExpandedProps; + manualSorting?: boolean; + onSortChange?: (sorting: ColumnSort[]) => void; + renderSubComponent?: ( + row: Row, + headerRefs?: MutableRefObject>, + ) => JSX.Element; + rowSelection?: RowSelectionProps; + setColumnVisibility?: (columnVisibility: VisibilityState) => void; + sorting?: ColumnSort[]; +}; + +export interface ExpandableRow { + subRows?: T[]; +} diff --git a/packages/manager-ui-kit/src/components/datagrid/useDatagrid.tsx b/packages/manager-ui-kit/src/components/datagrid/useDatagrid.tsx new file mode 100644 index 000000000000..3b46f3425838 --- /dev/null +++ b/packages/manager-ui-kit/src/components/datagrid/useDatagrid.tsx @@ -0,0 +1,73 @@ +import { useMemo } from 'react'; +import { useReactTable, VisibilityState } from '@tanstack/react-table'; +import { UseDatagridTableProps, ExpandableRow } from './useDatagrid.props'; +import { useTableBuilder } from './builder'; + +export const useDatagrid = >({ + columns, + columnVisibility, + data, + expandable, + manualSorting, + onSortChange, + renderSubComponent, + rowSelection, + setColumnVisibility, + sorting, +}: UseDatagridTableProps) => { + const { + hasSortingFeature, + hasSearchFeature, + hasColumnVisibilityFeature, + hasFilterFeature, + } = useMemo(() => { + const has = (key: string) => columns?.some((col) => col?.[key]); + return { + hasSortingFeature: has('isSortable'), + hasSearchFeature: has('isSearchable'), + hasColumnVisibilityFeature: has('enableHiding'), + hasFilterFeature: has('isFilterable'), + }; + }, [columns]); + const hasExpandableFeature = useMemo( + () => data?.some((row) => row?.subRows) ?? false, + [data], + ); + const builder = useTableBuilder({ + columns, + columnVisibility, + data, + expandable, + hasExpandableFeature, + hasSortingFeature, + manualSorting, + onSortChange, + renderSubComponent, + rowSelection, + setColumnVisibility, + sorting, + }) + .setColumns() + .setColumnsVisibility() + .setCoreRowModel() + .setData() + .setExpanded() + .setExpandedRowModel() + .setRowSelection() + .setSorting() + .setState() + .setSubRows() + .build(); + const table = useReactTable(builder); + + return { + ...table, + features: { + hasSortingFeature, + hasSearchFeature, + hasColumnVisibilityFeature, + hasFilterFeature, + hasExpandableFeature, + }, + }; +}; diff --git a/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.component.tsx b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.component.tsx new file mode 100644 index 000000000000..48c7dc1af242 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.component.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Message, + MessageBody, + MessageIcon, + MODAL_COLOR, + MESSAGE_COLOR, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import './translations/translations'; +import { Modal } from '../modal'; +import { Text } from '../text'; +import { DeleteModalProps } from './DeleteModal.props'; + +export const DeleteModal: React.FC = ({ + open = false, + serviceTypeName, + isLoading, + onConfirmDelete, + onClose, + error, + children, +}) => { + const { t } = useTranslation('delete-modal'); + + const handleClose = () => { + if (onClose) { + onClose(); + } + }; + + return ( + +
+ {error && ( + + + {t('deleteModalError', { error })} + + )} + + {t('deleteModalDescription')} + + {children && <>{children}} +
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.props.ts b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.props.ts new file mode 100644 index 000000000000..d8dc4be862c7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.props.ts @@ -0,0 +1,11 @@ +import React from 'react'; + +export type DeleteModalProps = { + serviceTypeName?: string; + isLoading?: boolean; + onConfirmDelete: () => void; + onClose?: () => void; + error?: string; + children?: React.ReactNode; + open?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.snapshot.test.tsx b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.snapshot.test.tsx new file mode 100644 index 000000000000..d15d5751a7f2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.snapshot.test.tsx @@ -0,0 +1,46 @@ +import { vitest } from 'vitest'; +import { render } from '@/setupTest'; +import { DeleteModal } from './DeleteModal.component'; +import { DeleteModalProps } from './DeleteModal.props'; + +export const sharedProps: DeleteModalProps = { + onClose: vitest.fn(), + onConfirmDelete: vitest.fn(), + serviceTypeName: 'serviceType', + open: true, +}; + +describe('Delete Modal component', () => { + it('renders basic modal', () => { + const { container } = render(); + expect(container.parentElement).toMatchSnapshot(); + }); + + it('renders loading modal', () => { + const { container } = render(); + expect(container.parentElement).toMatchSnapshot(); + }); + + it('renders modal with error', () => { + const { container } = render( + , + ); + expect(container.parentElement).toMatchSnapshot(); + }); + + it('renders modal with custom service type', () => { + const { container } = render( + , + ); + expect(container.parentElement).toMatchSnapshot(); + }); + + it('renders modal with children content', () => { + const { container } = render( + +
Additional content
+
, + ); + expect(container.parentElement).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.spec.tsx b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.spec.tsx new file mode 100644 index 000000000000..8ccd29d0f3b4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/DeleteModal.spec.tsx @@ -0,0 +1,56 @@ +import { vitest } from 'vitest'; +import { waitFor, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '@/setupTest'; +import { DeleteModal } from './DeleteModal.component'; +import { DeleteModalProps } from './DeleteModal.props'; + +export const sharedProps: DeleteModalProps = { + onClose: vitest.fn(), + onConfirmDelete: vitest.fn(), + serviceTypeName: 'serviceType', + open: true, +}; + +describe('Delete Modal component', () => { + it('renders correctly', () => { + render(); + expect( + screen.getByTestId('manager-delete-modal-description'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('manager-delete-modal-cancel'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('manager-delete-modal-confirm'), + ).toBeInTheDocument(); + }); + + it('renders error message in modal', () => { + const errorMessage = 'Error message'; + render(); + waitFor(() => { + expect( + screen.getByText(errorMessage, { exact: false }), + ).toBeInTheDocument(); + }); + }); + + it('clicking cancel should call onClose', () => { + render(); + const button = screen.getByTestId('manager-delete-modal-cancel'); + fireEvent.click(button); + waitFor(() => { + expect(sharedProps.onClose).toHaveBeenCalled(); + }); + }); + + it('clicking confirm should call onConfirmDelete', () => { + render(); + const button = screen.getByTestId('manager-delete-modal-confirm'); + userEvent.click(button); + waitFor(() => { + expect(sharedProps.onConfirmDelete).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/delete-modal/__snapshots__/DeleteModal.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/delete-modal/__snapshots__/DeleteModal.snapshot.test.tsx.snap new file mode 100644 index 000000000000..de83c3cd0025 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/__snapshots__/DeleteModal.snapshot.test.tsx.snap @@ -0,0 +1,522 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Delete Modal component > renders basic modal 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Delete Modal component > renders loading modal 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Delete Modal component > renders modal with children content 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Delete Modal component > renders modal with custom service type 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Delete Modal component > renders modal with error 1`] = ` + +
+
+
+ +
+ +`; diff --git a/packages/manager-ui-kit/src/components/delete-modal/index.ts b/packages/manager-ui-kit/src/components/delete-modal/index.ts new file mode 100644 index 000000000000..1654bbac35e2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/index.ts @@ -0,0 +1,2 @@ +export { DeleteModal } from './DeleteModal.component'; +export type { DeleteModalProps } from './DeleteModal.props'; diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_de_DE.json diff --git a/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_en_GB.json new file mode 100644 index 000000000000..0872be5fa3f5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_en_GB.json @@ -0,0 +1,9 @@ +{ + "deleteModalError": "The following error has occurred: {{error}}.", + "deleteModalCancelButton": "No, dismiss", + "deleteModalDeleteButton": "Yes, cancel", + "deleteModalDescription": "Cancelling your service will irretrievably erase all related data. Do you wish to cancel your service?", + "deleteModalHeadline": "Cancel {{serviceType}}?", + "deleteModalHeadlineService": "the service", + "deleteModalAdditionalContent": "Additional information" +} diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/delete-modal/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/templates/delete-modal/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/delete-modal/translations/Messages_pt_PT.json diff --git a/packages/manager-ui-kit/src/components/delete-modal/translations/translations.ts b/packages/manager-ui-kit/src/components/delete-modal/translations/translations.ts new file mode 100644 index 000000000000..2f20ae7c7d81 --- /dev/null +++ b/packages/manager-ui-kit/src/components/delete-modal/translations/translations.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'delete-modal'); diff --git a/packages/manager-react-components/src/components/drawer/Drawer.types.tsx b/packages/manager-ui-kit/src/components/drawer/Drawer.types.tsx similarity index 100% rename from packages/manager-react-components/src/components/drawer/Drawer.types.tsx rename to packages/manager-ui-kit/src/components/drawer/Drawer.types.tsx diff --git a/packages/manager-ui-kit/src/components/drawer/__tests__/Drawer.snapshot.test.tsx b/packages/manager-ui-kit/src/components/drawer/__tests__/Drawer.snapshot.test.tsx new file mode 100644 index 000000000000..871d3afc0bc7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/__tests__/Drawer.snapshot.test.tsx @@ -0,0 +1,220 @@ +import { vitest } from 'vitest'; +import { TEXT_PRESET } from '@ovhcloud/ods-react'; +import { Drawer } from '../index'; +import { render } from '@/setupTest'; +import { Text } from '../../text/Text.component'; +import './drawer.mocks'; + +describe('Drawer Snapshot Tests', () => { + const mockOnDismiss = vitest.fn(); + + afterEach(() => { + vitest.clearAllMocks(); + }); + + describe('Drawer.Root', () => { + it('should render basic drawer with header and content', () => { + const { container } = render( + + + + Basic Drawer +

This is the drawer content

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render drawer with header, content and footer', () => { + const { container } = render( + + + + + Drawer with all components + +

Drawer with all components

+
+ +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render closed drawer', () => { + const { container } = render( + + + + Closed Drawer +

This drawer is closed

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render drawer without createPortal', () => { + const { container } = render( + + + + Rendered without portal +

Rendered without portal

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render drawer with trigger element', () => { + const { container } = render( + Open Drawer} + > + + + + Drawer with trigger button + +

Drawer with trigger button

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render drawer with complex content', () => { + const { container } = render( + + + + Complex Content Drawer +

Some text content

+
    +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
+
+ +
, + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Drawer.RootCollapsible', () => { + it('should render basic collapsible drawer', () => { + const { container } = render( + + + + Collapsible Drawer +

This is a collapsible drawer

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render collapsible drawer with all components', () => { + const { container } = render( + + + + + Complete Collapsible Drawer + +

Complete collapsible drawer

+
+ +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render closed collapsible drawer', () => { + const { container } = render( + + + + Closed Collapsible Drawer +

This collapsible drawer is closed

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render collapsible drawer with complex content', () => { + const { container } = render( + + + + + Complex Collapsible Drawer + +
+ + + + +
+
+ +
, + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/drawer/__tests__/__snapshots__/Drawer.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/drawer/__tests__/__snapshots__/Drawer.snapshot.test.tsx.snap new file mode 100644 index 000000000000..0e893565e2e1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/__tests__/__snapshots__/Drawer.snapshot.test.tsx.snap @@ -0,0 +1,939 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Drawer Snapshot Tests > Drawer.Root > should render basic drawer with header and content 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.Root > should render closed drawer 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.Root > should render drawer with complex content 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.Root > should render drawer with header, content and footer 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.Root > should render drawer with trigger element 1`] = ` +
+
+ + +
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.Root > should render drawer without createPortal 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.RootCollapsible > should render basic collapsible drawer 1`] = ` +
+
+
+ +
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.RootCollapsible > should render closed collapsible drawer 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.RootCollapsible > should render collapsible drawer with all components 1`] = ` +
+
+
+ +
+
+ +
+
+
+`; + +exports[`Drawer Snapshot Tests > Drawer.RootCollapsible > should render collapsible drawer with complex content 1`] = ` +
+
+
+ +
+
+ +
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/drawer/__tests__/drawer.mocks.tsx b/packages/manager-ui-kit/src/components/drawer/__tests__/drawer.mocks.tsx new file mode 100644 index 000000000000..003b2572876a --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/__tests__/drawer.mocks.tsx @@ -0,0 +1,9 @@ +import { vi } from 'vitest'; + +vitest.mock('../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); diff --git a/packages/manager-react-components/src/components/drawer/DrawerBackdrop.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.component.tsx similarity index 83% rename from packages/manager-react-components/src/components/drawer/DrawerBackdrop.component.tsx rename to packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.component.tsx index e162d371c70e..45145a94e484 100644 --- a/packages/manager-react-components/src/components/drawer/DrawerBackdrop.component.tsx +++ b/packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.component.tsx @@ -1,8 +1,6 @@ import { useTranslation } from 'react-i18next'; - -type DrawerBackdropProps = { - onClick: () => void; -}; +import { DrawerBackdropProps } from './DrawerBackdrop.props'; +import '../translations'; const DrawerBackdrop = ({ onClick }: DrawerBackdropProps) => { const { t } = useTranslation('drawer'); diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.props.ts b/packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.props.ts new file mode 100644 index 000000000000..6ddc2d60d782 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-backdrop/DrawerBackdrop.props.ts @@ -0,0 +1,3 @@ +export type DrawerBackdropProps = { + onClick: () => void; +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.component.tsx new file mode 100644 index 000000000000..0a7928a16d92 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.component.tsx @@ -0,0 +1,70 @@ +import { + Drawer, + DrawerTrigger, + DrawerContent, + DrawerBody, + BUTTON_COLOR, + BUTTON_VARIANT, + DRAWER_POSITION, + Spinner, + SPINNER_SIZE, + Icon, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { DrawerBaseProps } from './DrawerBase.props'; +import '../translations'; +import { Button } from '../../button/Button.component'; + +export const DrawerBase = ({ + children, + createPortal, + isOpen, + isLoading, + onDismiss, + className, + trigger, +}: DrawerBaseProps) => { + const { t } = useTranslation('drawer'); + + return ( + + {trigger && {trigger}} + + +
+ + + {isLoading && ( +
+ +
+ )} + {!isLoading && children} +
+
+
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.props.tsx new file mode 100644 index 000000000000..30ece54b93f1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-base/DrawerBase.props.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren, ReactNode } from 'react'; + +export type DrawerBaseProps = PropsWithChildren & { + /** Open/close the drawer (default to true) */ + isOpen?: boolean; + /** Callback function to be called on close of the Drawer */ + onDismiss: () => void; + /** Show a loader instead of the drawer content */ + isLoading?: boolean; + /** Class name for the drawer */ + className?: string; + /** Create a portal for the drawer */ + createPortal?: boolean; + /** Trigger for the drawer */ + trigger?: ReactNode; +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-base/__tests__/DrawerBase.spec.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-base/__tests__/DrawerBase.spec.tsx new file mode 100644 index 000000000000..5971e36fb45d --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-base/__tests__/DrawerBase.spec.tsx @@ -0,0 +1,74 @@ +import '../../__tests__/drawer.mocks'; // import the mock first +import { vi, type MockInstance } from 'vitest'; +import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DrawerBase } from '../DrawerBase.component'; +import { DrawerBaseProps } from '../DrawerBase.props'; +import { useAuthorizationIam } from '../../../../hooks/iam'; + +vi.mock('../../../../hooks/iam'); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +const mockUseAuthorizationIam = useAuthorizationIam as unknown as MockInstance; + +export const mockedDrawerBaseProps: DrawerBaseProps = { + isOpen: true, + children:
Drawer content
, + isLoading: false, + onDismiss: vi.fn(), +}; + +describe('DrawerBase', () => { + beforeEach(() => { + mockUseAuthorizationIam.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + it('should display the drawer', async () => { + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + + const dismissButton = screen.getByTestId('drawer-dismiss-button'); + expect(dismissButton).toHaveAttribute('aria-label', 'close'); + }); + + it('should show a loader with the dismiss button when isLoading is true', () => { + render(); + + expect(screen.getByTestId('drawer')).not.toBeNull(); + expect(screen.getByTestId('drawer-spinner')).not.toBeNull(); + expect(screen.getByTestId('drawer-dismiss-button')).not.toBeNull(); + }); + + it('should show the content when isLoading is false', () => { + render( + +
Drawer content
+
, + ); + + expect(screen.getByTestId('drawer')).not.toBeNull(); + expect(screen.queryByTestId('drawer-spinner')).toBeNull(); + expect(screen.queryByText('Drawer content')).not.toBeNull(); + expect(screen.getByTestId('drawer-dismiss-button')).not.toBeNull(); + }); + + it('should close the drawer when the dismiss button is clicked', async () => { + const user = userEvent.setup(); + render(); + + expect(screen.getByTestId('drawer')).not.toBeNull(); + + const dismissButton = screen.getByTestId('drawer-dismiss-button'); + await act(() => user.click(dismissButton)); + + expect(mockedDrawerBaseProps.onDismiss).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-content/DrawerContent.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-content/DrawerContent.component.tsx new file mode 100644 index 000000000000..bfabe212d4e1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-content/DrawerContent.component.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; + +export const DrawerContent = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.component.tsx new file mode 100644 index 000000000000..66f590c0d1ac --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.component.tsx @@ -0,0 +1,47 @@ +import { BUTTON_VARIANT, BUTTON_COLOR } from '@ovhcloud/ods-react'; +import { DrawerFooterProps } from './DrawerFooter.props'; +import { Button } from '../../button/Button.component'; + +export const DrawerFooter = ({ + primaryButton, + secondaryButton, +}: DrawerFooterProps) => { + const { + label: primaryButtonLabel, + isLoading: isPrimaryButtonLoading, + isDisabled: isPrimaryButtonDisabled, + onClick: onPrimaryButtonClick, + } = primaryButton || {}; + const { + label: secondaryButtonLabel, + isLoading: isSecondaryButtonLoading, + isDisabled: isSecondaryButtonDisabled, + onClick: onSecondaryButtonClick, + } = secondaryButton || {}; + return ( +
+ {secondaryButtonLabel && ( + + )} + {primaryButtonLabel && ( + + )} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.props.tsx new file mode 100644 index 000000000000..459be7d747b1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-footer/DrawerFooter.props.tsx @@ -0,0 +1,23 @@ +export type DrawerFooterProps = { + /** Primary Button */ + primaryButton?: { + /** Label of the button */ + label?: string; + /** Loading state for the button */ + isLoading?: boolean; + /** Disabled state for the button */ + isDisabled?: boolean; + /** Callback for the button */ + onClick?: () => void; + }; + /** Secondary Button */ + secondaryButton?: { + /** Label of the button */ + label?: string; + /** Loading state for the button */ + isLoading?: boolean; + /** Disabled state for the button */ + isDisabled?: boolean; + onClick?: () => void; + }; +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-footer/__tests__/DrawerFooter.spec.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-footer/__tests__/DrawerFooter.spec.tsx new file mode 100644 index 000000000000..54c5f8642f34 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-footer/__tests__/DrawerFooter.spec.tsx @@ -0,0 +1,85 @@ +import { vi } from 'vitest'; +import { screen } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { DrawerFooter } from '../DrawerFooter.component'; +import { DrawerFooterProps } from '../DrawerFooter.props'; +import { useAuthorizationIam } from '../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; + +vitest.mock('../../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +export const mockedDrawerFooterProps: DrawerFooterProps = { + primaryButton: { + label: 'Confirm', + isLoading: false, + isDisabled: false, + onClick: vi.fn(), + }, + secondaryButton: { + label: 'Cancel', + isLoading: false, + isDisabled: false, + onClick: vi.fn(), + }, +}; + +describe('DrawerFooter', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + it('should display the buttons if they are provided', () => { + render(); + const primaryButton = screen.getByText('Confirm'); + const secondaryButton = screen.getByText('Cancel'); + expect(primaryButton).toBeInTheDocument(); + expect(primaryButton).toHaveTextContent('Confirm'); + expect(secondaryButton).toBeInTheDocument(); + expect(secondaryButton).toHaveTextContent('Cancel'); + }); + + it('should display only the primary button if only the primary button is provided', () => { + const mockedProps = { + ...mockedDrawerFooterProps, + secondaryButton: { + label: undefined, + }, + }; + render(); + + const primaryButton = screen.getByText('Confirm'); + expect(primaryButton).toBeInTheDocument(); + expect(primaryButton).toHaveTextContent('Confirm'); + const secondaryButton = screen.queryByText('Cancel'); + expect(secondaryButton).not.toBeInTheDocument(); + }); + + it('should display only the secondary button if only the secondary button is provided', () => { + const mockedProps = { + ...mockedDrawerFooterProps, + primaryButton: { + label: undefined, + }, + }; + render(); + + const secondaryButton = screen.getByText('Cancel'); + expect(secondaryButton).toBeInTheDocument(); + expect(secondaryButton).toHaveTextContent('Cancel'); + const primaryButton = screen.queryByText('Confirm'); + expect(primaryButton).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.component.tsx new file mode 100644 index 000000000000..b542c0d549e7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.component.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from 'react'; +import { + Icon, + ICON_NAME, + BUTTON_COLOR, + BUTTON_VARIANT, +} from '@ovhcloud/ods-react'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { DrawerHandleProps } from './DrawerHandle.props'; +import '../translations'; +import { Button } from '../../button/Button.component'; + +const DrawerHandle = ({ onClick, collapseState }: DrawerHandleProps) => { + const { t } = useTranslation('drawer'); + const [hasEscapeBeenPressed, setHasEscapeBeenPressed] = useState(false); + + // Handle Escape key press to hide the handle + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && collapseState === 'visible') { + setHasEscapeBeenPressed(true); + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => document.removeEventListener('keydown', handleKeyDown); + }, [collapseState]); + + return ( +
+
+
+ +
+
+
+ ); +}; + +export default DrawerHandle; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.props.tsx new file mode 100644 index 000000000000..45194a7a80b2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-handle/DrawerHandle.props.tsx @@ -0,0 +1,6 @@ +import { DrawerCollapseState } from '../Drawer.types'; + +export type DrawerHandleProps = { + onClick: () => void; + collapseState: DrawerCollapseState; +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.component.tsx new file mode 100644 index 000000000000..5ecb5b9f43fd --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.component.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; +import { TEXT_PRESET } from '@ovhcloud/ods-react'; +import { Text } from '../../text/Text.component'; +import { DrawerHeaderProps } from './DrawerHeader.props'; + +export const DrawerHeader = ({ title }: DrawerHeaderProps) => { + return ( +
+
+ {title} +
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.props.tsx new file mode 100644 index 000000000000..c1412234db18 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-header/DrawerHeader.props.tsx @@ -0,0 +1,3 @@ +export type DrawerHeaderProps = { + title: string; +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-header/__tests__/DrawerHeader.spec.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-header/__tests__/DrawerHeader.spec.tsx new file mode 100644 index 000000000000..b3775ccf6f7e --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-header/__tests__/DrawerHeader.spec.tsx @@ -0,0 +1,32 @@ +import { screen } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { DrawerHeader } from '../DrawerHeader.component'; +import { useAuthorizationIam } from '../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; + +vitest.mock('../../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +describe('DrawerHeader', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + it('should display the title', () => { + render(); + const title = screen.getByText('Drawer header title'); + expect(title).toBeInTheDocument(); + expect(title).toHaveTextContent('Drawer header title'); + }); +}); diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.component.tsx new file mode 100644 index 000000000000..6db4a87d24d2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.component.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import clsx from 'clsx'; +import DrawerHandle from '../drawer-handle/DrawerHandle.component'; +import { DrawerBase } from '../drawer-base/DrawerBase.component'; +import { DrawerCollapseState } from '../Drawer.types'; +import { DrawerRootCollapsibleProps } from './DrawerRootCollapsible.props'; + +export const DrawerRootCollapsible = ({ + isOpen = true, + ...props +}: DrawerRootCollapsibleProps) => { + const [collapseState, setCollapseState] = + useState('visible'); + + const handleToggleCollapseState = () => { + setCollapseState((prevState) => + prevState === 'visible' ? 'collapsed' : 'visible', + ); + }; + + return ( +
+ + + {isOpen && ( + + )} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.props.tsx new file mode 100644 index 000000000000..cfa7f3a90e8b --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/DrawerRootCollapsible.props.tsx @@ -0,0 +1,3 @@ +import { DrawerBaseProps } from '../drawer-base/DrawerBase.props'; + +export type DrawerRootCollapsibleProps = Omit; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/__tests__/DrawerRootCollapsible.spec.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/__tests__/DrawerRootCollapsible.spec.tsx new file mode 100644 index 000000000000..b87adc6d7a54 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root-collapsible/__tests__/DrawerRootCollapsible.spec.tsx @@ -0,0 +1,65 @@ +import '../../__tests__/drawer.mocks'; // import the mock first +import { vi } from 'vitest'; +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '@/setupTest'; +import { DrawerRootCollapsible } from '../DrawerRootCollapsible.component'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +it('should display the drawer in its collapsible variant', () => { + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + expect(screen.queryByTestId('drawer-backdrop')).toBeNull(); + expect(screen.queryByTestId('drawer-handle')).not.toBeNull(); +}); + +it('should collapse and reopen the drawer when the handle is clicked', async () => { + const user = userEvent.setup(); + + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + + // Collapse the drawer + const handle = screen.getByTestId('drawer-handle'); + await act(() => user.click(handle)); + + await waitFor(() => { + const drawer = screen.getByTestId('drawer'); + const classList = Array.from(drawer.classList); + const hasTranslateX = classList.some((className) => + className.includes('translate-x'), + ); + expect(hasTranslateX).toBe(true); + }); + + // Reopen the drawer + await act(() => user.click(handle)); + + await waitFor(() => { + const drawer = screen.getByTestId('drawer'); + const classList = Array.from(drawer.classList); + const hasTranslateX = classList.some((className) => + className.includes('translate-x'), + ); + expect(hasTranslateX).toBe(false); + }); +}); + +it('should hide the handle immediately after the user presses the “Esc” key', () => { + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + const handle = screen.getByTestId('drawer-handle'); + expect(handle).toBeVisible(); + act(() => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + }); + waitFor(() => { + expect(handle).not.toBeVisible(); + expect(screen.queryByTestId('drawer-handle')).toBeNull(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.component.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.component.tsx new file mode 100644 index 000000000000..e3bc9db1bbf1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.component.tsx @@ -0,0 +1,22 @@ +import DrawerBackdrop from '../drawer-backdrop/DrawerBackdrop.component'; +import { DrawerBase } from '../drawer-base/DrawerBase.component'; +import { DrawerRootProps } from './DrawerRoot.props'; + +export const DrawerRoot = ({ + createPortal = false, + trigger = null, + isOpen = true, + ...props +}: DrawerRootProps) => { + return ( +
+ + {isOpen && } +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.props.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.props.tsx new file mode 100644 index 000000000000..84826ad86dad --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root/DrawerRoot.props.tsx @@ -0,0 +1,3 @@ +import { DrawerBaseProps } from '../drawer-base/DrawerBase.props'; + +export type DrawerRootProps = Omit; diff --git a/packages/manager-ui-kit/src/components/drawer/drawer-root/__tests__/DrawerRoot.spec.tsx b/packages/manager-ui-kit/src/components/drawer/drawer-root/__tests__/DrawerRoot.spec.tsx new file mode 100644 index 000000000000..8d5ba896564f --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/drawer-root/__tests__/DrawerRoot.spec.tsx @@ -0,0 +1,44 @@ +import { vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import '../../__tests__/drawer.mocks'; +import { render } from '@/setupTest'; +import { DrawerRoot } from '../DrawerRoot.component'; +import { useAuthorizationIam } from '../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; + +vitest.mock('../../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +describe('DrawerRoot', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + it('should display a backdrop overlay', () => { + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + expect(screen.getByTestId('drawer-backdrop')).toBeVisible(); + }); + + it('should close the drawer on backdrop click', () => { + const onDismiss = vi.fn(); + render(); + expect(screen.getByTestId('drawer')).not.toBeNull(); + + const backdrop = screen.getByTestId('drawer-backdrop'); + fireEvent.click(backdrop); + expect(onDismiss).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-react-components/src/components/drawer/drawer.scss b/packages/manager-ui-kit/src/components/drawer/drawer.scss similarity index 100% rename from packages/manager-react-components/src/components/drawer/drawer.scss rename to packages/manager-ui-kit/src/components/drawer/drawer.scss diff --git a/packages/manager-ui-kit/src/components/drawer/index.ts b/packages/manager-ui-kit/src/components/drawer/index.ts new file mode 100644 index 000000000000..669994ccec5d --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/index.ts @@ -0,0 +1 @@ +export * as Drawer from './namespace'; diff --git a/packages/manager-ui-kit/src/components/drawer/namespace.ts b/packages/manager-ui-kit/src/components/drawer/namespace.ts new file mode 100644 index 000000000000..0ea6f172c2c0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/namespace.ts @@ -0,0 +1,9 @@ +export { DrawerRoot as Root } from './drawer-root/DrawerRoot.component'; +export { DrawerRootCollapsible as RootCollapsible } from './drawer-root-collapsible/DrawerRootCollapsible.component'; +export { DrawerContent as Content } from './drawer-content/DrawerContent.component'; +export { DrawerHeader as Header } from './drawer-header/DrawerHeader.component'; +export { DrawerFooter as Footer } from './drawer-footer/DrawerFooter.component'; +export type { DrawerFooterProps as FooterProps } from './drawer-footer/DrawerFooter.props'; +export type { DrawerRootProps as RootProps } from './drawer-root/DrawerRoot.props'; +export type { DrawerRootCollapsibleProps as RootCollapsibleProps } from './drawer-root-collapsible/DrawerRootCollapsible.props'; +export type { DrawerHeaderProps as HeaderProps } from './drawer-header/DrawerHeader.props'; diff --git a/packages/manager-ui-kit/src/components/drawer/readme.md b/packages/manager-ui-kit/src/components/drawer/readme.md new file mode 100644 index 000000000000..eeeedb0f0332 --- /dev/null +++ b/packages/manager-ui-kit/src/components/drawer/readme.md @@ -0,0 +1,43 @@ +# Drawer Component + +## How to use the Drawer Component + +The Drawer component is designed in a composable way. +The most common way to declare a drawer will be : + +```tsx + + + +
The is the custom content of the drawer
+
+ +
+``` + +⚠ Please put you content in a `Drawer.Content` component to ensure good positioning. + +## `DrawerRoot` and `DrawerRootCollapsible` + +The drawer component comes in two variants, accessible via two separate components: `DrawerRoot` and `DrawerRootCollapsible`. + +Both components use the `DrawerBase` component. + +- `DrawerBase` should not be exposed to library consumers. +- `DrawerBase` implements all shared behaviors for the two exported components. + +### Where to Add New Features + +When adding a feature to the drawer that must be in the root component (DrawerRoot or DrawerRootComposable): + +- If it is shared by both variants, add it to `DrawerBase`. +- If it is specific to one component, add it to the relevant component. +- If it is specific to one component but cannot be implemented outside of `DrawerBase`, add it to `DrawerBase` but only expose the related props in the relevant component. +- If it diverges too much from the existing variants, consider creating a new one. diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/drawer/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/drawer/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/drawer/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/drawer/translations/index.ts b/packages/manager-ui-kit/src/components/drawer/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/drawer/translations/index.ts rename to packages/manager-ui-kit/src/components/drawer/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.component.tsx b/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.component.tsx new file mode 100644 index 000000000000..ba68c7543ac0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.component.tsx @@ -0,0 +1,55 @@ +import { useContext, useEffect } from 'react'; +import { useRouteError } from 'react-router-dom'; +import { + ShellContext, + useRouteSynchro, +} from '@ovh-ux/manager-react-shell-client'; +import { ErrorBoundaryProps, ResponseAPIError } from './ErrorBoundary.props'; +import { Error } from '../error/Error.component'; + +const ShellRoutingSync = () => { + useRouteSynchro(); + return null; +}; + +export const ErrorBoundary = ({ + redirectionApp, + isPreloaderHide = false, + isRouteShellSync = false, +}: ErrorBoundaryProps) => { + const error = useRouteError() as ResponseAPIError; + const shell = useContext(ShellContext)?.shell; + const navigateToHomePage = () => { + shell?.navigation.navigateTo(redirectionApp, '', {}); + }; + const errorObject = + typeof error === 'object' && Object.keys(error)?.length > 0 + ? { + data: { + message: error?.response?.data?.message || error?.message, + }, + headers: error?.response?.headers || {}, + } + : {}; + + const reloadPage = () => { + shell?.navigation.reload(); + }; + + useEffect(() => { + if (isPreloaderHide) { + shell?.ux.hidePreloader(); + } + }, [isPreloaderHide]); + + return ( + <> + + {isRouteShellSync && } + + ); +}; diff --git a/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.props.ts b/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.props.ts new file mode 100644 index 000000000000..be571934693e --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/ErrorBoundary.props.ts @@ -0,0 +1,24 @@ +export interface ResponseAPIError { + message: string; + stack: string; + name: string; + code: string; + response?: { + headers?: { + [key: string]: string; + 'x-ovh-queryid': string; + }; + data?: { + message?: string; + }; + }; +} + +export interface ErrorBoundaryProps { + /** application name to redirect */ + redirectionApp: string; + /** Trigger the preloader hiding */ + isPreloaderHide?: boolean; + /** Trigger the routes sync beetween shell and the app */ + isRouteShellSync?: boolean; +} diff --git a/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.snapshot.test.tsx b/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.snapshot.test.tsx new file mode 100644 index 000000000000..199f3670c8af --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.snapshot.test.tsx @@ -0,0 +1,31 @@ +import { vitest } from 'vitest'; +import { ErrorBoundary } from '../ErrorBoundary.component'; +import { + renderWithContext, + mockGetEnvironment, +} from '../../../utils/Test.utils'; + +vitest.mock('react-router-dom', (importOriginal) => ({ + ...importOriginal(), + useRouteError: vitest.fn(), + useMatches: () => ({ + pathname: 'vrackServices', + }), +})); + +mockGetEnvironment.mockResolvedValue({ + applicationName: 'test-application', +}); + +describe('ErrorBoundary Tests', () => { + beforeEach(() => { + vitest.clearAllMocks(); + }); + + it('should render Error Boundary component when error occurs', () => { + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.spec.tsx b/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.spec.tsx new file mode 100644 index 000000000000..c9dbb0a6c597 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/__tests__/ErrorBoundary.spec.tsx @@ -0,0 +1,92 @@ +import { vitest } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { useRouteError } from 'react-router-dom'; +import tradFr from '../../error/translations/Messages_fr_FR.json'; +import { ErrorBoundary } from '../ErrorBoundary.component'; +import { ErrorProps } from '../../error/Error.props'; +import { renderWithContext, shellContext } from '../../../utils/Test.utils'; + +vitest.mock('react-router-dom', (importOriginal) => ({ + ...importOriginal(), + useRouteError: vitest.fn(), + useMatches: () => ({ + pathname: 'vrackServices', + }), +})); + +export const defaultProps: ErrorProps = { + error: {}, +}; + +describe('ErrorBoundary Tests render without error', () => { + beforeEach(() => { + vitest.clearAllMocks(); + }); + + it('should render Error component when error occurs', () => { + renderWithContext({ + children: , + }); + + const img = screen.getByAltText('OOPS'); + const title = screen.queryByText(tradFr.manager_error_page_boundary_title); + const errorMessage = screen.queryByText( + tradFr.manager_error_page_boundary_description, + ); + + expect(img).not.toBeNull(); + expect(title).toBeTruthy(); + expect(errorMessage).toBeTruthy(); + }); + + it('should call useRouteError hook', () => { + renderWithContext({ + children: , + }); + expect(useRouteError).toHaveBeenCalled(); + }); + + it('should call navigateToHomePage when onRedirectHome is called', () => { + renderWithContext({ + children: , + }); + const navigateToHomePage = screen.getByText( + tradFr.manager_error_page_action_home_label, + ); + expect(navigateToHomePage).toBeTruthy(); + fireEvent.click(navigateToHomePage); + expect(shellContext.shell.navigation.navigateTo).toHaveBeenCalledWith( + 'test', + '', + {}, + ); + }); + + it('should call reload when onRedirectReload is called', () => { + renderWithContext({ + children: , + }); + const reload = screen.getByText( + tradFr.manager_error_page_action_reload_label, + ); + expect(reload).toBeTruthy(); + fireEvent.click(reload); + expect(shellContext.shell.navigation.reload).toHaveBeenCalled(); + }); + + it('should not call hidePreloader when isPreloaderHide is false', () => { + renderWithContext({ + children: , + }); + expect(shellContext.shell.ux.hidePreloader).not.toHaveBeenCalled(); + }); + + it('should not render ShellRoutingSync component when isRouteShellSync is false', () => { + renderWithContext({ + children: ( + + ), + }); + expect(screen.getByTestId('error-template-action-reload')).toBeTruthy(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/error-boundary/__tests__/__snapshots__/ErrorBoundary.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/error-boundary/__tests__/__snapshots__/ErrorBoundary.snapshot.test.tsx.snap new file mode 100644 index 000000000000..a6fb7326e891 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/__tests__/__snapshots__/ErrorBoundary.snapshot.test.tsx.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ErrorBoundary Tests > should render Error Boundary component when error occurs 1`] = ` +
+ OOPS +

+ + Oups ! Une erreur est survenue. +

+

+ + Une erreur s'est produite, veuillez réessayer plus tard. +

+
+ + +
+
+`; diff --git a/packages/manager-ui-kit/src/components/error-boundary/index.ts b/packages/manager-ui-kit/src/components/error-boundary/index.ts new file mode 100644 index 000000000000..b9c2b2f083dd --- /dev/null +++ b/packages/manager-ui-kit/src/components/error-boundary/index.ts @@ -0,0 +1,5 @@ +export { ErrorBoundary } from './ErrorBoundary.component'; +export type { + ErrorBoundaryProps, + ResponseAPIError, +} from './ErrorBoundary.props'; diff --git a/packages/manager-ui-kit/src/components/error/Error.component.tsx b/packages/manager-ui-kit/src/components/error/Error.component.tsx new file mode 100644 index 000000000000..c48ac506c8fe --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/Error.component.tsx @@ -0,0 +1,119 @@ +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Text, + Button, + Message, + MessageIcon, + MessageBody, + BUTTON_VARIANT, + MESSAGE_COLOR, + TEXT_PRESET, +} from '@ovhcloud/ods-react'; +import { PageType, ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { ErrorProps } from './Error.props'; +import './translations/translations'; +import { getTrackingTypology } from './Error.utils'; +import ErrorImg from '../../../public/assets/error-banner-oops.png'; + +export const Error = ({ + error, + onRedirectHome, + onReloadPage, + labelTracking, + ...rest +}: ErrorProps) => { + const { t } = useTranslation('error'); + const { shell } = React.useContext(ShellContext); + const isBoundaryError = + typeof error === 'undefined' || + error === null || + (typeof error === 'object' && + error !== null && + typeof error === 'object' && + Object.keys(error)?.length === 0); + + useEffect(() => { + const env = shell?.environment?.getEnvironment(); + env?.then((response) => { + const { applicationName } = response; + const name = `errors::${getTrackingTypology(error)}::${applicationName}`; + shell?.tracking?.trackPage({ + name, + level2: '81', + type: 'navigation', + page_category: PageType.bannerError, + }); + }); + }, []); + + return ( +
+ OOPS + + {error?.status === 404 && <> {t('manager_error_page_404_title')}} + {error?.status && error?.status !== 404 && ( + <>{t('manager_error_api_page_title')} + )} + {isBoundaryError && <> {t('manager_error_page_boundary_title')}} + + + {error?.status === 404 && ( + <>{t('manager_error_page_404_description')} + )} + {error?.status && error?.status !== 404 && ( + <>{t('manager_error_page_api_description')} + )} + {isBoundaryError && ( + <> {t('manager_error_page_boundary_description')} + )} + + {error?.status !== 404 && + (error?.data?.message || error?.headers?.['x-ovh-queryid']) && ( + + + +

+ {error?.data?.message && ( + + {error.data.message} + + )} + {error?.headers?.['x-ovh-queryid'] && ( + + {t('manager_error_page_detail_code')} + {error.headers['x-ovh-queryid']} + + )} +

+
+
+ )} +
+ + +
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/error/Error.props.ts b/packages/manager-ui-kit/src/components/error/Error.props.ts new file mode 100644 index 000000000000..1164c695cc2d --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/Error.props.ts @@ -0,0 +1,30 @@ +import React from 'react'; + +export interface ErrorObject { + status: number; + data: any; + headers: any; +} + +export interface ErrorMessage { + message?: string; + status?: number; + detail?: any; +} + +export const TRACKING_LABELS = { + SERVICE_NOT_FOUND: 'service_not_found', + UNAUTHORIZED: 'unauthorized', + PAGE_LOAD: 'error_during_page_loading', +}; + +export interface ErrorProps extends React.HTMLProps { + error: { + status?: number; + data?: any; + headers?: any; + }; + onRedirectHome?: () => void; + onReloadPage?: () => void; + labelTracking?: string; +} diff --git a/packages/manager-ui-kit/src/components/error/Error.utils.ts b/packages/manager-ui-kit/src/components/error/Error.utils.ts new file mode 100644 index 000000000000..83dd8a83cc47 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/Error.utils.ts @@ -0,0 +1,13 @@ +import { ErrorMessage, TRACKING_LABELS } from './Error.props'; + +function getTrackingTypology(error: ErrorMessage) { + if (error?.status && Math.floor(error.status / 100) === 4) { + return [401, 403].includes(error.status) + ? TRACKING_LABELS.UNAUTHORIZED + : TRACKING_LABELS.SERVICE_NOT_FOUND; + } + + return TRACKING_LABELS.PAGE_LOAD; +} + +export { getTrackingTypology }; diff --git a/packages/manager-ui-kit/src/components/error/__tests__/Error.snapshot.test.tsx b/packages/manager-ui-kit/src/components/error/__tests__/Error.snapshot.test.tsx new file mode 100644 index 000000000000..b03a4b62a6b3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/__tests__/Error.snapshot.test.tsx @@ -0,0 +1,158 @@ +import { vitest } from 'vitest'; +import { Error } from '../Error.component'; +import { ErrorObject } from '../Error.props'; +import { + mockGetEnvironment, + renderWithContext, +} from '../../../utils/Test.utils'; + +mockGetEnvironment.mockResolvedValue({ + applicationName: 'test-application', +}); + +describe('ErrorBanner Snapshot Tests', () => { + beforeEach(() => { + vitest.clearAllMocks(); + }); + + it('should match snapshot with default 404 error', () => { + const defaultError: ErrorObject = { + status: 404, + data: { message: 'Service not found' }, + headers: { 'x-ovh-queryid': '123456789' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with 401 unauthorized error', () => { + const unauthorizedError: ErrorObject = { + status: 401, + data: { message: 'Unauthorized access' }, + headers: { 'x-ovh-queryid': '987654321' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with 500 server error', () => { + const serverError: ErrorObject = { + status: 500, + data: { message: 'Internal server error' }, + headers: { 'x-ovh-queryid': '555666777' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with error without query ID', () => { + const errorWithoutQueryId: ErrorObject = { + status: 404, + data: { message: 'Page not found' }, + headers: {}, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with error without data message', () => { + const errorWithoutMessage: ErrorObject = { + status: 500, + data: {}, + headers: { 'x-ovh-queryid': '111222333' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with custom label tracking', () => { + const error: ErrorObject = { + status: 404, + data: { message: 'Custom error message' }, + headers: { 'x-ovh-queryid': '444555666' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with custom className', () => { + const error: ErrorObject = { + status: 404, + data: { message: 'Test error' }, + headers: { 'x-ovh-queryid': '777888999' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with all props', () => { + const error: ErrorObject = { + status: 403, + data: { message: 'Forbidden access' }, + headers: { 'x-ovh-queryid': '000111222' }, + }; + + const onRedirectHome = vitest.fn(); + const onReloadPage = vitest.fn(); + const { container } = renderWithContext({ + children: ( + + ), + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with minimal error object', () => { + const minimalError: ErrorObject = { + status: 404, + data: {}, + headers: {}, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with complex error message', () => { + const complexError: ErrorObject = { + status: 500, + data: { + message: + 'This is a very long error message that might contain special characters like é, ñ, or symbols like @#$%^&*() and should be displayed properly in the error banner component', + }, + headers: { 'x-ovh-queryid': 'complex123' }, + }; + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should match snapshot with null error', () => { + const { container } = renderWithContext({ + children: , + }); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/error/__tests__/Error.spec.tsx b/packages/manager-ui-kit/src/components/error/__tests__/Error.spec.tsx new file mode 100644 index 000000000000..ae266c0c466d --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/__tests__/Error.spec.tsx @@ -0,0 +1,141 @@ +import { vitest } from 'vitest'; +import { PageType } from '@ovh-ux/manager-react-shell-client'; +import { waitFor, screen, fireEvent } from '@testing-library/react'; +import { Error } from '../Error.component'; +import tradFr from '../translations/Messages_fr_FR.json'; +import { ErrorObject, ErrorProps } from '../Error.props'; +import { + renderWithContext, + mockTrackPage, + mockGetEnvironment, +} from '../../../utils/Test.utils'; + +const defaultProps: ErrorProps = { + error: { + headers: { 'x-ovh-queryid': '123456789' }, + data: { message: "Votre requête n'a pas abouti" }, + status: 404, + }, +}; + +describe('specs:error.component', () => { + beforeEach(() => { + vitest.clearAllMocks(); + mockGetEnvironment.mockResolvedValue({ + applicationName: 'test-application', + }); + }); + + it('renders without error', () => { + renderWithContext({ children: }); + const img = screen.getByAltText('OOPS'); + const title = screen.queryByText(tradFr.manager_error_page_404_title); + const errorMessage = screen.queryByText( + tradFr.manager_error_page_404_description, + ); + + expect(img).not.toBeNull(); + expect(title).toBeInTheDocument(); + expect(errorMessage).toBeInTheDocument(); + }); + + describe('tracking functionality', async () => { + it('calls tracking with correct parameters for 404 error', async () => { + const error404: ErrorObject = { + status: 404, + data: { message: 'Not found' }, + headers: { 'x-ovh-queryid': '123456789' }, + }; + renderWithContext({ children: }); + await waitFor(() => { + expect(true).toBe(true); + expect(mockGetEnvironment).toHaveBeenCalled(); + expect(mockTrackPage).toHaveBeenCalledWith({ + name: 'errors::service_not_found::test-application', + level2: '81', + type: 'navigation', + page_category: PageType.bannerError, + }); + }); + }); + + it('calls tracking with correct parameters for 401 error', () => { + const error401: ErrorObject = { + status: 401, + data: { message: 'Unauthorized' }, + headers: { 'x-ovh-queryid': '123456789' }, + }; + renderWithContext({ children: }); + waitFor(() => { + expect(mockGetEnvironment).toHaveBeenCalled(); + expect(mockTrackPage).toHaveBeenCalledWith({ + name: 'errors::unauthorized::test-application', + level2: '81', + type: 'navigation', + page_category: PageType.bannerError, + }); + }); + }); + + it('calls tracking with correct parameters for 500 error', async () => { + const error500: ErrorObject = { + status: 500, + data: { message: 'Internal server error' }, + headers: { 'x-ovh-queryid': '123456789' }, + }; + renderWithContext({ children: }); + await waitFor(() => { + expect(mockGetEnvironment).toHaveBeenCalled(); + expect(mockTrackPage).toHaveBeenCalledWith({ + name: 'errors::error_during_page_loading::test-application', + level2: '81', + type: 'navigation', + page_category: PageType.bannerError, + }); + }); + }); + }); + + describe('contents', () => { + it('displays error details if present', () => { + const customError: ErrorObject = { + status: 500, + data: { message: 'Custom data message' }, + headers: { 'x-ovh-queryid': '123456789' }, + }; + renderWithContext({ + children: , + }); + const strongMessage = screen.queryByText('Custom data message'); + expect(strongMessage).toBeTruthy(); + }); + + it('calls onRedirectHome when home button is clicked', () => { + const onRedirectHomeMock = vitest.fn(); + const { getByTestId } = renderWithContext({ + children: ( + + ), + }); + + const homeButton = getByTestId('error-template-action-home'); + fireEvent.click(homeButton); + expect(onRedirectHomeMock).toHaveBeenCalled(); + }); + + it('calls onReloadPage when reload button is clicked', () => { + const onReloadPageMock = vitest.fn(); + const { getByTestId } = renderWithContext({ + children: ( + + ), + }); + const reloadButton = getByTestId('error-template-action-reload'); + fireEvent.click(reloadButton); + expect(onReloadPageMock).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/error/__tests__/__snapshots__/Error.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/error/__tests__/__snapshots__/Error.snapshot.test.tsx.snap new file mode 100644 index 000000000000..c6ae887fe348 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/__tests__/__snapshots__/Error.snapshot.test.tsx.snap @@ -0,0 +1,582 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ErrorBanner Snapshot Tests > should match snapshot with 401 unauthorized error 1`] = ` +
+ OOPS +

+ Oups ! Une erreur interne est survenue +

+

+ Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous. +

+
+ +
+

+ + + Unauthorized access + + + + Code d'erreur : + 987654321 + +

+
+
+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with 500 server error 1`] = ` +
+ OOPS +

+ Oups ! Une erreur interne est survenue +

+

+ Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous. +

+
+ +
+

+ + + Internal server error + + + + Code d'erreur : + 555666777 + +

+
+
+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with all props 1`] = ` +
+ OOPS +

+ Oups ! Une erreur interne est survenue +

+

+ Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous. +

+
+ +
+

+ + + Forbidden access + + + + Code d'erreur : + 000111222 + +

+
+
+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with complex error message 1`] = ` +
+ OOPS +

+ Oups ! Une erreur interne est survenue +

+

+ Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous. +

+
+ +
+

+ + + This is a very long error message that might contain special characters like é, ñ, or symbols like @#$%^&*() and should be displayed properly in the error banner component + + + + Code d'erreur : + complex123 + +

+
+
+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with custom className 1`] = ` +
+ OOPS +

+ + Oups ! Page introuvable. +

+

+ Erreur 404. La page que vous recherchez semble introuvable. +

+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with custom label tracking 1`] = ` +
+ OOPS +

+ + Oups ! Page introuvable. +

+

+ Erreur 404. La page que vous recherchez semble introuvable. +

+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with default 404 error 1`] = ` +
+ OOPS +

+ + Oups ! Page introuvable. +

+

+ Erreur 404. La page que vous recherchez semble introuvable. +

+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with error without data message 1`] = ` +
+ OOPS +

+ Oups ! Une erreur interne est survenue +

+

+ Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous. +

+
+ +
+

+ + Code d'erreur : + 111222333 + +

+
+
+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with error without query ID 1`] = ` +
+ OOPS +

+ + Oups ! Page introuvable. +

+

+ Erreur 404. La page que vous recherchez semble introuvable. +

+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with minimal error object 1`] = ` +
+ OOPS +

+ + Oups ! Page introuvable. +

+

+ Erreur 404. La page que vous recherchez semble introuvable. +

+
+ + +
+
+`; + +exports[`ErrorBanner Snapshot Tests > should match snapshot with null error 1`] = ` +
+ OOPS +

+ + Oups ! Une erreur est survenue. +

+

+ + Une erreur s'est produite, veuillez réessayer plus tard. +

+
+ + +
+
+`; diff --git a/packages/manager-ui-kit/src/components/error/index.ts b/packages/manager-ui-kit/src/components/error/index.ts new file mode 100644 index 000000000000..0e9fcd0efc18 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/index.ts @@ -0,0 +1,2 @@ +export { Error } from './Error.component'; +export type { ErrorProps, ErrorObject, ErrorMessage } from './Error.props'; diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/error/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/error/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/error/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/error/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_fr_CA.json diff --git a/packages/manager-ui-kit/src/components/error/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/error/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..d673fef6f75c --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/translations/Messages_fr_FR.json @@ -0,0 +1,14 @@ +{ + "manager_error_page_title": "Oops …!", + "manager_error_page_button_cancel": "Annuler", + "manager_error_page_detail_code": "Code d'erreur : ", + "manager_error_page_action_reload_label": "Réessayer", + "manager_error_page_action_home_label": "Retour à la page d'accueil", + "manager_error_page_default": "Une erreur est survenue lors du chargement de la page.", + "manager_error_page_404_title": "Oups ! Page introuvable.", + "manager_error_page_404_description": "Erreur 404. La page que vous recherchez semble introuvable.", + "manager_error_api_page_title": "Oups ! Une erreur interne est survenue", + "manager_error_page_api_description": "Une erreur s'est produite, vous pouvez nous contacter avec le code d'erreur ci dessous.", + "manager_error_page_boundary_title": "Oups ! Une erreur est survenue.", + "manager_error_page_boundary_description": "Une erreur s'est produite, veuillez réessayer plus tard." +} diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/error/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/error/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/templates/error/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/error/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/templates/error/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/error/translations/Messages_pt_PT.json diff --git a/packages/manager-ui-kit/src/components/error/translations/translations.ts b/packages/manager-ui-kit/src/components/error/translations/translations.ts new file mode 100644 index 000000000000..9d722ee328f9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/error/translations/translations.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'error'); diff --git a/packages/manager-ui-kit/src/components/filters/Filter.props.ts b/packages/manager-ui-kit/src/components/filters/Filter.props.ts new file mode 100644 index 000000000000..4f03ca8ad023 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/Filter.props.ts @@ -0,0 +1,7 @@ +import { Filter } from '@ovh-ux/manager-core-api'; + +export type FilterWithLabel = Filter & { label: string }; + +export type TagsFilterFormProps = { + setTagKey: (tagKey: string) => void; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/FilterAdd.props.ts b/packages/manager-ui-kit/src/components/filters/filter-add/FilterAdd.props.ts new file mode 100644 index 000000000000..0dba847831c8 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/FilterAdd.props.ts @@ -0,0 +1,24 @@ +import { + Filter, + FilterComparator, + FilterTypeCategories, +} from '@ovh-ux/manager-core-api'; + +export type Option = { + label: string; + value: string; +}; + +export type ColumnFilter = { + id: string; + label: string; + comparators: FilterComparator[]; + type?: FilterTypeCategories; + options?: Option[]; +}; + +export type FilterAddProps = { + columns: ColumnFilter[]; + resourceType?: string; + onAddFilter: (filter: Filter, column: ColumnFilter) => void; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/Filteradd.component.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/Filteradd.component.tsx new file mode 100644 index 000000000000..de6189bc4ba3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/Filteradd.component.tsx @@ -0,0 +1,184 @@ +import { useEffect, useMemo, useState, Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FilterComparator, + FilterTypeCategories, +} from '@ovh-ux/manager-core-api'; +import { + BUTTON_SIZE, + FormField, + FormFieldLabel, + Select, + SelectControl, + SelectContent, +} from '@ovhcloud/ods-react'; +import { FilterSectionValue } from './filter-section-value/FilterSectionValue.component'; +import { Button } from '../../button/Button.component'; +import { FilterTagsForm } from './filter-tags-form/FilterTagsForm.component'; +import { FilterAddProps } from './FilterAdd.props'; +import '../translations'; + +export function FilterAdd({ + columns, + onAddFilter, + resourceType, +}: Readonly) { + const { t } = useTranslation('filters'); + + const [selectedId, setSelectedId] = useState(columns?.[0]?.id || ''); + const [selectedComparator, setSelectedComparator] = useState( + columns?.[0]?.comparators?.[0] || FilterComparator.IsEqual, + ); + const [value, setValue] = useState(''); + const [dateValue, setDateValue] = useState(null); + const [tagKey, setTagKey] = useState(''); + + const selectedColumn = useMemo( + () => columns.find(({ id }) => selectedId === id), + [columns, selectedId], + ); + + const isInputValid = useMemo(() => { + if (selectedColumn?.type === FilterTypeCategories.Date) { + return dateValue !== null; + } + if (selectedColumn?.type === FilterTypeCategories.Numeric) { + return !Number.isNaN(Number(value)) && value !== ''; + } + + if (selectedColumn?.type === FilterTypeCategories.Tags) { + return ( + (!!tagKey && !!value) || + (!!tagKey && + [FilterComparator.TagExists, FilterComparator.TagNotExists].includes( + selectedComparator, + )) + ); + } + + return value !== ''; + }, [selectedColumn, dateValue, value, tagKey, selectedComparator]); + + const submitAddFilter = () => { + if (!isInputValid) { + return; + } + onAddFilter( + { + key: selectedId, + comparator: selectedComparator, + value: + selectedColumn?.type === FilterTypeCategories.Date + ? dateValue?.toISOString() || '' + : value, + type: selectedColumn?.type, + tagKey, + }, + selectedColumn!, + ); + setValue(''); + setTagKey(''); + setDateValue(null); + }; + + useEffect(() => { + setSelectedComparator( + selectedColumn?.comparators?.[0] || FilterComparator.IsEqual, + ); + setValue(''); + setTagKey(''); + setDateValue(null); + }, [selectedColumn]); + + return ( + <> +
+ + + {t('common_criteria_adder_column_label')} + + + +
+
+ + + {t('common_criteria_adder_operator_label')} + + {selectedColumn && ( +
+ +
+ )} +
+
+ {selectedColumn?.type !== FilterTypeCategories.Tags && ( +
+ + + {t('common_criteria_adder_value_label')} + + + +
+ )} + {selectedColumn?.type === FilterTypeCategories.Tags && ( +
+ +
+ )} +
+ +
+ + ); +} diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.snapshot.spec.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.snapshot.spec.tsx new file mode 100644 index 000000000000..2356e29cfc0e --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.snapshot.spec.tsx @@ -0,0 +1,147 @@ +import { vi } from 'vitest'; +import { FilterAdd } from '../Filteradd.component'; +import { FilterAddProps } from '../FilterAdd.props'; +import { render } from '@/setupTest'; + +vi.mock('../../../../hooks/iam/useOvhIam', () => ({ + useGetResourceTags: vi.fn().mockReturnValue({ + tags: [], + isError: false, + isLoading: false, + }), + useAuthorizationIam: vi.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const renderComponent = (props: FilterAddProps) => { + return render(); +}; + +describe('FilterAdd Snapshot Tests', () => { + it('should match snapshot with string filter type', () => { + const props = { + columns: [ + { + id: 'username', + label: "Nom d'utilisateur", + comparators: [ + 'includes', + 'starts_with', + 'ends_with', + 'is_equal', + 'is_different', + ], + type: 'String', + }, + ], + onAddFilter: vi.fn(), + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with date filter type', () => { + const props = { + columns: [ + { + id: 'createdAt', + label: 'Created At', + type: 'Date', + comparators: ['is_before', 'is_after', 'is_equal'], + }, + ], + onAddFilter: vi.fn(), + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with numeric filter type', () => { + const props = { + columns: [ + { + id: 'age', + label: 'Age', + type: 'Numeric', + comparators: ['is_lower', 'is_higher', 'is_equal'], + }, + ], + onAddFilter: vi.fn(), + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with select options', () => { + const props = { + columns: [ + { + id: 'status', + label: 'Status', + comparators: ['is_equal', 'is_different'], + options: [ + { label: 'Active', value: 'active' }, + { label: 'Inactive', value: 'inactive' }, + ], + }, + ], + onAddFilter: vi.fn(), + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with tags filter type', () => { + const props = { + columns: [ + { + id: 'tags', + label: 'Tags', + type: 'Tags', + comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], + }, + ], + onAddFilter: vi.fn(), + resourceType: 'dedicatedServer', + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with multiple columns', () => { + const props = { + columns: [ + { + id: 'username', + label: "Nom d'utilisateur", + comparators: ['includes', 'is_equal'], + type: 'String', + }, + { + id: 'age', + label: 'Age', + type: 'Numeric', + comparators: ['is_lower', 'is_higher'], + }, + { + id: 'createdAt', + label: 'Created At', + type: 'Date', + comparators: ['is_before', 'is_after'], + }, + ], + onAddFilter: vi.fn(), + } as FilterAddProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.spec.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.spec.tsx new file mode 100644 index 000000000000..04cc73519d24 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/FilterAdd.spec.tsx @@ -0,0 +1,343 @@ +import { vi, vitest } from 'vitest'; +import { act, fireEvent, waitFor } from '@testing-library/react'; +import { FilterAdd } from '../Filteradd.component'; +import { FilterAddProps } from '../FilterAdd.props'; +import { render } from '@/setupTest'; +import { TagsFilterFormProps } from '../../Filter.props'; +import { useAuthorizationIam } from '../../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../../hooks/iam/iam.interface'; + +vi.mock('../../../../hooks/iam'); + +vi.mock('./tags-filter-form.component', () => { + return { + TagsFilterForm: ({ setTagKey }: TagsFilterFormProps) => { + setTagKey('tagKey'); + return
; + }, + }; +}); + +vi.mock('@ovhcloud/ods-react', async () => { + const actual = await vi.importActual('@ovhcloud/ods-react'); + return { + ...actual, + Datepicker: ({ onValueChange, value, children, ...props }: any) => ( +
+ { + const dateValue = e.target.value ? new Date(e.target.value) : null; + onValueChange({ value: dateValue }); + }} + /> + {children} +
+ ), + DatepickerControl: ({ children, ...props }: any) => ( +
{children}
+ ), + DatepickerContent: ({ children, ...props }: any) => ( +
{children}
+ ), + }; +}); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; + +const renderComponent = (props: FilterAddProps) => { + return render(); +}; + +describe('FilterAdd tests', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + it('should deactivate the add filter button when value est undefined', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'username', + label: "Nom d'utilisateur", + comparators: [ + 'includes', + 'starts_with', + 'ends_with', + 'is_equal', + 'is_different', + ], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const addFilterButton = getByTestId('filter-add_submit'); + expect(addFilterButton).toBeDisabled(); + }); + + it('should set the id of first columns items as value of the id select', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'username', + label: "Nom d'utilisateur", + comparators: [ + 'includes', + 'starts_with', + 'ends_with', + 'is_equal', + 'is_different', + ], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const idColumnSelect = getByTestId('add-filter_select_idColumn'); + // how to get the value of the selection + const value = idColumnSelect.querySelector('select')?.value; + expect(value).toBe('username'); + }); + + it('should display a date picker when the filter type is Date', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'createdAt', + label: 'Created At', + type: 'Date', + comparators: ['is_before', 'is_after', 'is_equal'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + const { getByTestId } = renderComponent(props); + + const valueField = getByTestId('filter-add_value-date'); + expect(valueField).toBeVisible(); + }); + + it('should return a valid date string when a date is set in the value field', async () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'createdAt', + label: 'Created At', + type: 'Date', + comparators: ['is_before', 'is_after', 'is_equal'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const dateContainer = getByTestId('filter-add_value-date'); + const dateInput = dateContainer.querySelector('input'); + const addFilterButton = getByTestId('filter-add_submit'); + + const testDate = new Date('2023-10-01'); + const isoDate = testDate.toISOString(); + + // Submit button should be disabled initially + expect(addFilterButton).toBeDisabled(); + + // Set a date value + act(() => { + fireEvent.change(dateInput, { + target: { value: '2023-10-01' }, + }); + }); + + await waitFor(() => { + expect(addFilterButton).toBeEnabled(); + }); + + act(() => { + fireEvent.click(addFilterButton); + }); + + expect(mockOnAddFilter).toHaveBeenCalledWith( + expect.objectContaining({ + value: isoDate, + }), + expect.any(Object), + ); + }); + + it('should disable submit button when the filter type is Numeric and the value is not', async () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'size', + label: 'Size', + type: 'Numeric', + comparators: ['is_lower', 'is_higher', 'is_equal'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const valueField = getByTestId('filter-add_value-numeric'); + const addFilterButton = getByTestId('filter-add_submit'); + + const badValue = 'foo'; + const goodValue = '-123.12'; + + // Submit button is initially disabled + await waitFor(() => { + expect(addFilterButton).toBeDisabled(); + }); + + act(() => { + fireEvent.change(valueField, { target: { value: goodValue } }); + }); + + // Submit button is enabled with a valid number + await waitFor(() => { + expect(addFilterButton).not.toBeDisabled(); + }); + + act(() => { + fireEvent.change(valueField, { target: { value: badValue } }); + }); + + // Submit button is disabled with an invalid number + await waitFor(() => { + expect(addFilterButton).toBeDisabled(); + }); + }); + + it('should set the select option', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'status', + label: 'Status', + comparators: [ + 'includes', + 'starts_with', + 'ends_with', + 'is_equal', + 'is_different', + ], + options: [ + { label: 'option1', value: 'option_1' }, + { label: 'option2', value: 'option_2' }, + ], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const idSelect = getByTestId('filter-add_value-select'); + expect(idSelect).toBeDefined(); + }); + + it('should display tag filter form if the filter is a tag', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'tags', + label: 'Tags', + type: 'Tags', + comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const tagInputs = getByTestId('filter-tag-inputs'); + expect(tagInputs).toBeDefined(); + }); + + it('should display tags inputs if the filter is a tag', () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'tags', + label: 'Tags', + type: 'Tags', + comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const tagInputs = getByTestId('filter-tag-inputs'); + expect(tagInputs).toBeDefined(); + }); + + it('should disable submit if tag value is not set and comparator is not EXISTS/NOT_EXISTS', async () => { + const mockOnAddFilter = vitest.fn(); + const props = { + columns: [ + { + id: 'tags', + label: 'Tags', + type: 'Tags', + comparators: ['EQ', 'NEQ', 'EXISTS', 'NOT_EXISTS'], + }, + ], + onAddFilter: mockOnAddFilter, + } as FilterAddProps; + + const { getByTestId } = renderComponent(props); + + const columnSelect = getByTestId('add-filter_select_idColumn'); + fireEvent.change(columnSelect.querySelector('select'), { + target: { value: 'tags' }, + }); + + const operatorSelect = + getByTestId('add-operator-tags')?.querySelector('select'); + fireEvent.change(operatorSelect, { + target: { value: 'EQ' }, + }); + + const submitButton = getByTestId('filter-add_submit'); + expect(submitButton).toBeDisabled(); + + act(() => { + fireEvent.change(operatorSelect, { target: { value: 'EXISTS' } }); + }); + + await waitFor(() => { + expect(submitButton).toBeDisabled(); + }); + + act(() => { + fireEvent.change(operatorSelect, { target: { value: 'NOT_EXISTS' } }); + }); + + await waitFor(() => { + expect(submitButton).toBeDisabled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/__snapshots__/FilterAdd.snapshot.spec.tsx.snap b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/__snapshots__/FilterAdd.snapshot.spec.tsx.snap new file mode 100644 index 000000000000..0e0aecd784b1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/__tests__/__snapshots__/FilterAdd.snapshot.spec.tsx.snap @@ -0,0 +1,4297 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FilterAdd Snapshot Tests > should match snapshot with date filter type 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+ +
+
+`; + +exports[`FilterAdd Snapshot Tests > should match snapshot with multiple columns 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+`; + +exports[`FilterAdd Snapshot Tests > should match snapshot with numeric filter type 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+`; + +exports[`FilterAdd Snapshot Tests > should match snapshot with select options 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+`; + +exports[`FilterAdd Snapshot Tests > should match snapshot with string filter type 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+`; + +exports[`FilterAdd Snapshot Tests > should match snapshot with tags filter type 1`] = ` +
+
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+ Clé +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+`; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.component.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.component.tsx new file mode 100644 index 000000000000..3ac9fe8147d0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.component.tsx @@ -0,0 +1,91 @@ +import { FilterTypeCategories } from '@ovh-ux/manager-core-api'; +import { + Datepicker, + DatepickerControl, + DatepickerContent, + Input, + INPUT_TYPE, + Select, + SelectControl, + SelectContent, +} from '@ovhcloud/ods-react'; +import { FilterSectionValueProps } from './FilterSectionValue.props'; + +export const FilterSectionValue = ({ + selectedColumn, + value, + setValue, + submitAddFilter, + selectedId, + dateValue, + setDateValue, +}: FilterSectionValueProps) => { + let inputComponent: JSX.Element = null; + if (selectedColumn?.type === FilterTypeCategories.Date) { + inputComponent = ( +
+ setDateValue(detail.value || null)} + > + + + +
+ ); + } else if (selectedColumn?.type === FilterTypeCategories.Numeric) { + inputComponent = ( + setValue(`${e.target.value}`)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitAddFilter(); + } + }} + /> + ); + } else if (selectedColumn?.options && selectedColumn.options.length > 0) { + inputComponent = ( + + ); + } else { + inputComponent = ( + setValue(`${e.target.value}`)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitAddFilter(); + } + }} + /> + ); + } + + return inputComponent; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.props.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.props.tsx new file mode 100644 index 000000000000..7c3eaff67e62 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/filter-section-value/FilterSectionValue.props.tsx @@ -0,0 +1,11 @@ +import { ColumnFilter } from '../FilterAdd.props'; + +export type FilterSectionValueProps = { + selectedColumn: ColumnFilter; + value: string; + setValue: (value: string) => void; + submitAddFilter: () => void; + selectedId: string; + dateValue: Date | null; + setDateValue: (dateValue: Date | null) => void; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.component.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.component.tsx new file mode 100644 index 000000000000..2054b82aafe5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.component.tsx @@ -0,0 +1,95 @@ +import { + FormField, + FormFieldLabel, + Combobox, + Skeleton, + ComboboxControl, + ComboboxContent, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { useGetResourceTags } from '../../../../hooks/iam/useOvhIam'; +import { FilterTagsFormProps } from './FilterTagsForm.props'; + +export function FilterTagsForm({ + resourceType, + tagKey, + setTagKey, + setValue, +}: FilterTagsFormProps) { + const { t } = useTranslation('filters'); + const { + tags, + isError: isTagsError, + isLoading: isTagsLoading, + } = useGetResourceTags({ + resourceType, + }); + + const TagsLoading = () => ( +
+ +
+ ); + + const TagsMapped = + !isTagsError && tags + ? tags.map((tag) => ({ + value: tag.key, + label: tag.key, + })) + : []; + + const TagsValuesMapped = + !isTagsError && tags + ? tags + .find((tag) => tag.key === tagKey) + ?.values?.map((tagValue) => ({ + value: tagValue, + label: tagValue, + })) || [] + : []; + + return ( + <> + + {t('common_criteria_adder_key_label')} + {isTagsLoading ? ( + + ) : ( + setTagKey(value?.value?.[0] || '')} + data-testid="tags-filter-form-key-field" + items={TagsMapped} + > + + + + )} + + + + {t('common_criteria_adder_value_label')} + + {isTagsLoading ? ( + + ) : ( + { + setValue(value?.value?.[0] || ''); + }} + data-testid="tags-filter-form-value-field" + items={TagsValuesMapped} + > + + + + )} + + + ); +} diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.props.ts b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.props.ts new file mode 100644 index 000000000000..b179c330ffa0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/FilterTagsForm.props.ts @@ -0,0 +1,7 @@ +export type FilterTagsFormProps = { + resourceType: string; + tagKey: string; + setTagKey: (tagKey: string) => void; + value: string; + setValue: (value: string) => void; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/__tests__/FilterTagsForm.component.spec.tsx b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/__tests__/FilterTagsForm.component.spec.tsx new file mode 100644 index 000000000000..847e95b6a8ba --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-add/filter-tags-form/__tests__/FilterTagsForm.component.spec.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { vi } from 'vitest'; +import { waitFor } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { FilterTagsForm } from '../FilterTagsForm.component'; +import { FilterTagsFormProps } from '../FilterTagsForm.props'; + +const mocks = vi.hoisted(() => ({ + useGetResourceTags: vi.fn(), +})); + +vi.mock('../../../../hooks/iam/useOvhIam', () => ({ + useGetResourceTags: mocks.useGetResourceTags, +})); + +const TestWrapper = () => { + const [tagKey, setTagKey] = React.useState(''); + const [value, setValue] = React.useState(''); + + const defaultProps: FilterTagsFormProps = { + resourceType: 'testResource', + tagKey, + setTagKey, + value, + setValue, + }; + + return ; +}; + +describe('FilterTagValue', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows loading skeletons when loading', async () => { + mocks.useGetResourceTags.mockReturnValue({ + tags: [], + isError: false, + isLoading: true, + }); + const { container, queryByTestId } = render(); + + expect(queryByTestId('tags-filter-form-key-field')).not.toBeInTheDocument(); + expect( + queryByTestId('tags-filter-form-value-field'), + ).not.toBeInTheDocument(); + + const skeletons = container.querySelectorAll('[class*="skeleton"]'); + expect(skeletons.length).toBeGreaterThan(0); + }); + + it('shows tag key options when loaded', async () => { + mocks.useGetResourceTags.mockReturnValue({ + tags: [ + { key: 'env', values: ['prod', 'dev'] }, + { key: 'region', values: ['EU', 'US'] }, + ], + isError: false, + isLoading: false, + }); + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('tags-filter-form-key-field')).toBeInTheDocument(); + }); + + const tagKeyCombo = getByTestId('tags-filter-form-key-field'); + const tagValueCombo = getByTestId('tags-filter-form-value-field'); + expect(tagKeyCombo).toBeInTheDocument(); + expect(tagValueCombo).toBeInTheDocument(); + + const tagKeyInput = tagKeyCombo.querySelector('input'); + const tagValueInput = tagValueCombo.querySelector('input'); + expect(tagKeyInput).toBeInTheDocument(); + expect(tagValueInput).toBeInTheDocument(); + expect(tagKeyInput).not.toBeDisabled(); + }); + + it('disables tag value combobox until tag key is selected', async () => { + mocks.useGetResourceTags.mockReturnValue({ + tags: [{ key: 'env', values: ['prod', 'dev'] }], + isError: false, + isLoading: false, + }); + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('tags-filter-form-key-field')).toBeInTheDocument(); + }); + + const tagKeyCombo = getByTestId('tags-filter-form-key-field'); + const tagValueCombo = getByTestId('tags-filter-form-value-field'); + + const tagKeyInput = tagKeyCombo.querySelector('input'); + const tagValueInput = tagValueCombo.querySelector('input'); + + expect(tagValueInput).toBeDisabled(); + expect(tagKeyInput).not.toBeDisabled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.component.tsx b/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.component.tsx new file mode 100644 index 000000000000..73dae943b0d9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.component.tsx @@ -0,0 +1,32 @@ +import { Tag, TAG_COLOR } from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { formatFilter } from '../filters.utils'; +import { FilterListProps } from './FilterList.props'; +import '../translations'; + +export function FilterList({ + filters, + onRemoveFilter, +}: Readonly) { + const { t, i18n } = useTranslation('filters'); + const tComp = (comparator: string) => + t(`common_criteria_adder_operator_${comparator}`); + const locale = i18n.language?.replace('_', '-') || 'FR-fr'; + + return ( + <> + {filters?.map((filter, key) => ( + onRemoveFilter(filter)} + data-testid="filter-list_tag_item" + > + {`${filter.label && `${filter.label} ${tComp(filter.comparator)} ${formatFilter(filter, locale)}`}`} + + ))} + + ); +} diff --git a/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.props.ts b/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.props.ts new file mode 100644 index 000000000000..48aceb218561 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-list/FilterList.props.ts @@ -0,0 +1,6 @@ +import { FilterWithLabel } from '../Filter.props'; + +export type FilterListProps = { + filters: FilterWithLabel[]; + onRemoveFilter: (filter: FilterWithLabel) => void; +}; diff --git a/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.snapshot.spec.tsx b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.snapshot.spec.tsx new file mode 100644 index 000000000000..ca72fce652fa --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.snapshot.spec.tsx @@ -0,0 +1,134 @@ +import { vi } from 'vitest'; +import { FilterList } from '../FilterList.component'; +import { FilterListProps } from '../FilterList.props'; +import { render } from '@/setupTest'; + +const renderComponent = (props: FilterListProps) => { + return render(); +}; + +describe('FilterList Snapshot Tests', () => { + it('should match snapshot with empty filters', () => { + const props = { + filters: [], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with single string filter', () => { + const props = { + filters: [ + { + key: 'username', + comparator: 'includes', + value: 'john_doe', + label: "Nom d'utilisateur", + type: 'String', + }, + ], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with date filter', () => { + const props = { + filters: [ + { + key: 'createdAt', + comparator: 'is_equal', + value: new Date('2023-10-15').toISOString(), + label: 'Date de création', + type: 'Date', + }, + ], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with numeric filter', () => { + const props = { + filters: [ + { + key: 'age', + comparator: 'is_higher', + value: '25', + label: 'Age', + type: 'Numeric', + }, + ], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with multiple filters', () => { + const props = { + filters: [ + { + key: 'username', + comparator: 'includes', + value: 'admin', + label: 'Username', + type: 'String', + }, + { + key: 'status', + comparator: 'is_equal', + value: 'active', + label: 'Status', + }, + { + key: 'createdAt', + comparator: 'is_after', + value: new Date('2023-01-01').toISOString(), + label: 'Created After', + type: 'Date', + }, + ], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with different comparators', () => { + const props = { + filters: [ + { + key: 'name', + comparator: 'starts_with', + value: 'Test', + label: 'Name', + }, + { + key: 'email', + comparator: 'ends_with', + value: '@example.com', + label: 'Email', + }, + { + key: 'role', + comparator: 'is_different', + value: 'guest', + label: 'Role', + }, + ], + onRemoveFilter: vi.fn(), + } as FilterListProps; + + const { container } = renderComponent(props); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.spec.tsx b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.spec.tsx new file mode 100644 index 000000000000..8b9c1151ed78 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/FilterList.spec.tsx @@ -0,0 +1,127 @@ +import { vitest } from 'vitest'; +import { act, fireEvent } from '@testing-library/react'; +import { FilterList } from '../FilterList.component'; +import { FilterListProps } from '../FilterList.props'; +import { render } from '@/setupTest'; + +const renderComponent = (props: FilterListProps) => { + return render(); +}; + +describe('FilterList tests', () => { + it('should not display tags when the filters props is empty', () => { + const propsWithEmptyFilters = { + filters: [], + onRemoveFilter: vitest.fn(), + } as FilterListProps; + + const { container } = renderComponent(propsWithEmptyFilters); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should display 1 tag when the filters array props have one element', () => { + const propsWithOneFiltersItem = { + filters: [ + { + key: 'username', + comparator: 'includes', + value: 'temp_user', + label: "Nom d'utilisateur", + }, + ], + onRemoveFilter: vitest.fn(), + } as FilterListProps; + + const { container, getAllByTestId } = renderComponent( + propsWithOneFiltersItem, + ); + + const filterChipItems = getAllByTestId('filter-list_tag_item'); + + expect(container).not.toBeEmptyDOMElement(); + expect(filterChipItems).toHaveLength(1); + }); + + it('should display 2 tags when the filters array props have two elements', () => { + const propsWithTwoFiltersItem = { + filters: [ + { + key: 'username', + comparator: 'includes', + value: 'random_user', + label: "Nom d'utilisateur", + }, + { + key: 'username', + comparator: 'includes', + value: 'temp_user', + label: "Nom d'utilisateur", + }, + ], + onRemoveFilter: vitest.fn(), + } as FilterListProps; + + const { container, getAllByTestId } = renderComponent( + propsWithTwoFiltersItem, + ); + + const filterChipItems = getAllByTestId('filter-list_tag_item'); + + expect(container).not.toBeEmptyDOMElement(); + expect(filterChipItems).toHaveLength(2); + }); + + it('should display a formatted date when the filter type is a Date', () => { + const propsWithDateFilter = { + filters: [ + { + key: 'createdAt', + comparator: 'is_equal', + value: new Date('2023-10-01').toISOString(), + label: 'Creation Date', + type: 'Date', + }, + ], + onRemoveFilter: vitest.fn(), + } as FilterListProps; + + const { container, getByTestId } = renderComponent(propsWithDateFilter); + + const filterChipItem = getByTestId('filter-list_tag_item'); + + expect(container).not.toBeEmptyDOMElement(); + expect(filterChipItem.textContent).toContain('Creation Date'); + expect(filterChipItem.textContent).toContain('01/10/2023'); + }); + + it('should call onRemoveFilter function when the chip cross is clicked', () => { + const mockOnRemoveFilter = vitest.fn(); + const propsWithOneFiltersItem = { + filters: [ + { + key: 'username', + comparator: 'includes', + value: 'temp_user', + label: "Nom d'utilisateur", + }, + ], + onRemoveFilter: mockOnRemoveFilter, + } as FilterListProps; + + const { getByTestId } = renderComponent(propsWithOneFiltersItem); + + const filterChipItem = getByTestId('filter-list_tag_item'); + + act(() => { + fireEvent.click(filterChipItem); + }); + + expect(mockOnRemoveFilter).toHaveBeenNthCalledWith(1, { + comparator: 'includes', + key: 'username', + label: "Nom d'utilisateur", + value: 'temp_user', + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/__snapshots__/FilterList.snapshot.spec.tsx.snap b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/__snapshots__/FilterList.snapshot.spec.tsx.snap new file mode 100644 index 000000000000..0ba0462cf4f3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filter-list/__tests__/__snapshots__/FilterList.snapshot.spec.tsx.snap @@ -0,0 +1,118 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`FilterList Snapshot Tests > should match snapshot with date filter 1`] = ` +
+ +
+`; + +exports[`FilterList Snapshot Tests > should match snapshot with different comparators 1`] = ` +
+ + + +
+`; + +exports[`FilterList Snapshot Tests > should match snapshot with empty filters 1`] = `
`; + +exports[`FilterList Snapshot Tests > should match snapshot with multiple filters 1`] = ` +
+ + + +
+`; + +exports[`FilterList Snapshot Tests > should match snapshot with numeric filter 1`] = ` +
+ +
+`; + +exports[`FilterList Snapshot Tests > should match snapshot with single string filter 1`] = ` +
+ +
+`; diff --git a/packages/manager-ui-kit/src/components/filters/filters.utils.ts b/packages/manager-ui-kit/src/components/filters/filters.utils.ts new file mode 100644 index 000000000000..c3f536c4e7b4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/filters.utils.ts @@ -0,0 +1,16 @@ +import { FilterTypeCategories } from '@ovh-ux/manager-core-api'; +import { FilterWithLabel } from './Filter.props'; + +export function formatFilter(filter: FilterWithLabel, locale?: string): string { + if (!filter) return ''; + switch (filter.type) { + case FilterTypeCategories.Date: + return new Date(`${filter.value}`).toLocaleDateString(locale); + case FilterTypeCategories.Tags: + return filter.value + ? `${filter.tagKey}:${filter.value}` + : filter.tagKey || ''; + default: + return filter.value as string; + } +} diff --git a/packages/manager-ui-kit/src/components/filters/index.ts b/packages/manager-ui-kit/src/components/filters/index.ts new file mode 100644 index 000000000000..7970bc98e39c --- /dev/null +++ b/packages/manager-ui-kit/src/components/filters/index.ts @@ -0,0 +1,8 @@ +export { FilterAdd } from './filter-add/Filteradd.component'; +export { FilterList } from './filter-list/FilterList.component'; +export type { FilterWithLabel } from './Filter.props'; +export type { + Option, + ColumnFilter, + FilterAddProps, +} from './filter-add/FilterAdd.props'; diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/filters/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/filters/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/filters/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/filters/translations/index.ts b/packages/manager-ui-kit/src/components/filters/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/filters/translations/index.ts rename to packages/manager-ui-kit/src/components/filters/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/grid-layout/GridLayout.component.tsx b/packages/manager-ui-kit/src/components/grid-layout/GridLayout.component.tsx new file mode 100644 index 000000000000..18076a8cbf9f --- /dev/null +++ b/packages/manager-ui-kit/src/components/grid-layout/GridLayout.component.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren, FC } from 'react'; + +export const GridLayout: FC = ({ children }) => ( +
+
+ {children} +
+
+); diff --git a/packages/manager-ui-kit/src/components/grid-layout/__tests__/GridLayout.snapshot.test.tsx b/packages/manager-ui-kit/src/components/grid-layout/__tests__/GridLayout.snapshot.test.tsx new file mode 100644 index 000000000000..e1e4f7a6ff3b --- /dev/null +++ b/packages/manager-ui-kit/src/components/grid-layout/__tests__/GridLayout.snapshot.test.tsx @@ -0,0 +1,47 @@ +import { render } from '@/setupTest'; +import { GridLayout } from '../GridLayout.component'; +import { Tile } from '../../tile'; +import { Text } from '../../text'; + +describe('GridLayout Snapshot Tests', () => { + it('should render empty grid layout', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should render grid layout with children', () => { + const DashboardTiles = [ + { + title: 'General Informations', + label: 'Sample Term', + description: 'Sample Description', + }, + { + title: 'Second child', + label: 'Second child', + description: 'Sample Description', + }, + { + title: 'Third child', + label: 'Third child', + description: 'Sample Description', + }, + ]; + const { container } = render( + + {DashboardTiles.map((element) => ( + + + + + {element.description} + + + + ))} + , + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/grid-layout/__tests__/__snapshots__/GridLayout.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/grid-layout/__tests__/__snapshots__/GridLayout.snapshot.test.tsx.snap new file mode 100644 index 000000000000..6929f220e84f --- /dev/null +++ b/packages/manager-ui-kit/src/components/grid-layout/__tests__/__snapshots__/GridLayout.snapshot.test.tsx.snap @@ -0,0 +1,173 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GridLayout Snapshot Tests > should render empty grid layout 1`] = ` +
+
+
+
+
+`; + +exports[`GridLayout Snapshot Tests > should render grid layout with children 1`] = ` +
+
+
+
+
+

+ General Informations +

+
+
+
+
+
+ + Sample Term + +
+
+
+

+ Sample Description +

+
+
+
+
+
+
+
+
+

+ Second child +

+
+
+
+
+
+ + Second child + +
+
+
+

+ Sample Description +

+
+
+
+
+
+
+
+
+

+ Third child +

+
+
+
+
+
+ + Third child + +
+
+
+

+ Sample Description +

+
+
+
+
+
+
+
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/grid-layout/index.ts b/packages/manager-ui-kit/src/components/grid-layout/index.ts new file mode 100644 index 000000000000..206b86462624 --- /dev/null +++ b/packages/manager-ui-kit/src/components/grid-layout/index.ts @@ -0,0 +1 @@ +export { GridLayout } from './GridLayout.component'; diff --git a/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.component.tsx b/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.component.tsx new file mode 100644 index 000000000000..949a2c88930e --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.component.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { + Button, + BUTTON_SIZE, + BUTTON_VARIANT, + Popover, + PopoverTrigger, + PopoverContent, + POPOVER_POSITION, + Icon, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { Link, LinkType } from '../Link'; +import './translations/translation'; +import { GuideMenuProps } from './GuideMenu.props'; + +export const GuideMenu: React.FC = ({ isLoading, items }) => { + const { t } = useTranslation('guide-button'); + return ( + + + + + +
+ {items.map(({ id, onClick, ...rest }) => ( + + ))} +
+
+
+ ); +}; + +export default GuideMenu; diff --git a/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.props.ts b/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.props.ts new file mode 100644 index 000000000000..4b3a94d8baf5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/GuideMenu.props.ts @@ -0,0 +1,16 @@ +import { LinkProps } from '../Link'; + +export interface GuideMenuItem extends Omit { + id: number; + href: string; + download?: string; + target?: string; + rel?: string; + onClick?: () => void; + children?: string | JSX.Element; +} + +export interface GuideMenuProps { + items: GuideMenuItem[]; + isLoading?: boolean; +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.snapshot.test.tsx b/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.snapshot.test.tsx new file mode 100644 index 000000000000..e8c94d153bfd --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.snapshot.test.tsx @@ -0,0 +1,60 @@ +import { vi } from 'vitest'; +import { GuideMenu } from '../GuideMenu.component'; +import { GuideMenuItem } from '../GuideMenu.props'; +import { render } from '@/setupTest'; + +describe('GuideMenu', () => { + const mockItems: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + target: '_blank', + children: 'OVH Guides', + onClick: vi.fn(), + }, + { + id: 2, + href: 'https://docs.ovh.com', + target: '_blank', + children: 'Documentation', + onClick: vi.fn(), + }, + ]; + + const defaultProps = { + items: mockItems, + isLoading: false, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('GuideMenu snapshots', () => { + it('should match snapshot with default props', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with loading state', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with empty items', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with single item', () => { + const singleItemProps = { + items: [mockItems[0]], + isLoading: false, + }; + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.spec.tsx b/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.spec.tsx new file mode 100644 index 000000000000..20b6ba5f8743 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/__tests__/GuideMenu.spec.tsx @@ -0,0 +1,200 @@ +import { vi } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { GuideMenu } from '../GuideMenu.component'; +import { GuideMenuItem } from '../GuideMenu.props'; +import { render } from '@/setupTest'; + +// Mock IntersectionObserver for this component +global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + root: null, + rootMargin: '', + thresholds: [], + takeRecords: vi.fn().mockReturnValue([]), +})); + +describe('GuideMenu component', () => { + const mockItems: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + target: '_blank', + children: 'OVH Guides', + onClick: vi.fn(), + }, + { + id: 2, + href: 'https://docs.ovh.com', + target: '_blank', + children: 'Documentation', + onClick: vi.fn(), + }, + ]; + + const defaultProps = { + items: mockItems, + isLoading: false, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render the guide button with correct text', () => { + render(); + expect(screen.getByText('OVH Guides')).toBeInTheDocument(); + }); + + it('should render with loading state', () => { + render(); + const button = screen.getByRole('button'); + + expect(button).toHaveAttribute('disabled'); + }); + + it('should render without loading state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).not.toHaveAttribute('aria-disabled', 'true'); + }); + + it('should render all guide items', () => { + render(); + expect(screen.getByText('OVH Guides')).toBeInTheDocument(); + expect(screen.getByText('Documentation')).toBeInTheDocument(); + }); + }); + + describe('Interaction', () => { + it('should open popover when button is clicked', async () => { + render(); + const button = screen.getByRole('button'); + + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByText('OVH Guides')).toBeVisible(); + expect(screen.getByText('Documentation')).toBeVisible(); + }); + }); + + it('should call onClick when guide item is clicked', async () => { + const mockOnClick = vi.fn(); + const itemsWithClick: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + target: '_blank', + children: 'OVH Guides', + onClick: mockOnClick, + }, + ]; + + render(); + const button = screen.getByRole('button'); + + fireEvent.click(button); + + await waitFor(() => { + const guideLink = screen.getByText('OVH Guides'); + fireEvent.click(guideLink); + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + }); + + it('should be disabled when loading', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + }); + }); + + describe('Props validation', () => { + it('should handle items with all optional properties', () => { + const itemsWithAllProps: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + target: '_blank', + rel: 'noopener noreferrer', + download: 'guide.pdf', + children: 'OVH Guides', + onClick: vi.fn(), + }, + ]; + render(); + expect(screen.getByText('Guides')).toBeInTheDocument(); + }); + + it('should handle items with minimal required properties', () => { + const itemsWithMinimalProps: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + children: 'OVH Guides', + }, + ]; + render(); + expect(screen.getByText('Guides')).toBeInTheDocument(); + }); + + it('should handle undefined onClick in items', () => { + const itemsWithoutOnClick: GuideMenuItem[] = [ + { + id: 1, + href: 'https://help.ovhcloud.com/guides', + children: 'OVH Guides', + }, + ]; + render(); + expect(screen.getByText('Guides')).toBeInTheDocument(); + }); + }); + + describe('Accessibility', () => { + it('should have proper button role', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + }); + + it('should have proper aria-label', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('aria-label', 'Guides'); + }); + + it('should be keyboard accessible', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + button.focus(); + expect(button).toHaveFocus(); + }); + + it('should open popover on Enter key press', () => { + render(); + const button = screen.getByRole('button'); + + fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' }); + + waitFor(() => { + expect(screen.getByText('OVH Guides')).toBeVisible(); + }); + }); + + it('should open popover on Space key press', () => { + render(); + const button = screen.getByRole('button'); + + fireEvent.keyDown(button, { key: ' ', code: 'Space' }); + + waitFor(() => { + expect(screen.getByText('OVH Guides')).toBeVisible(); + }); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/guide-menu/__tests__/__snapshots__/GuideMenu.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/guide-menu/__tests__/__snapshots__/GuideMenu.snapshot.test.tsx.snap new file mode 100644 index 000000000000..d5e0ff151d42 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/__tests__/__snapshots__/GuideMenu.snapshot.test.tsx.snap @@ -0,0 +1,138 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GuideMenu > GuideMenu snapshots > should match snapshot with default props 1`] = ` +
+ +
+`; + +exports[`GuideMenu > GuideMenu snapshots > should match snapshot with empty items 1`] = ` +
+ +
+`; + +exports[`GuideMenu > GuideMenu snapshots > should match snapshot with loading state 1`] = ` +
+ +
+`; + +exports[`GuideMenu > GuideMenu snapshots > should match snapshot with single item 1`] = ` +
+ +
+`; diff --git a/packages/manager-ui-kit/src/components/guide-menu/index.ts b/packages/manager-ui-kit/src/components/guide-menu/index.ts new file mode 100644 index 000000000000..d21524ae9d16 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/index.ts @@ -0,0 +1,2 @@ +export { GuideMenu } from './GuideMenu.component'; +export type { GuideMenuItem, GuideMenuProps } from './GuideMenu.props'; diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_de_DE.json new file mode 100644 index 000000000000..315527bac9b4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_de_DE.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Anleitungen" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_en_GB.json new file mode 100644 index 000000000000..345392c104a1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_en_GB.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Guides" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_es_ES.json new file mode 100644 index 000000000000..5df7df2753e4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_es_ES.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Guías" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_CA.json new file mode 100644 index 000000000000..345392c104a1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_CA.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Guides" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..345392c104a1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_fr_FR.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Guides" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_it_IT.json new file mode 100644 index 000000000000..072c79cd546c --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_it_IT.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Guide" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pl_PL.json new file mode 100644 index 000000000000..3a897f110428 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pl_PL.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Przewodniki" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pt_PT.json new file mode 100644 index 000000000000..c70256e7f7ea --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/Messages_pt_PT.json @@ -0,0 +1,3 @@ +{ + "user_account_guides_header": "Manuais" +} diff --git a/packages/manager-ui-kit/src/components/guide-menu/translations/translation.ts b/packages/manager-ui-kit/src/components/guide-menu/translations/translation.ts new file mode 100644 index 000000000000..aa1b72644683 --- /dev/null +++ b/packages/manager-ui-kit/src/components/guide-menu/translations/translation.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'guide-button'); diff --git a/packages/manager-ui-kit/src/components/index.ts b/packages/manager-ui-kit/src/components/index.ts new file mode 100644 index 000000000000..07033f516a10 --- /dev/null +++ b/packages/manager-ui-kit/src/components/index.ts @@ -0,0 +1,42 @@ +export * from './action-banner'; +export * from './action-menu'; +export * from './redirection-guard'; +export * from './breadcrumb'; +export * from './base-layout'; +export * from './clipboard'; +export * from './step'; +export * from './tabs'; +export * from './tiles-input-group'; +export * from './tiles-input'; + +export * from './Link'; +export * from './drawer'; +export * from './guide-menu'; + +export * from './notifications'; + +export * from './price'; + +export * from './filters'; + +export * from './button'; +export * from './text'; + +export * from './service-state-badge'; + +export * from './tile'; +export * from './order'; + +export * from './badge'; +export * from './modal'; +export * from './tags-list'; +export * from './tags-tile'; +export * from './changelog-menu'; +export * from './link-card'; +export * from './error'; +export * from './error-boundary'; +export * from './update-name-modal'; +export * from './delete-modal'; +export * from './grid-layout'; +export * from './onboarding-layout'; +export * from './datagrid'; diff --git a/packages/manager-ui-kit/src/components/link-card/LinkCard.component.tsx b/packages/manager-ui-kit/src/components/link-card/LinkCard.component.tsx new file mode 100644 index 000000000000..c5ff2a500559 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/LinkCard.component.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, CARD_COLOR, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { Badge } from '../badge'; +import { LinkType, Link } from '../Link'; +import './translations/translations'; +import { LinkCardProps, LinkCardBadge } from './LinkCard.props'; + +export const LinkCard: React.FC = ({ + href, + externalHref, + hrefLabel, + img, + badges, + texts, + hoverable, + onClick, + trackingLabel, + ...props +}) => { + const { title, description, category } = texts; + const { t } = useTranslation('card'); + + return ( + + +
+ {img?.src && ( + {img.alt} + )} +
+ + {category} + + {badges && badges.length > 0 && ( + + {badges.map((badge: LinkCardBadge) => ( + + {badge.text} + + ))} + + )} +
+ + + {title} + + {description && ( + + {description} + + )} +
+ + {hrefLabel ?? t('see_more_label')} + +
+
+
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/link-card/LinkCard.props.tsx b/packages/manager-ui-kit/src/components/link-card/LinkCard.props.tsx new file mode 100644 index 000000000000..58ba6e72adbc --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/LinkCard.props.tsx @@ -0,0 +1,26 @@ +import { MouseEvent } from 'react'; + +export type LinkCardBadge = { + text: string; +}; + +type LinkCardImageDetails = { + src?: string; + alt?: string; +}; + +export type LinkCardProps = { + href: string; + externalHref?: boolean; + hrefLabel?: string; + img?: LinkCardImageDetails; + texts: { + title: string; + description?: string; + category: string; + }; + badges?: LinkCardBadge[]; + hoverable?: boolean; + onClick?: (event: MouseEvent) => void; + trackingLabel?: string; +}; diff --git a/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.tsx b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.tsx new file mode 100644 index 000000000000..2178293fb275 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { vitest, describe, it } from 'vitest'; +import { fireEvent, screen } from '@testing-library/react'; +import { + texts, + href, + description, + img, + renderLinkCard, + badges, +} from './LinkCard.spec.utils'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vi.fn(() => ({ isAuthorized: true })), +})); + +describe('LinkCard tests', () => { + it('renders with mandatory props', async () => { + renderLinkCard({ texts, href }); + const titleElement = screen.getByText(texts.title); + expect(titleElement).toBeInTheDocument(); + expect(titleElement.tagName).toBe('H3'); + + const categoryElement = screen.getByText(texts.category); + expect(categoryElement).toBeInTheDocument(); + expect(categoryElement.tagName).toBe('H4'); + }); + + it('renders with description', () => { + renderLinkCard({ + href, + texts: { + ...texts, + description, + }, + }); + const descriptionElement = screen.getByText(description); + expect(descriptionElement).toBeInTheDocument(); + expect(descriptionElement.tagName).toBe('P'); + }); + + it('renders with image', () => { + renderLinkCard({ texts, href, img }); + const imageElement = screen.getByAltText(img.alt); + expect(imageElement).toBeInTheDocument(); + expect(imageElement.tagName).toBe('IMG'); + }); + + it('renders with badge', () => { + renderLinkCard({ texts, href, badges }); + badges.forEach((badge) => { + expect(screen.queryByText(badge.text)).toBeInTheDocument(); + }); + }); + + it('renders with custom href', () => { + const hrefLabel = 'Custom Link Label'; + renderLinkCard({ texts, href, hrefLabel }); + expect(screen.queryByText(hrefLabel)).toBeInTheDocument(); + }); + + it('renders with external href', () => { + const { container } = renderLinkCard({ texts, href, externalHref: true }); + const linkElement = container.querySelector('[tab-index="-1"]'); + const [iconElement] = linkElement.getElementsByTagName('span'); + expect(iconElement.className).toContain('external-link'); + }); + + it('calls onClick when the card is clicked', () => { + const onClick = vitest.fn(); + renderLinkCard({ texts, href, onClick }); + const linkElement = screen.getByRole('link'); + fireEvent.click(linkElement); + expect(onClick).toHaveBeenCalled(); + }); + + it('sets data-tracking label', () => { + const trackingLabel = 'Tracking Label'; + const { container } = renderLinkCard({ texts, href, trackingLabel }); + expect( + container.querySelector(`[data-tracking="${trackingLabel}"]`), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.utils.tsx b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.utils.tsx new file mode 100644 index 000000000000..abb15e5df8ab --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.spec.utils.tsx @@ -0,0 +1,29 @@ +import { render } from '@testing-library/react'; +import { LinkCard, LinkCardProps } from '..'; + +export const texts = { + title: 'Titre du produit', + category: 'NAS', +}; + +export const description = + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."; + +export const img = { + alt: 'offer', + src: 'https://www.ovhcloud.com/sites/default/files/styles/offer_range_card/public/2021-06/1886_AI_Notebook1_Hero_600x400.png', +}; + +export const href = 'https://ovh.com'; + +export const badges = [ + { + text: 'Cloud computing', + }, + { + text: 'Beta', + }, +]; + +export const renderLinkCard = (props?: LinkCardProps) => + render(); diff --git a/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.test.tsx b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.test.tsx new file mode 100644 index 000000000000..d02b96b046d4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/__tests__/LinkCard.test.tsx @@ -0,0 +1,63 @@ +import { vitest, describe, it } from 'vitest'; +import { + texts, + href, + description, + img, + renderLinkCard, + badges, +} from './LinkCard.spec.utils'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vi.fn(() => ({ isAuthorized: true })), +})); + +describe('LinkCard Component Snapshot Tests', () => { + it('renders the component with texts', () => { + const { container } = renderLinkCard({ + texts: { + ...texts, + description, + }, + href, + }); + expect(container).toMatchSnapshot(); + }); + + it('renders the component with image', () => { + const { container } = renderLinkCard({ + texts: { + ...texts, + description, + }, + href, + img, + }); + expect(container).toMatchSnapshot(); + }); + + it('renders the component with badges', () => { + const { container } = renderLinkCard({ + texts: { + ...texts, + description, + }, + href, + badges, + }); + expect(container).toMatchSnapshot(); + }); + + it('renders the complete component', () => { + const { container } = renderLinkCard({ + texts: { + ...texts, + description, + }, + href, + badges, + img, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/link-card/__tests__/__snapshots__/LinkCard.test.tsx.snap b/packages/manager-ui-kit/src/components/link-card/__tests__/__snapshots__/LinkCard.test.tsx.snap new file mode 100644 index 000000000000..981db5103ec7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/__tests__/__snapshots__/LinkCard.test.tsx.snap @@ -0,0 +1,243 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LinkCard Component Snapshot Tests > renders the complete component 1`] = ` + +`; + +exports[`LinkCard Component Snapshot Tests > renders the component with badges 1`] = ` + +`; + +exports[`LinkCard Component Snapshot Tests > renders the component with image 1`] = ` + +`; + +exports[`LinkCard Component Snapshot Tests > renders the component with texts 1`] = ` + +`; diff --git a/packages/manager-ui-kit/src/components/link-card/index.ts b/packages/manager-ui-kit/src/components/link-card/index.ts new file mode 100644 index 000000000000..6f0ff1cc8374 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/index.ts @@ -0,0 +1,3 @@ +export { LinkCard } from './LinkCard.component'; + +export type { LinkCardProps } from './LinkCard.props'; diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/navigation/card/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/link-card/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/navigation/card/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/link-card/translations/Messages_pt_PT.json diff --git a/packages/manager-ui-kit/src/components/link-card/translations/translations.ts b/packages/manager-ui-kit/src/components/link-card/translations/translations.ts new file mode 100644 index 000000000000..16560a99c541 --- /dev/null +++ b/packages/manager-ui-kit/src/components/link-card/translations/translations.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'card'); diff --git a/packages/manager-ui-kit/src/components/modal/Modal.component.tsx b/packages/manager-ui-kit/src/components/modal/Modal.component.tsx new file mode 100644 index 000000000000..67c42f797667 --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/Modal.component.tsx @@ -0,0 +1,121 @@ +import { ElementRef, ForwardedRef, forwardRef, useMemo } from 'react'; +import { + Button, + BUTTON_VARIANT, + Modal as OdsModal, + ModalBody, + ModalContent, + MODAL_COLOR, + Spinner, + SPINNER_SIZE, + Text, + TEXT_PRESET, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { ModalProps } from './Modal.props'; + +export const Modal = forwardRef( + ( + { + heading, + type = MODAL_COLOR.information, + loading, + primaryButton, + secondaryButton, + onOpenChange, + dismissible = true, + open = true, + children, + step, + }: ModalProps, + ref: ForwardedRef>, + ) => { + const { t } = useTranslation(NAMESPACES.FORM); + const buttonColor = useMemo( + () => + type === MODAL_COLOR.critical + ? MODAL_COLOR.critical + : MODAL_COLOR.primary, + [type], + ); + + return ( + + + + {heading && ( +
+ + {heading} + + {Number.isInteger(step?.current) && + Number.isInteger(step?.total) && ( + + {t('stepPlaceholder', { + current: step.current, + total: step.total, + })} + + )} +
+ )} + {loading && ( +
+ +
+ )} + {!loading && ( + <> +
{children}
+
+ {secondaryButton?.label && ( + + )} + {primaryButton?.label && ( + + )} +
+ + )} +
+
+
+ ); + }, +); diff --git a/packages/manager-ui-kit/src/components/modal/Modal.props.ts b/packages/manager-ui-kit/src/components/modal/Modal.props.ts new file mode 100644 index 000000000000..3d534f998342 --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/Modal.props.ts @@ -0,0 +1,43 @@ +import { ReactNode, ComponentPropsWithRef } from 'react'; +import { Modal, MODAL_COLOR } from '@ovhcloud/ods-react'; + +type ModalButton = { + /** Label of the button */ + label: string; + /** loading state for primary button */ + loading?: boolean; + /** disabled state for primary button */ + disabled?: boolean; + /** Action of primary button */ + onClick?: () => void; + /** Test id of primary button */ + testId?: string; +}; + +export type ModalProps = ComponentPropsWithRef & { + /** Title of modal */ + heading?: string; + /** Type of modal. It can be any of `ODS_MODAL_COLOR` */ + type?: MODAL_COLOR; + /** Is loading state for display a spinner */ + loading?: boolean; + /** Primary button details */ + primaryButton?: ModalButton; + /** Secondary button details */ + secondaryButton?: ModalButton; + /** Properties for the step number displayed on the top right of the modal */ + step?: { + /** Current step displayed on the modal (must define heading and total) */ + current?: number; + /** Total number of steps in the modal (must defined heading and current) */ + total?: number; + }; + /** Display dismissible button */ + dismissible?: boolean; + /** Callback fired when the modal open state changes. */ + onOpenChange?: () => void; + /** Is modal open state */ + open?: boolean; + /** Children of modal */ + children?: ReactNode; +}; diff --git a/packages/manager-ui-kit/src/components/modal/__tests__/Modal.spec.tsx b/packages/manager-ui-kit/src/components/modal/__tests__/Modal.spec.tsx new file mode 100644 index 000000000000..a9e836f9a2bd --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/__tests__/Modal.spec.tsx @@ -0,0 +1,163 @@ +import { vi, describe, it, beforeEach } from 'vitest'; +import { MODAL_COLOR } from '@ovhcloud/ods-react'; +import { fireEvent, screen, within } from '@testing-library/react'; +import { renderModal, heading, ModalContent, actions } from './ModalTest.utils'; + +describe('Modal Tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('displays the basic modal', () => { + renderModal({ heading, children: }); + expect(screen.queryByText(heading)).toBeInTheDocument(); + expect(screen.queryByTestId('test-input')).toBeInTheDocument(); + }); + + it('displays the modal with actions and calls onClick', () => { + renderModal({ heading, children: , ...actions }); + const primaryButton = screen.getByTestId('primary-button'); + const secondaryButton = screen.getByTestId('secondary-button'); + + expect(primaryButton.textContent).toBe('Confirm'); + expect(primaryButton.className).toContain('default'); + expect(primaryButton).not.toBeDisabled(); + + expect(secondaryButton.textContent).toBe('Cancel'); + expect(secondaryButton.className).toContain('ghost'); + expect(secondaryButton).not.toBeDisabled(); + + fireEvent.click(primaryButton); + expect(actions.primaryButton.onClick).toHaveBeenCalled(); + + fireEvent.click(secondaryButton); + expect(actions.secondaryButton.onClick).toHaveBeenCalled(); + }); + + it('displays loading Modal', () => { + renderModal({ + heading, + children: , + ...actions, + loading: true, + }); + + expect(screen.queryByLabelText('Test Input')).not.toBeInTheDocument(); + expect(screen.queryByTestId('primary-button')).not.toBeInTheDocument(); + expect(screen.queryByTestId('secondary-button')).not.toBeInTheDocument(); + expect( + within(screen.getByTestId('spinner')).queryByRole('progressbar'), + ).toBeInTheDocument(); + }); + + it('displays disabled primary and secondary buttons', () => { + renderModal({ + heading, + children: , + ...{ + primaryButton: { + ...actions.primaryButton, + disabled: true, + }, + secondaryButton: { + ...actions.secondaryButton, + disabled: true, + }, + }, + }); + + const primaryButton = screen.getByTestId('primary-button'); + const secondaryButton = screen.getByTestId('secondary-button'); + + expect(primaryButton).toBeDisabled(); + expect(secondaryButton).toBeDisabled(); + + fireEvent.click(primaryButton); + expect(actions.primaryButton.onClick).not.toHaveBeenCalled(); + + fireEvent.click(secondaryButton); + expect(actions.secondaryButton.onClick).not.toHaveBeenCalled(); + }); + + it('displays loading primary and secondary buttons', () => { + renderModal({ + heading, + children: , + ...{ + primaryButton: { + ...actions.primaryButton, + loading: true, + }, + secondaryButton: { + ...actions.secondaryButton, + loading: true, + }, + }, + }); + + expect( + within(screen.getByTestId('primary-button')).queryByRole('progressbar'), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('secondary-button')).queryByRole('progressbar'), + ).toBeInTheDocument(); + }); + + it('displays he modal whith new test ids', () => { + renderModal({ + heading, + children: , + ...{ + primaryButton: { + ...actions.primaryButton, + testId: 'new-primary-button-testid', + }, + secondaryButton: { + ...actions.secondaryButton, + testId: 'new-secondary-button-testid', + }, + }, + }); + + expect(screen.getByTestId('new-primary-button-testid')).toBeInTheDocument(); + expect( + screen.getByTestId('new-secondary-button-testid'), + ).toBeInTheDocument(); + }); + + it('displays the modal with critical type', () => { + renderModal({ + heading, + children: , + ...actions, + type: MODAL_COLOR.critical, + }); + + const primaryButton = screen.getByTestId('primary-button'); + + expect(primaryButton.className).toContain('critical'); + }); + it('should display the basic modal with steps', () => { + renderModal({ + heading, + children: , + ...actions, + type: MODAL_COLOR.critical, + step: { current: 1, total: 5 }, + }); + expect(screen.getByTestId('step-placeholder')).toBeVisible(); + expect(screen.getByText(heading)).toBeVisible(); + }); + + it('should not display the step count', () => { + renderModal({ + heading, + children: , + ...actions, + type: MODAL_COLOR.critical, + step: { total: 5 }, + }); + expect(screen.queryByTestId('step-placeholder')).not.toBeInTheDocument(); + expect(screen.getByText(heading)).toBeVisible(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/modal/__tests__/Modal.test.tsx b/packages/manager-ui-kit/src/components/modal/__tests__/Modal.test.tsx new file mode 100644 index 000000000000..84731bc8e70a --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/__tests__/Modal.test.tsx @@ -0,0 +1,78 @@ +import { describe, it } from 'vitest'; +import { MODAL_COLOR } from '@ovhcloud/ods-react'; +import { renderModal, heading, actions, ModalContent } from './ModalTest.utils'; + +describe('Modal Snapshot Tests', () => { + it('displays the basic modal', () => { + const { baseElement } = renderModal({ + heading, + children: , + }); + expect(baseElement).toMatchSnapshot(); + }); + + it('displays the modal with actions', () => { + const { baseElement } = renderModal({ + heading, + children: , + ...actions, + }); + expect(baseElement).toMatchSnapshot(); + }); + + it('displays loading Modal', () => { + const { baseElement } = renderModal({ + heading, + children: , + ...actions, + loading: true, + }); + expect(baseElement).toMatchSnapshot(); + }); + + it('displays disabled primary and secondary buttons', () => { + const { baseElement } = renderModal({ + heading, + children: , + ...{ + primaryButton: { + ...actions.primaryButton, + disabled: true, + }, + secondaryButton: { + ...actions.secondaryButton, + disabled: true, + }, + }, + }); + expect(baseElement).toMatchSnapshot(); + }); + + it('displays loading primary and secondary buttons', () => { + const { baseElement } = renderModal({ + heading, + children: , + ...{ + primaryButton: { + ...actions.primaryButton, + loading: true, + }, + secondaryButton: { + ...actions.secondaryButton, + loading: true, + }, + }, + }); + expect(baseElement).toMatchSnapshot(); + }); + + it('displays the modal with critical type', () => { + const { baseElement } = renderModal({ + heading, + children: , + ...actions, + type: MODAL_COLOR.critical, + }); + expect(baseElement).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/modal/__tests__/ModalTest.utils.tsx b/packages/manager-ui-kit/src/components/modal/__tests__/ModalTest.utils.tsx new file mode 100644 index 000000000000..f76d3913063f --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/__tests__/ModalTest.utils.tsx @@ -0,0 +1,39 @@ +import { vi } from 'vitest'; +import { render } from '@/setupTest'; +import { Modal } from '../Modal.component'; + +export const heading = 'Example Heading'; + +export const ModalContent = () => ( +
+

+ Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus + ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus + duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar + vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl + malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class + aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos + himenaeos. +

+ + +
+); + +export const actions = { + primaryButton: { + label: 'Confirm', + loading: false, + disabled: false, + onClick: vi.fn(), + }, + secondaryButton: { + label: 'Cancel', + loading: false, + disabled: false, + onClick: vi.fn(), + }, +}; + +export const renderModal = ({ children, ...props }) => + render({children}); diff --git a/packages/manager-ui-kit/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap b/packages/manager-ui-kit/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap new file mode 100644 index 000000000000..c713e0cb5a2d --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap @@ -0,0 +1,684 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Modal Snapshot Tests > displays disabled primary and secondary buttons 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Modal Snapshot Tests > displays loading Modal 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Modal Snapshot Tests > displays loading primary and secondary buttons 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Modal Snapshot Tests > displays the basic modal 1`] = ` + +
+
+
+ + +`; + +exports[`Modal Snapshot Tests > displays the modal with actions 1`] = ` + +
+
+
+ +
+ +`; + +exports[`Modal Snapshot Tests > displays the modal with critical type 1`] = ` + +
+
+
+ +
+ +`; diff --git a/packages/manager-ui-kit/src/components/modal/index.ts b/packages/manager-ui-kit/src/components/modal/index.ts new file mode 100644 index 000000000000..d78cb7f222b6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/modal/index.ts @@ -0,0 +1,3 @@ +export { Modal } from './Modal.component'; + +export type { ModalProps } from './Modal.props'; diff --git a/packages/manager-ui-kit/src/components/notifications/Notifications.component.tsx b/packages/manager-ui-kit/src/components/notifications/Notifications.component.tsx new file mode 100644 index 000000000000..a26e3d96cbcf --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/Notifications.component.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState, FC } from 'react'; +import { useLocation } from 'react-router-dom'; +import { Message, MESSAGE_COLOR } from '@ovhcloud/ods-react'; +import { useNotifications } from './useNotifications'; +import { NotificationType, Notification } from './Notifications.type'; +import { NotificationProps } from './Notifications.props'; + +const NOTIFICATION_TYPE_MAP = { + [NotificationType.Success]: MESSAGE_COLOR.success, + [NotificationType.Error]: MESSAGE_COLOR.critical, + [NotificationType.Warning]: MESSAGE_COLOR.warning, + [NotificationType.Info]: MESSAGE_COLOR.information, +}; + +/** + * This component display the list of notifications. It acts + * as a "flash" component because by default once the notifications have been + * shown they are cleared. It means that you can use this component on multiple + * pages, switching page won't display notifications twice. + * + * It replicates the current behavior of public cloud notifications for + * actions (success / errors / etc) + */ +export const Notifications: FC = ({ + clearAfterRead = true, +}) => { + const location = useLocation(); + const [originLocation] = useState(location); + const { notifications, clearNotifications, clearNotification } = + useNotifications(); + + useEffect(() => { + if (clearAfterRead && originLocation.pathname !== location.pathname) + clearNotifications(); + }, [clearAfterRead, location.pathname]); + + return ( + <> + {notifications.map((notification: Notification) => ( + clearNotification(notification.uid)} + dismissible={notification.dismissible ?? true} + > + {notification.content} + + ))} + + ); +}; + +export default Notifications; diff --git a/packages/manager-ui-kit/src/components/notifications/Notifications.props.ts b/packages/manager-ui-kit/src/components/notifications/Notifications.props.ts new file mode 100644 index 000000000000..adea1325e0ff --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/Notifications.props.ts @@ -0,0 +1,4 @@ +export type NotificationProps = { + /** Clear notifications once they have been displayed (on location changes) */ + clearAfterRead?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/notifications/Notifications.type.ts b/packages/manager-ui-kit/src/components/notifications/Notifications.type.ts new file mode 100644 index 000000000000..34c088a9d1ca --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/Notifications.type.ts @@ -0,0 +1,33 @@ +import { ReactNode } from 'react'; + +export enum NotificationType { + Success = 'success', + Error = 'error', + Info = 'info', + Warning = 'warning', +} + +export interface Notification { + /** unique notification identifier */ + uid: number; + content: ReactNode; + type: NotificationType; + dismissible?: boolean; + creationTimestamp?: number; +} + +export interface NotificationState { + uid: number; + notifications: Notification[]; + addNotification: ( + content: ReactNode, + type: NotificationType, + dismissible?: boolean, + ) => void; + addSuccess: (content: ReactNode, dismissible?: boolean) => void; + addError: (content: ReactNode, dismissible?: boolean) => void; + addWarning: (content: ReactNode, dismissible?: boolean) => void; + addInfo: (content: ReactNode, dismissible?: boolean) => void; + clearNotification: (uid: number) => void; + clearNotifications: () => void; +} diff --git a/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.snapshot.test.tsx b/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.snapshot.test.tsx new file mode 100644 index 000000000000..224665bc8822 --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.snapshot.test.tsx @@ -0,0 +1,39 @@ +import { vitest } from 'vitest'; +import React from 'react'; +import { act, render, renderHook } from '@testing-library/react'; +import { + useNotifications, + NOTIFICATION_MINIMAL_DISPLAY_TIME, +} from '../useNotifications'; +import { Notifications } from '../Notifications.component'; +import { NotificationType } from '../Notifications.type'; + +vitest.useFakeTimers(); + +vitest.mock('react-router-dom', async () => ({ + ...(await vitest.importActual('react-router-dom')), + useLocation: () => ({ + pathname: '/foo', + }), +})); + +describe('Notifications component - Snapshot Testing', () => { + it('should list notifications', async () => { + let { container } = render(); + const { result } = renderHook(() => useNotifications()); + act(() => { + result.current.addNotification( + 'Notification-1', + NotificationType.Success, + ); + result.current.addNotification( + 'Notification-2', + NotificationType.Warning, + ); + result.current.addNotification('Notification-3', NotificationType.Info); + result.current.addNotification('Notification-4', NotificationType.Error); + }); + container = render().container; + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.spec.tsx b/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.spec.tsx new file mode 100644 index 000000000000..fae1724c6b31 --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/__tests__/Notifications.spec.tsx @@ -0,0 +1,194 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { useLocation } from 'react-router-dom'; +import { Notifications } from '../Notifications.component'; +import { useNotifications } from '../useNotifications'; + +vi.mock('@ovhcloud/ods-react', () => ({ + Message: vi.fn(({ color, dismissible, children, onRemove }) => ( +
+ {children} + {dismissible && ( + + )} +
+ )), + MESSAGE_COLOR: { + success: 'success', + critical: 'critical', + warning: 'warning', + information: 'information', + }, +})); + +// Mock react-router-dom's useLocation +vi.mock('react-router-dom', () => ({ + useLocation: vi.fn(), +})); + +// Mock custom hook +const mockClearNotifications = vi.fn(); +const mockClearNotification = vi.fn(); + +vi.mock('../useNotifications', () => ({ + useNotifications: vi.fn(() => ({ + notifications: [], + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + })), +})); + +// Reset mocks before each test +beforeEach(() => { + vi.clearAllMocks(); + (useLocation as any).mockReturnValue({ pathname: '/home' }); +}); + +describe('Notifications Component', () => { + const mockNotifications = [ + { + uid: '1', + content: 'This is a success message.', + type: 'success', + dismissible: true, + }, + { + uid: '2', + content: 'This is an error message.', + type: 'error', + dismissible: false, + }, + { + uid: '3', + content: 'This is an alert message.', + type: 'warning', + dismissible: false, + }, + { + uid: '4', + content: 'This is an information message.', + type: 'info', + }, + ]; + + it('should render all notifications', () => { + (useLocation as any).mockReturnValue({ pathname: '/home' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + render(); + + expect(screen.getByTestId('message-success')).toBeInTheDocument(); + expect(screen.getByTestId('message-critical')).toBeInTheDocument(); + expect(screen.getByTestId('message-warning')).toBeInTheDocument(); + expect(screen.getByTestId('message-information')).toBeInTheDocument(); + }); + + it('should map notification types to correct ODS Message colors', () => { + (useLocation as any).mockReturnValue({ pathname: '/home' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + const { getAllByTestId } = render(); + const messages = getAllByTestId(/message-/i); + + expect(messages[0]).toHaveClass('success'); + expect(messages[1]).toHaveClass('critical'); + expect(messages[2]).toHaveClass('warning'); + expect(messages[3]).toHaveClass('information'); + }); + + it('should call clearNotification when a dismissible notification is dismissed', async () => { + (useLocation as any).mockReturnValue({ pathname: '/home' }); + (useNotifications as any).mockReturnValue({ + notifications: [mockNotifications[0]], + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + render(); + const dismissButton = screen.getByTestId('dismissible-true'); + + fireEvent.click(dismissButton); + + await waitFor(() => { + expect(mockClearNotification).toHaveBeenCalledWith('1'); + }); + }); + + it('should not render dismiss button if dismissible is false', () => { + (useLocation as any).mockReturnValue({ pathname: '/home' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + render(); + const buttons = screen.getAllByTestId(/dismissible-/i); + + expect(buttons.length).toBe(2); + }); + + it('should clear notifications when location changes and clearAfterRead is true', () => { + (useLocation as any).mockReturnValue({ pathname: '/old-path' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + const { rerender } = render(); + + // Simulate route change + (useLocation as any).mockReturnValue({ pathname: '/new-path' }); + rerender(); + + expect(mockClearNotifications).toHaveBeenCalled(); + }); + + it('should NOT clear notifications when location changes and clearAfterRead is false', () => { + (useLocation as any).mockReturnValue({ pathname: '/old-path' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + const { rerender } = render(); + + // Simulate route change + (useLocation as any).mockReturnValue({ pathname: '/new-path' }); + rerender(); + + expect(mockClearNotifications).not.toHaveBeenCalled(); + }); + + it('should not clear notifications if same path', () => { + (useLocation as any).mockReturnValue({ pathname: '/same-path' }); + (useNotifications as any).mockReturnValue({ + notifications: mockNotifications, + clearNotifications: mockClearNotifications, + clearNotification: mockClearNotification, + }); + + const { rerender } = render(); + + rerender(); // re-render with same path + + expect(mockClearNotifications).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/notifications/__tests__/__snapshots__/Notifications.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/notifications/__tests__/__snapshots__/Notifications.snapshot.test.tsx.snap new file mode 100644 index 000000000000..953c72a420e1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/__tests__/__snapshots__/Notifications.snapshot.test.tsx.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Notifications component - Snapshot Testing > should list notifications 1`] = ` +
+
+ Notification-1 +
+
+ Notification-2 +
+
+ Notification-3 +
+
+ Notification-4 +
+
+`; diff --git a/packages/manager-ui-kit/src/components/notifications/__tests__/useNotifications.spec.ts b/packages/manager-ui-kit/src/components/notifications/__tests__/useNotifications.spec.ts new file mode 100644 index 000000000000..96437c4be2d0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/__tests__/useNotifications.spec.ts @@ -0,0 +1,168 @@ +import { vi, describe, it, expect } from 'vitest'; +import useNotifications, { + NOTIFICATION_MINIMAL_DISPLAY_TIME, +} from '../useNotifications'; +import { NotificationType } from '../Notifications.type'; + +describe('useNotifications Hook', () => { + beforeEach(() => { + // Reset state before each test + useNotifications.setState({ + uid: 0, + notifications: [], + }); + }); + + it('should initialize with empty notifications and uid = 0', () => { + const state = useNotifications.getState(); + expect(state.notifications).toEqual([]); + expect(state.uid).toBe(0); + }); + + it('should add a notification with correct properties', () => { + const content = 'Test notification'; + const type = NotificationType.Info; + const dismissible = true; + + useNotifications.getState().addNotification(content, type, dismissible); + + const state = useNotifications.getState(); + + expect(state.notifications).toHaveLength(1); + expect(state.notifications[0]).toMatchObject({ + content, + type, + dismissible, + uid: 0, // First UID starts at 0 + }); + expect(typeof state.notifications[0].creationTimestamp).toBe('number'); + }); + + it('should increment UID for each new notification', () => { + useNotifications + .getState() + .addNotification('1st', NotificationType.Success); + useNotifications + .getState() + .addNotification('2nd', NotificationType.Success); + useNotifications + .getState() + .addNotification('3rd', NotificationType.Success); + + const state = useNotifications.getState(); + + expect(state.notifications[0].uid).toBe(0); + expect(state.notifications[1].uid).toBe(1); + expect(state.notifications[2].uid).toBe(2); + }); + + it('should correctly add notification using helper methods', () => { + useNotifications.getState().addSuccess('Success message'); + useNotifications.getState().addError('Error message'); + useNotifications.getState().addWarning('Warning message'); + useNotifications.getState().addInfo('Info message'); + + const state = useNotifications.getState(); + + expect(state.notifications[0]).toMatchObject({ + content: 'Success message', + type: NotificationType.Success, + }); + expect(state.notifications[1]).toMatchObject({ + content: 'Error message', + type: NotificationType.Error, + }); + expect(state.notifications[2]).toMatchObject({ + content: 'Warning message', + type: NotificationType.Warning, + }); + expect(state.notifications[3]).toMatchObject({ + content: 'Info message', + type: NotificationType.Info, + }); + }); + + it('should clear a specific notification by UID', () => { + useNotifications + .getState() + .addNotification('1st', NotificationType.Success); + useNotifications + .getState() + .addNotification('2nd', NotificationType.Success); + useNotifications + .getState() + .addNotification('3rd', NotificationType.Success); + + useNotifications.getState().clearNotification(1); + + const state = useNotifications.getState(); + + expect(state.notifications).toHaveLength(2); + expect(state.notifications.some((n) => n.uid === 1)).toBe(false); + }); + + it('should keep notifications displayed less than minimal display time', () => { + const now = Date.now(); + + // Mock Date.now() to simulate consistent timing + vi.spyOn(Date, 'now').mockImplementation(() => now); + + useNotifications + .getState() + .addNotification('Recent', NotificationType.Success); + + // Simulate that this notification was shown just now + const recentTimestamp = now; + const oldTimestamp = now - NOTIFICATION_MINIMAL_DISPLAY_TIME - 100; + + useNotifications.setState({ + notifications: [ + { + uid: 0, + content: 'Recent', + type: NotificationType.Success, + dismissible: false, + creationTimestamp: recentTimestamp, + }, + { + uid: 1, + content: 'Old', + type: NotificationType.Error, + dismissible: true, + creationTimestamp: oldTimestamp, + }, + ], + }); + + useNotifications.getState().clearNotifications(); + + const state = useNotifications.getState(); + + // Old notification should be removed, recent should stay + expect(state.notifications).toHaveLength(1); + expect(state.notifications[0].content).toBe('Recent'); + }); + + it('should clear all notifications if they are older than minimal display time', () => { + const now = Date.now(); + vi.spyOn(Date, 'now').mockImplementation(() => now); + + useNotifications.setState({ + notifications: [ + { + uid: 0, + content: 'Old notification', + type: NotificationType.Success, + dismissible: false, + creationTimestamp: now - NOTIFICATION_MINIMAL_DISPLAY_TIME - 100, + }, + ], + }); + + useNotifications.getState().clearNotifications(); + + const state = useNotifications.getState(); + + expect(state.notifications).toHaveLength(0); + }); +}); diff --git a/packages/manager-ui-kit/src/components/notifications/index.ts b/packages/manager-ui-kit/src/components/notifications/index.ts new file mode 100644 index 000000000000..f2c2f2609c2c --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/index.ts @@ -0,0 +1,5 @@ +export { Notifications } from './Notifications.component'; + +export type { NotificationProps } from './Notifications.props'; + +export { useNotifications } from './useNotifications'; diff --git a/packages/manager-ui-kit/src/components/notifications/useNotifications.ts b/packages/manager-ui-kit/src/components/notifications/useNotifications.ts new file mode 100644 index 000000000000..e7a1f827f7ea --- /dev/null +++ b/packages/manager-ui-kit/src/components/notifications/useNotifications.ts @@ -0,0 +1,53 @@ +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { NotificationType, NotificationState } from './Notifications.type'; + +export const NOTIFICATION_MINIMAL_DISPLAY_TIME = 1000; + +export const useNotifications = create((set, get) => ({ + uid: 0, + notifications: [], + addNotification: ( + content: ReactNode, + type: NotificationType, + dismissible = false, + ) => + set((state) => ({ + uid: state.uid + 1, + notifications: [ + ...state.notifications, + { + uid: state.uid, + content, + type, + dismissible, + creationTimestamp: Date.now(), + }, + ], + })), + addSuccess: (content: ReactNode, dismissible = true) => + get().addNotification(content, NotificationType.Success, dismissible), + addError: (content: ReactNode, dismissible = true) => + get().addNotification(content, NotificationType.Error, dismissible), + addWarning: (content: ReactNode, dismissible = true) => + get().addNotification(content, NotificationType.Warning, dismissible), + addInfo: (content: ReactNode, dismissible = true) => + get().addNotification(content, NotificationType.Info, dismissible), + clearNotification: (toRemoveUid: number) => + set((state) => ({ + notifications: state.notifications.filter( + ({ uid }) => uid !== toRemoveUid, + ), + })), + clearNotifications: () => + set((state) => ({ + notifications: state.notifications.filter( + (notification) => + notification.creationTimestamp && + Date.now() - notification.creationTimestamp < + NOTIFICATION_MINIMAL_DISPLAY_TIME, + ), + })), +})); + +export default useNotifications; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.component.tsx b/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.component.tsx new file mode 100644 index 000000000000..77b507dd517b --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.component.tsx @@ -0,0 +1,68 @@ +import { FC } from 'react'; +import { TEXT_PRESET } from '@ovhcloud/ods-react'; +import { Text } from '../text'; +import { OnboardingLayoutButton } from './onboarding-layout-button'; +import { OnboardingLayoutProps } from './OnboardingLayout.type'; +import placeholderSrc from '../../../public/assets/placeholder.png'; + +export const OnboardingLayout: FC = ({ + hideHeadingSection, + title, + description, + orderButtonLabel, + orderHref, + isActionDisabled, + orderIam, + onOrderButtonClick, + moreInfoHref, + moreInfoButtonLabel, + moreInfoButtonIcon, + isMoreInfoButtonDisabled, + img = {}, + children, +}) => { + const { className: imgClassName, alt: altText, ...imgProps } = img; + return ( +
+ {!hideHeadingSection && ( +
+ {(img?.src || placeholderSrc) && ( +
+ {altText +
+ )} + + {title} + + {description} + +
+ )} + {children && ( +
+ {children} +
+ )} +
+ ); +}; + +export default OnboardingLayout; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.type.ts b/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.type.ts new file mode 100644 index 000000000000..90a80bb03eab --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/OnboardingLayout.type.ts @@ -0,0 +1,10 @@ +import { ReactNode, ComponentProps, PropsWithChildren } from 'react'; +import { OnboardingLayoutButtonProps } from './onboarding-layout-button'; + +export type OnboardingLayoutProps = OnboardingLayoutButtonProps & + PropsWithChildren<{ + hideHeadingSection?: boolean; + title: string; + description?: ReactNode; + img?: ComponentProps<'img'>; + }>; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.snapshot.test.tsx b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.snapshot.test.tsx new file mode 100644 index 000000000000..9c86d55b9a12 --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.snapshot.test.tsx @@ -0,0 +1,441 @@ +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { render } from '@/setupTest'; +import { OnboardingLayout } from '../OnboardingLayout.component'; +import { LinkCard } from '../../link-card/LinkCard.component'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe('OnboardingLayout Snapshot Tests', () => { + afterEach(() => { + vitest.resetAllMocks(); + }); + + describe('Basic OnboardingLayout States', () => { + it('should render basic onboarding layout with title and description', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with image', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with buttons', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with hidden heading section', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('OnboardingLayout with LinkCard Children', () => { + it('should render onboarding layout with single LinkCard', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with multiple LinkCards', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + Get started with your cloud journey

} + > + + + +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with LinkCards and buttons', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + Get started with your cloud journey

} + orderButtonLabel="Get Started" + orderHref="https://ovhcloud.com" + moreInfoButtonLabel="Learn More" + moreInfoHref="https://docs.ovhcloud.com" + > + + +
, + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('OnboardingLayout with Complex LinkCard Configurations', () => { + it('should render onboarding layout with LinkCards having images and badges', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with LinkCards and custom onClick handlers', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const mockOnClick = vitest.fn(); + + const { container } = render( + + + + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Edge Cases', () => { + it('should render onboarding layout with empty children', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + {null} + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with mixed children types', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + +
Custom content
+ +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render onboarding layout with disabled buttons', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Responsive Layout Tests', () => { + it('should render onboarding layout with many LinkCards for grid testing', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + + const { container } = render( + + {Array.from({ length: 6 }, (_, index) => ( + + ))} + , + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.spec.tsx b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.spec.tsx new file mode 100644 index 000000000000..4d6f32d5dad2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/OnboardingLayout.spec.tsx @@ -0,0 +1,132 @@ +import { vi, vitest } from 'vitest'; +import { fireEvent, screen, act } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { Text } from '../../text'; +import { OnboardingLayout } from '../index'; +import placeholderSrc from '../../../../public/assets/placeholder.png'; +import { useAuthorizationIam } from '../../../hooks/iam'; +import { IamAuthorizationResponse } from '../../../hooks/iam/iam.interface'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = + useAuthorizationIam as unknown as jest.Mock; +const customTitle = 'onboarding title'; +const imgAltText = 'img alt text'; +const descriptionText = 'description text'; +const orderBtnLabel = 'Order Now'; +const infoBtnLabel = 'more info'; +const children = <>Test Onboarding 1; + +describe('specs:onboarding', () => { + describe('default content', () => { + it('displays default content', () => { + render(); + expect(screen.getByText(customTitle)).toBeVisible(); + expect(screen.getByAltText('placeholder image')).toBeVisible(); + }); + }); + describe('additional contents', () => { + it('displays description correctly', () => { + render( + + {descriptionText} + + } + />, + ); + expect(screen.getByText(descriptionText)).toBeVisible(); + }); + it('displays img correctly', () => { + render( + , + ); + expect(screen.getByAltText(imgAltText)).toBeVisible(); + }); + it('disable order button with false value for useAuthorizationIam', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: true, + isFetched: true, + }); + render( + , + ); + const orderButton = screen.getByText(orderBtnLabel); + expect(orderButton).toBeVisible(); + expect(orderButton).toHaveAttribute('disabled'); + }); + it('displays order button correctly', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + const onOrderButtonClick = vi.fn(); + + render( + , + ); + const orderButton = screen.getByText(orderBtnLabel); + expect(orderButton).toBeVisible(); + act(() => fireEvent.click(orderButton)); + expect(onOrderButtonClick).toHaveBeenCalledTimes(1); + }); + it('disable buttons', () => { + render( + , + ); + const orderButton = screen.getByText(orderBtnLabel); + const moreInfoButton = screen.getByText(infoBtnLabel); + expect(orderButton).toHaveAttribute('disabled'); + expect(moreInfoButton).toHaveAttribute('disabled'); + }); + + it('displays children correctly', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + render( + {children}, + ); + const card = screen.getByText('Test Onboarding 1'); + expect(card).toBeVisible(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/__snapshots__/OnboardingLayout.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/__snapshots__/OnboardingLayout.snapshot.test.tsx.snap new file mode 100644 index 000000000000..28c446d52a4e --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/__tests__/__snapshots__/OnboardingLayout.snapshot.test.tsx.snap @@ -0,0 +1,1428 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`OnboardingLayout Snapshot Tests > Basic OnboardingLayout States > should render basic onboarding layout with title and description 1`] = ` +
+
+
+
+ placeholder image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Basic OnboardingLayout States > should render onboarding layout with buttons 1`] = ` +
+
+
+
+ placeholder image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+ + +
+
+
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Basic OnboardingLayout States > should render onboarding layout with hidden heading section 1`] = ` +
+
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Basic OnboardingLayout States > should render onboarding layout with image 1`] = ` +
+
+
+
+ Welcome image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Edge Cases > should render onboarding layout with disabled buttons 1`] = ` +
+
+
+
+ placeholder image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+ + +
+
+ +
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Edge Cases > should render onboarding layout with empty children 1`] = ` +
+
+
+
+ placeholder image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Edge Cases > should render onboarding layout with mixed children types 1`] = ` + +`; + +exports[`OnboardingLayout Snapshot Tests > OnboardingLayout with Complex LinkCard Configurations > should render onboarding layout with LinkCards and custom onClick handlers 1`] = ` + +`; + +exports[`OnboardingLayout Snapshot Tests > OnboardingLayout with Complex LinkCard Configurations > should render onboarding layout with LinkCards having images and badges 1`] = ` + +`; + +exports[`OnboardingLayout Snapshot Tests > OnboardingLayout with LinkCard Children > should render onboarding layout with LinkCards and buttons 1`] = ` + +`; + +exports[`OnboardingLayout Snapshot Tests > OnboardingLayout with LinkCard Children > should render onboarding layout with multiple LinkCards 1`] = ` + +`; + +exports[`OnboardingLayout Snapshot Tests > OnboardingLayout with LinkCard Children > should render onboarding layout with single LinkCard 1`] = ` +
+
+
+
+ placeholder image +
+

+ Welcome to OVHcloud +

+ Get started with your cloud journey +
+ +
+
+`; + +exports[`OnboardingLayout Snapshot Tests > Responsive Layout Tests > should render onboarding layout with many LinkCards for grid testing 1`] = ` + +`; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/index.ts b/packages/manager-ui-kit/src/components/onboarding-layout/index.ts new file mode 100644 index 000000000000..639c03d80a6d --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/index.ts @@ -0,0 +1,3 @@ +export { OnboardingLayout } from './OnboardingLayout.component'; +export { OnboardingLayoutButton } from './onboarding-layout-button'; +export * from './onboarding-layout-button'; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.component.tsx b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.component.tsx new file mode 100644 index 000000000000..be1c58b6b180 --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.component.tsx @@ -0,0 +1,68 @@ +import { FC } from 'react'; +import { + Icon, + ICON_NAME, + BUTTON_SIZE, + BUTTON_VARIANT, +} from '@ovhcloud/ods-react'; +import { Button } from '../../button'; +import { OnboardingLayoutButtonProps } from './OnboardingLayoutButton.type'; + +const OnboardingLayoutButton: FC = ({ + orderButtonLabel, + orderHref, + onOrderButtonClick, + isActionDisabled, + orderIam, + moreInfoHref, + moreInfoButtonLabel, + moreInfoButtonIcon = ICON_NAME.externalLink, + isMoreInfoButtonDisabled, +}) => { + if (!orderButtonLabel && !moreInfoButtonLabel) { + return null; + } + + return ( +
+ {orderButtonLabel && (onOrderButtonClick || orderHref) && ( + + )} + {moreInfoButtonLabel && moreInfoHref && ( + + )} +
+ ); +}; + +export default OnboardingLayoutButton; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.type.ts b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.type.ts new file mode 100644 index 000000000000..363494b18af0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/OnboardingLayoutButton.type.ts @@ -0,0 +1,17 @@ +import { ICON_NAME } from '@ovhcloud/ods-react'; + +export type OnboardingLayoutButtonProps = { + orderButtonLabel?: string; + orderHref?: string; + onOrderButtonClick?: () => void; + isActionDisabled?: boolean; + orderIam?: { + urn: string; + iamActions: string[]; + displayTooltip?: boolean; + }; + moreInfoHref?: string; + moreInfoButtonIcon?: ICON_NAME; + moreInfoButtonLabel?: string; + isMoreInfoButtonDisabled?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/index.ts b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/index.ts new file mode 100644 index 000000000000..f1630efad006 --- /dev/null +++ b/packages/manager-ui-kit/src/components/onboarding-layout/onboarding-layout-button/index.ts @@ -0,0 +1,2 @@ +export { default as OnboardingLayoutButton } from './OnboardingLayoutButton.component'; +export type { OnboardingLayoutButtonProps } from './OnboardingLayoutButton.type'; diff --git a/packages/manager-ui-kit/src/components/order/Order.component.tsx b/packages/manager-ui-kit/src/components/order/Order.component.tsx new file mode 100644 index 000000000000..7be6a9446d02 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/Order.component.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; +import { OrderContextProvider } from './Order.context'; +import { OrderConfiguration } from './order-configuration/OrderConfiguration.component'; +import { OrderSummary } from './order-summary/OrderSummary.component'; +import './translations'; + +export const Order = ({ children }: PropsWithChildren) => { + return {children}; +}; + +Order.Configuration = OrderConfiguration; +Order.Summary = OrderSummary; diff --git a/packages/manager-ui-kit/src/components/order/Order.context.tsx b/packages/manager-ui-kit/src/components/order/Order.context.tsx new file mode 100644 index 000000000000..6fffca7bff0f --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/Order.context.tsx @@ -0,0 +1,28 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { TOrderContext } from './Order.type'; + +const OrderContext = createContext({} as TOrderContext); + +export const OrderContextProvider = ({ children }: React.PropsWithChildren) => { + const [isOrderInitialized, setIsOrderInitialized] = useState(false); + useState(false); + + const context = useMemo( + () => ({ + isOrderInitialized, + setIsOrderInitialized, + }), + [isOrderInitialized], + ); + + return ( + {children} + ); +}; + +export const useOrderContext = (): TOrderContext => { + const context = useContext(OrderContext); + let error = false; + if (context === undefined) error = true; + return { ...context, error }; +}; diff --git a/packages/manager-ui-kit/src/components/order/Order.type.ts b/packages/manager-ui-kit/src/components/order/Order.type.ts new file mode 100644 index 000000000000..566ad4bcc3b6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/Order.type.ts @@ -0,0 +1,5 @@ +export type TOrderContext = { + setIsOrderInitialized: (isOrderInitialized: boolean) => void; + isOrderInitialized: boolean; + error?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/order/__tests__/Order.snapshot.test.tsx b/packages/manager-ui-kit/src/components/order/__tests__/Order.snapshot.test.tsx new file mode 100644 index 000000000000..ddf7dd79cb29 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/__tests__/Order.snapshot.test.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { render } from '@/setupTest'; +import { Order } from '../Order.component'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe('Order Snapshot Tests', () => { + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }); + }); + + afterEach(() => { + vitest.resetAllMocks(); + }); + + it('should render basic Order component with children', () => { + const { container } = render( + +
Test content
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with Configuration component', () => { + const { container } = render( + + {}} + onConfirm={() => {}} + > +

Configuration content

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with Summary component', () => { + const { container } = render( + + {}} + orderLink="https://example.com" + productName="Test Product" + /> + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with both Configuration and Summary components', () => { + const { container } = render( + + {}} + onConfirm={() => {}} + > +

Configuration steps

+
+ {}} + orderLink="https://example.com/order" + productName="Cloud Service" + /> +
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with Configuration in invalid state', () => { + const { container } = render( + + {}} + onConfirm={() => {}} + > +

Invalid configuration

+
+
, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with Summary without product name', () => { + const { container } = render( + + {}} orderLink="https://example.com" /> + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render Order with complex nested content', () => { + const { container } = render( + + {}} + onConfirm={() => {}} + > +
+

Order Steps

+
    +
  • Step 1: Choose service
  • +
  • Step 2: Configure options
  • +
  • Step 3: Review and confirm
  • +
+
+
+ {}} + orderLink="https://example.com/order" + productName="Premium Cloud Service" + onClickLink={() => {}} + /> +
, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/order/__tests__/Order.spec.tsx b/packages/manager-ui-kit/src/components/order/__tests__/Order.spec.tsx new file mode 100644 index 000000000000..dd7815a0c49f --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/__tests__/Order.spec.tsx @@ -0,0 +1,144 @@ +import { fireEvent, waitFor } from '@testing-library/react'; +import { vi, vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { Order } from '../Order.component'; +import { render } from '@/setupTest'; +import { useAuthorizationIam } from '../../../hooks/iam'; +import fr_FR from '../translations/Messages_fr_FR.json'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe(' tests suite', () => { + // Mock global window.open + vi.stubGlobal('open', vi.fn()); + + const onCancelSpy = vi.fn(); + const onValidateSpy = vi.fn(); + const onFinishSpy = vi.fn(); + const onClickLinkSpy = vi.fn(); + const orderLink = 'https://order-link'; + + beforeEach(() => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: true, + isFetched: true, + }); + }); + + afterEach(() => { + vitest.resetAllMocks(); + }); + + const renderComponent = ( + isValid: boolean, + link: string, + productName: string, + ) => + render( + + +

Order steps

+
+ +
, + ); + + it.each([{ valid: true }, { valid: false }])( + 'when order configuration validity is $valid confirm button disabled attribute should be $valid', + ({ valid }) => { + const { getByText } = renderComponent(valid, '', ''); + + expect(getByText('Order steps')).toBeVisible(); + expect(getByText(fr_FR.order_configuration_order)).toBeVisible(); + }, + ); + + it('confirm button should be enabled and clickable when order configuration is valid', () => { + const { getByText } = renderComponent(true, '', ''); + + fireEvent.click(getByText(fr_FR.order_configuration_order)); + expect(onValidateSpy).toHaveBeenCalled(); + }); + + it('should cancel order configuration when cancel button is clicked', () => { + const { getByText } = renderComponent(false, '', ''); + + fireEvent.click(getByText(fr_FR.order_configuration_cancel)); + expect(onCancelSpy).toHaveBeenCalled(); + }); + + it('should open order link and display order summary when order configuration is confirmed ', () => { + vi.spyOn(window, 'open'); + const { getByTestId, queryByText } = renderComponent(true, orderLink, ''); + + fireEvent.click(getByTestId('cta-order-configuration-order')); + + // order configuration is hidden + expect(queryByText('Order steps')).not.toBeInTheDocument(); + + expect(getByTestId('order-summary-title')).toBeVisible(); + expect(getByTestId('order-summary-link')).toBeVisible(); + + expect(window.open).toHaveBeenCalledTimes(1); + expect(window.open).toHaveBeenCalledWith( + orderLink, + '_blank', + 'noopener,noreferrer', + ); + }); + + it('should open order link when order link is clicked', () => { + vi.spyOn(window, 'open'); + const { getByTestId } = renderComponent(true, orderLink, ''); + + fireEvent.click(getByTestId('cta-order-configuration-order')); + fireEvent.click(getByTestId('order-summary-link')); + + waitFor(() => expect(onClickLinkSpy).toHaveBeenCalled()); + }); + + it('should close order summary when finish button is clicked', () => { + vi.spyOn(window, 'open'); + const { getByTestId, getByText } = renderComponent(true, orderLink, ''); + + fireEvent.click(getByText(fr_FR.order_configuration_order)); + fireEvent.click(getByTestId('cta-order-summary-finish')); + + expect(onFinishSpy).toHaveBeenCalled(); + expect(getByText(fr_FR.order_configuration_order)).toBeVisible(); + }); + + it.each([{ productName: '' }, { productName: 'OVHcloud product' }])( + 'should display given product name with value $productName', + ({ productName }) => { + vi.spyOn(window, 'open'); + const { getByTestId, getByText } = renderComponent( + true, + orderLink, + productName, + ); + fireEvent.click(getByText(fr_FR.order_configuration_order)); + fireEvent.click(getByTestId('order-summary-link')); + const product = productName || 'service'; + expect(getByText(`Commande de votre ${product} initiée`)).toBeVisible(); + }, + ); +}); diff --git a/packages/manager-ui-kit/src/components/order/__tests__/__snapshots__/Order.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/order/__tests__/__snapshots__/Order.snapshot.test.tsx.snap new file mode 100644 index 000000000000..9985b7c85459 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/__tests__/__snapshots__/Order.snapshot.test.tsx.snap @@ -0,0 +1,146 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Order Snapshot Tests > should render Order with Configuration component 1`] = ` +
+

+ Configuration content +

+
+ + +
+
+`; + +exports[`Order Snapshot Tests > should render Order with Configuration in invalid state 1`] = ` +
+

+ Invalid configuration +

+
+ + +
+
+`; + +exports[`Order Snapshot Tests > should render Order with Summary component 1`] = `
`; + +exports[`Order Snapshot Tests > should render Order with Summary without product name 1`] = `
`; + +exports[`Order Snapshot Tests > should render Order with both Configuration and Summary components 1`] = ` +
+

+ Configuration steps +

+
+ + +
+
+`; + +exports[`Order Snapshot Tests > should render Order with complex nested content 1`] = ` +
+
+

+ Order Steps +

+
    +
  • + Step 1: Choose service +
  • +
  • + Step 2: Configure options +
  • +
  • + Step 3: Review and confirm +
  • +
+
+
+ + +
+
+`; + +exports[`Order Snapshot Tests > should render basic Order component with children 1`] = ` +
+
+ Test content +
+
+`; diff --git a/packages/manager-ui-kit/src/components/order/index.ts b/packages/manager-ui-kit/src/components/order/index.ts new file mode 100644 index 000000000000..6cbeb3b99285 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/index.ts @@ -0,0 +1,3 @@ +export { Order } from './Order.component'; +export { useOrderContext } from './Order.context'; +export type { TOrderContext } from './Order.type'; diff --git a/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.component.tsx b/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.component.tsx new file mode 100644 index 000000000000..5b84b1bd7beb --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.component.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BUTTON_VARIANT, ICON_NAME, Icon } from '@ovhcloud/ods-react'; +import { useOrderContext } from '../Order.context'; +import { Button } from '../../button'; +import { OrderConfigurationProps } from './OrderConfiguration.props'; +import { Text } from '../../text'; + +export const OrderConfiguration: FC = ({ + children, + onCancel, + onConfirm, + isValid, +}: OrderConfigurationProps): JSX.Element => { + const { isOrderInitialized, setIsOrderInitialized, error } = + useOrderContext(); + const { t } = useTranslation('order'); + + if (isOrderInitialized) { + return <>; + } + + if (error) { + return {t('order_error_loading')}; + } + + return ( + <> + {children} +
+ + +
+ + ); +}; diff --git a/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.props.ts b/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.props.ts new file mode 100644 index 000000000000..7430f92a64e9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/order-configuration/OrderConfiguration.props.ts @@ -0,0 +1,8 @@ +import { ReactNode } from 'react'; + +export type OrderConfigurationProps = { + children: ReactNode; + onCancel: () => void; + onConfirm: () => void; + isValid: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.component.tsx b/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.component.tsx new file mode 100644 index 000000000000..7bcc91e19799 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.component.tsx @@ -0,0 +1,69 @@ +import { useEffect, FC } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { TEXT_PRESET } from '@ovhcloud/ods-react'; +import { useOrderContext } from '../Order.context'; +import { Link, LinkType } from '../../Link'; +import { Button } from '../../button'; +import { Text } from '../../text'; +import { OrderSummaryProps } from './OrderSummary.type'; + +export const OrderSummary: FC = ({ + onFinish, + onClickLink, + orderLink, + productName, +}: OrderSummaryProps): JSX.Element => { + const { t } = useTranslation('order'); + const { isOrderInitialized, setIsOrderInitialized } = useOrderContext(); + + useEffect(() => { + if (orderLink && isOrderInitialized) { + window.open(orderLink, '_blank', 'noopener,noreferrer'); + } + }, [orderLink, isOrderInitialized]); + + if (!isOrderInitialized) { + return <>; + } + + // set default label if no product name provided + const product = productName || t('order_summary_product_default_label'); + + return ( +
+ + {t('order_summary_order_initiated_title', { product })} + + + + ), + }} + /> + + + {t('order_summary_order_initiated_info', { product })} + + +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.type.ts b/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.type.ts new file mode 100644 index 000000000000..82f788062c5b --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/order-summary/OrderSummary.type.ts @@ -0,0 +1,6 @@ +export type OrderSummaryProps = { + onFinish: () => void; + onClickLink?: () => void; + orderLink: string; + productName?: string; +}; diff --git a/packages/manager-react-components/src/components/order/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/order/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/order/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/order/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/order/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/order/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/order/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/order/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_fr_CA.json diff --git a/packages/manager-ui-kit/src/components/order/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/order/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..35efa39bf528 --- /dev/null +++ b/packages/manager-ui-kit/src/components/order/translations/Messages_fr_FR.json @@ -0,0 +1,10 @@ +{ + "order_configuration_cancel": "Annuler", + "order_configuration_order": "Commander", + "order_summary_finish": "Terminer", + "order_summary_product_default_label": "service", + "order_summary_order_initiated_title": "Commande de votre {{product}} initiée", + "order_summary_order_initiated_subtitle": "Si vous n'avez pas pu finaliser votre commande, merci de la compléter en cliquant sur le lien suivant", + "order_summary_order_initiated_info": "Nous vous informerons de la disponibilité de votre {{product}} par e-mail.", + "order_error_loading": "Une erreur est survenue lors du chargement de la page." +} diff --git a/packages/manager-react-components/src/components/order/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/order/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/order/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/order/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/order/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/order/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/order/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/order/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/order/translations/index.ts b/packages/manager-ui-kit/src/components/order/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/order/translations/index.ts rename to packages/manager-ui-kit/src/components/order/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/price/Price.component.tsx b/packages/manager-ui-kit/src/components/price/Price.component.tsx new file mode 100644 index 000000000000..64952bdd085b --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/Price.component.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Text } from '@ovhcloud/ods-react'; + +import { IntervalUnitType } from '../../enumTypes'; +import { PriceProps } from './Price.props'; +import { + getPrice, + convertIntervalPrice, + getPriceTextFormatted, + checkAsianFormat, + checkGermanFormat, + checkFranceFormat, + checkUSFormat, +} from './Price.utils'; +import './translations'; + +import { PriceText, PriceTextPreset } from './price-text'; + +export function Price({ + value, + intervalUnit, + tax = 0, + ovhSubsidiary, + locale, + isConvertIntervalUnit, +}: Readonly) { + const { t } = useTranslation('price'); + + const isAsiaFormat = checkAsianFormat(ovhSubsidiary); + const isGermanFormat = checkGermanFormat(ovhSubsidiary); + const isFrenchFormat = checkFranceFormat(ovhSubsidiary); + const isUSFormat = checkUSFormat(ovhSubsidiary); + + const convertedValue = + isConvertIntervalUnit && intervalUnit + ? convertIntervalPrice(value, intervalUnit) + : value; + + const convertedTax = + isConvertIntervalUnit && intervalUnit + ? convertIntervalPrice(tax, intervalUnit) + : tax; + + const priceWithoutTax = getPriceTextFormatted( + ovhSubsidiary, + locale, + getPrice(convertedValue), + ); + + const priceWithTax = getPriceTextFormatted( + ovhSubsidiary, + locale, + getPrice(convertedValue, convertedTax), + ); + + const intervalUnitText = + intervalUnit && intervalUnit !== IntervalUnitType.none + ? t(`price_per_${intervalUnit}`) + : ''; + + const components = [ + { + condition: value === 0, + component: {t('price_free')}, + }, + { + condition: isFrenchFormat && tax > 0, + component: ( + <> + + + + ), + }, + { + condition: isFrenchFormat && !tax, + component: ( + + ), + }, + { + condition: isGermanFormat && tax > 0, + component: ( + + ), + }, + { + condition: isAsiaFormat && (!tax || tax === 0), + component: ( + + ), + }, + { + condition: isAsiaFormat, + component: ( + <> + + + + ), + }, + { + condition: isUSFormat, + component: ( + + ), + }, + ]; + + const matchingComponent = components.find(({ condition }) => condition); + if (!matchingComponent) { + return null; + } + + return {matchingComponent.component}; +} + +export default Price; diff --git a/packages/manager-ui-kit/src/components/price/Price.constants.ts b/packages/manager-ui-kit/src/components/price/Price.constants.ts new file mode 100644 index 000000000000..612f00941dfd --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/Price.constants.ts @@ -0,0 +1,20 @@ +export const ASIAN_FORMAT_SUBSIDIARIES = ['ASIA', 'AU', 'IN', 'SG']; + +export const GERMAN_FORMAT_SUBSIDIARIES = ['DE', 'FI', 'SN']; + +export const FRENCH_FORMAT_SUBSIDIARIES = [ + 'CZ', + 'ES', + 'FR', + 'GB', + 'IE', + 'IT', + 'LT', + 'MA', + 'NL', + 'PL', + 'PT', + 'TN', +]; + +export const US_FORMAT_SUBSIDIARIES = ['CA', 'QC', 'US', 'WE', 'WS']; diff --git a/packages/manager-ui-kit/src/components/price/Price.props.ts b/packages/manager-ui-kit/src/components/price/Price.props.ts new file mode 100644 index 000000000000..8cd6b3f32c1b --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/Price.props.ts @@ -0,0 +1,16 @@ +import { IntervalUnitType, OvhSubsidiary } from '../../enumTypes'; + +export type PriceProps = { + /** The price value to display */ + value: number; + /** The tax value to display */ + tax?: number; + /** The interval unit for the price (day, month, year) */ + intervalUnit?: IntervalUnitType; + /** The OVH subsidiary to determine price format */ + ovhSubsidiary: OvhSubsidiary; + /** Whether to convert the price based on interval unit */ + isConvertIntervalUnit?: boolean; + /** The locale for price formatting */ + locale: string; +}; diff --git a/packages/manager-ui-kit/src/components/price/Price.utils.ts b/packages/manager-ui-kit/src/components/price/Price.utils.ts new file mode 100644 index 000000000000..24518bc25525 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/Price.utils.ts @@ -0,0 +1,68 @@ +import { + OvhSubsidiary, + IntervalUnitType, + OVH_CURRENCY_BY_SUBSIDIARY, +} from '../../enumTypes'; +import { + ASIAN_FORMAT_SUBSIDIARIES, + GERMAN_FORMAT_SUBSIDIARIES, + FRENCH_FORMAT_SUBSIDIARIES, + US_FORMAT_SUBSIDIARIES, +} from './Price.constants'; + +export const getPrice = (value: number, tax?: number): number => { + const valueWithTax = tax ? value + tax : value; + return valueWithTax / 100000000; +}; + +export const convertIntervalPrice = ( + price: number, + intervalUnit: IntervalUnitType, +): number => { + const conversionRates = { + [IntervalUnitType.day]: price / 365, + [IntervalUnitType.month]: price / 12, + [IntervalUnitType.year]: price, + [IntervalUnitType.none]: price, + }; + + return conversionRates[intervalUnit] || price; +}; + +export const getPriceTextFormatted = ( + ovhSubsidiary: OvhSubsidiary, + locale: string, + priceValue: number, +): string => { + try { + return new Intl.NumberFormat(locale.replace('_', '-'), { + style: 'currency', + currency: OVH_CURRENCY_BY_SUBSIDIARY[ovhSubsidiary], + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(priceValue); + } catch (e) { + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: OVH_CURRENCY_BY_SUBSIDIARY[ovhSubsidiary], + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(priceValue); + } +}; + +export const checkAsianFormat = (subsidiary: string) => { + return ASIAN_FORMAT_SUBSIDIARIES.includes(subsidiary); +}; + +export const checkGermanFormat = (subsidiary: string) => { + return GERMAN_FORMAT_SUBSIDIARIES.includes(subsidiary); +}; + +export const checkFranceFormat = (subsidiary: string) => { + return FRENCH_FORMAT_SUBSIDIARIES.includes(subsidiary); +}; + +export const checkUSFormat = (subsidiary: string) => { + return US_FORMAT_SUBSIDIARIES.includes(subsidiary); +}; diff --git a/packages/manager-ui-kit/src/components/price/__tests__/Price.snapshot.test.tsx b/packages/manager-ui-kit/src/components/price/__tests__/Price.snapshot.test.tsx new file mode 100644 index 000000000000..3c36315881e6 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/__tests__/Price.snapshot.test.tsx @@ -0,0 +1,77 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render } from '@testing-library/react'; + +import Price from '../Price.component'; +import { OvhSubsidiary, IntervalUnitType } from '../../../enumTypes'; + +describe('Price Component', () => { + const defaultProps = { + value: 1000000000, + tax: 200000000, + intervalUnit: IntervalUnitType.month, + ovhSubsidiary: OvhSubsidiary.FR, + locale: 'fr-FR', + isConvertIntervalUnit: false, + }; + + it('renders free price when value is 0', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('displays both HT and TTC in French format when tax > 0', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('displays only HT in French format when no tax', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('displays price with tax in German format', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('displays only GST-excl in Asia format if no tax', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('displays both GST-excl and GST-incl in Asia format if tax exists', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('displays price without tax label in US format', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('handles different interval units correctly', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('does not show intervalUnitText if intervalUnit is "none"', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/price/__tests__/Price.spec.tsx b/packages/manager-ui-kit/src/components/price/__tests__/Price.spec.tsx new file mode 100644 index 000000000000..064fe52399ee --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/__tests__/Price.spec.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { screen, render } from '@testing-library/react'; +import Price from '../Price.component'; +import { IntervalUnitType, OvhSubsidiary } from '../../../enumTypes'; + +vitest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +// Mock sub-components +vitest.mock('../price-text', async () => { + const PriceText = ({ + price = '', + label = '', + intervalUnitText = '', + }: any) => {`${price} ${label} ${intervalUnitText} `}; + return { + PriceTextPreset: { + WITH_TAX: 'with-tax', + }, + PriceText, + }; +}); + +describe('Price component', () => { + const renderPriceComponent = (props: React.ComponentProps) => { + render(); + }; + + const baseProps = { + value: 3948000000, + ovhSubsidiary: OvhSubsidiary.FR, + intervalUnit: IntervalUnitType.month, + }; + const priceDefault = '39,48 €'; + const priceHtMonth = '39,48 € price_ht_label price_per_month'; + const priceTTC = '47,38 €'; + const taxNumber = 789600000; + const localeFr = 'fr-FR'; + it('renders "Inclus" when value is 0', () => { + const props = { + ...baseProps, + value: 0, + intervalUnit: IntervalUnitType.none, + locale: localeFr, + }; + renderPriceComponent(props); + const priceElement = screen.getByText('price_free'); + expect(priceElement.parentElement).toHaveTextContent('price_free'); + }); + + it('renders a price with locale of the form xx_XX correctly', () => { + const props = { ...baseProps, locale: 'fr_FR' }; + renderPriceComponent(props); + const priceElement = screen.getByText(priceDefault, { exact: false }); + expect(priceElement).toHaveTextContent(priceHtMonth); + }); + + it('renders a price for HT correctly', () => { + const props = { ...baseProps, locale: localeFr }; + renderPriceComponent(props); + const priceElement = screen.getByText(priceDefault, { exact: false }); + expect(priceElement).toHaveTextContent(priceHtMonth); + }); + + it('if I have a bad local I want it in French', () => { + const props = { ...baseProps, locale: 'toto' }; + renderPriceComponent(props); + const priceElement = screen.getByText(priceDefault, { exact: false }); + expect(priceElement).toHaveTextContent(priceHtMonth); + }); + + it('renders a price FR for TTC correctly', () => { + const props = { ...baseProps, locale: localeFr, tax: taxNumber }; + renderPriceComponent(props); + const priceElement = screen.getByText(priceDefault, { exact: false }); + expect(priceElement.parentElement).toHaveTextContent( + `${priceDefault} price_ht_label price_per_month ${priceTTC} price_ttc_label`, + ); + }); + + it('renders a price US correctly', () => { + const props = { + ...baseProps, + locale: 'en-US', + ovhSubsidiary: OvhSubsidiary.US, + }; + renderPriceComponent(props); + const priceElementTTC = screen.getByText('$39.48', { exact: false }); + expect(priceElementTTC).toHaveTextContent('$39.48'); + }); + + it('renders a price ASIA correctly with convert unit month', () => { + const props = { + ...baseProps, + locale: 'en-US', + ovhSubsidiary: OvhSubsidiary.ASIA, + isConvertIntervalUnit: true, + tax: taxNumber, + }; + renderPriceComponent(props); + const priceElementTTC = screen.getByText('$3.29', { exact: false }); + expect(priceElementTTC.parentElement).toHaveTextContent( + '$3.29 price_gst_excl_label price_per_month $3.95 price_gst_incl_label', + ); + }); + it('renders a price ASIA correctly with convert unit month excl gst', () => { + const props = { + ...baseProps, + locale: 'en-US', + ovhSubsidiary: OvhSubsidiary.ASIA, + isConvertIntervalUnit: true, + }; + renderPriceComponent(props); + const priceElementTTC = screen.getByText('$3.29', { exact: false }); + expect(priceElementTTC).toHaveTextContent( + '$3.29 price_gst_excl_label price_per_month', + ); + }); + + it('renders a price Deutch correctly', () => { + const props = { + ...baseProps, + locale: 'de-DE', + ovhSubsidiary: OvhSubsidiary.DE, + tax: taxNumber, + }; + renderPriceComponent(props); + const priceElementTTC = screen.getByText(priceTTC, { exact: false }); + expect(priceElementTTC).toHaveTextContent(priceTTC); + }); +}); diff --git a/packages/manager-ui-kit/src/components/price/__tests__/Price.utils.spec.ts b/packages/manager-ui-kit/src/components/price/__tests__/Price.utils.spec.ts new file mode 100644 index 000000000000..8723f189eb35 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/__tests__/Price.utils.spec.ts @@ -0,0 +1,118 @@ +import { vi, describe, it, expect } from 'vitest'; +import { + getPrice, + convertIntervalPrice, + getPriceTextFormatted, +} from '../Price.utils'; +import { IntervalUnitType, OvhSubsidiary } from '../../../enumTypes'; + +describe('getPrice', () => { + it('should return value divided by 100000000 when tax is not provided', () => { + expect(getPrice(200000000)).toBe(2); + }); + + it('should add tax to value before dividing', () => { + expect(getPrice(200000000, 100000000)).toBe(3); + }); + + it('should return zero when both value and tax are zero', () => { + expect(getPrice(0, 0)).toBe(0); + }); +}); + +describe('convertIntervalPrice', () => { + it('should convert yearly price to daily rate', () => { + expect(convertIntervalPrice(365, IntervalUnitType.day)).toBe(1); + }); + + it('should convert yearly price to monthly rate', () => { + expect(convertIntervalPrice(120, IntervalUnitType.month)).toBe(10); + }); + + it('should return same price for yearly interval', () => { + const price = 99.99; + expect(convertIntervalPrice(price, IntervalUnitType.year)).toBe(price); + }); + + it('should return same price for none interval', () => { + const price = 50; + expect(convertIntervalPrice(price, IntervalUnitType.none)).toBe(price); + }); + + it('should return original price if intervalUnit is invalid', () => { + const price = 100; + // @ts-expect-error - testing invalid input + expect(convertIntervalPrice(price, 'invalid')).toBe(price); + }); +}); + +describe('getPriceTextFormatted', () => { + const mockFormat = vi.fn(); + + // Mock the Intl.NumberFormat class + const mockNumberFormat = vi + .spyOn(Intl, 'NumberFormat') + .mockImplementation((locale) => { + if (locale === 'invalid-locale') { + throw new Error('Invalid locale'); + } + return { + format: mockFormat, + } as unknown as Intl.NumberFormat; + }); + + beforeEach(() => { + mockFormat.mockReset(); + mockNumberFormat.mockClear(); + }); + + it('should format price correctly for valid locale and subsidiary', () => { + getPriceTextFormatted(OvhSubsidiary.FR, 'fr_FR', 99.99); + expect(mockNumberFormat).toHaveBeenCalledWith('fr-FR', { + currency: 'EUR', + maximumFractionDigits: 2, + minimumFractionDigits: 2, + style: 'currency', + }); + expect(Intl.NumberFormat).toHaveBeenCalledOnce(); + }); + + it('should fallback to fr-FR when locale is invalid', () => { + getPriceTextFormatted(OvhSubsidiary.US, 'invalid_locale', 100); + expect(mockNumberFormat).toHaveBeenCalledWith('invalid-locale', { + currency: 'USD', + maximumFractionDigits: 2, + minimumFractionDigits: 2, + style: 'currency', + }); + expect(Intl.NumberFormat).toHaveBeenCalledTimes(2); + expect(mockNumberFormat).toHaveBeenCalledWith('fr-FR', { + currency: 'USD', + maximumFractionDigits: 2, + minimumFractionDigits: 2, + style: 'currency', + }); + }); + + it('should format with correct currency based on subsidiary', () => { + getPriceTextFormatted(OvhSubsidiary.US, 'en_US', 123.45); + expect(mockNumberFormat).toHaveBeenCalledWith('en-US', { + currency: 'USD', + maximumFractionDigits: 2, + minimumFractionDigits: 2, + style: 'currency', + }); + expect(Intl.NumberFormat).toHaveBeenCalledOnce(); + }); + + it('should format large numbers with thousand separators', () => { + getPriceTextFormatted(OvhSubsidiary.CA, 'fr_CA', 1234567.89); + expect(mockNumberFormat).toHaveBeenCalledWith('fr-CA', { + currency: 'CAD', + maximumFractionDigits: 2, + minimumFractionDigits: 2, + style: 'currency', + }); + expect(Intl.NumberFormat).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/price/__tests__/__snapshots__/Price.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/price/__tests__/__snapshots__/Price.snapshot.test.tsx.snap new file mode 100644 index 000000000000..b05c8086636e --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/__tests__/__snapshots__/Price.snapshot.test.tsx.snap @@ -0,0 +1,286 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Price Component > displays both GST-excl and GST-incl in Asia format if tax exists 1`] = ` +
+

+ + + 10,00 $SG + + + ex. GST + + + /mois + + + + ( + + 10,00 $SG + + + incl. GST + + ) + +

+
+`; + +exports[`Price Component > displays both HT and TTC in French format when tax > 0 1`] = ` +
+

+ + + 10,00 € + + + HT + + + /mois + + + + ( + + 10,00 € + + + TTC + + ) + +

+
+`; + +exports[`Price Component > displays only GST-excl in Asia format if no tax 1`] = ` +
+

+ + + 10,00 $SG + + + ex. GST + + + /mois + + +

+
+`; + +exports[`Price Component > displays only HT in French format when no tax 1`] = ` +
+

+ + + 10,00 € + + + HT + + + /mois + + +

+
+`; + +exports[`Price Component > displays price with tax in German format 1`] = ` +
+

+ + + 10,00 € + + + /mois + + +

+
+`; + +exports[`Price Component > displays price without tax label in US format 1`] = ` +
+

+ + + 10,00 $US + + + /mois + + +

+
+`; + +exports[`Price Component > does not show intervalUnitText if intervalUnit is "none" 1`] = ` +
+

+ + + 10,00 € + + + HT + + + + ( + + 12,00 € + + + TTC + + ) + +

+
+`; + +exports[`Price Component > handles different interval units correctly 1`] = ` +
+

+ + + 10,00 € + + + HT + + + /an + + + + ( + + 12,00 € + + + TTC + + ) + +

+
+`; + +exports[`Price Component > renders free price when value is 0 1`] = ` +
+

+ + Inclus + +

+
+`; diff --git a/packages/manager-ui-kit/src/components/price/index.ts b/packages/manager-ui-kit/src/components/price/index.ts new file mode 100644 index 000000000000..81e934d42cd2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/index.ts @@ -0,0 +1,3 @@ +export { Price } from './Price.component'; + +export type { PriceProps } from './Price.props'; diff --git a/packages/manager-ui-kit/src/components/price/price-text/PriceText.component.tsx b/packages/manager-ui-kit/src/components/price/price-text/PriceText.component.tsx new file mode 100644 index 000000000000..d79989689aec --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/price-text/PriceText.component.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { PriceTextProps, PriceTextPreset } from './PriceText.props'; + +export const PriceText: React.FC = ({ + preset = PriceTextPreset.BASE, + price, + intervalUnitText, + label, +}) => ( + + {preset === PriceTextPreset.WITH_TAX && '('} + + {price} + + {label && ( + + {label} + + )} + {intervalUnitText && preset === PriceTextPreset.BASE && ( + + {intervalUnitText} + + )} + {preset === PriceTextPreset.WITH_TAX && ')'} + +); diff --git a/packages/manager-ui-kit/src/components/price/price-text/PriceText.props.ts b/packages/manager-ui-kit/src/components/price/price-text/PriceText.props.ts new file mode 100644 index 000000000000..02297e0500cd --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/price-text/PriceText.props.ts @@ -0,0 +1,11 @@ +export enum PriceTextPreset { + BASE = 'base_price', + WITH_TAX = 'price_including_tax', +} + +export type PriceTextProps = { + preset?: PriceTextPreset; + price: string; + label?: string; + intervalUnitText?: string; +}; diff --git a/packages/manager-ui-kit/src/components/price/price-text/__tests__/PriceText.spec.tsx b/packages/manager-ui-kit/src/components/price/price-text/__tests__/PriceText.spec.tsx new file mode 100644 index 000000000000..15b839b7c2d4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/price-text/__tests__/PriceText.spec.tsx @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { PriceText } from '../PriceText.component'; +import { PriceTextPreset } from '../PriceText.props'; + +describe('PriceText Component', () => { + const defaultProps = { + price: '€9.99', + }; + + it('should render price with BASE preset by default', () => { + render(); + const span = screen.getByText('€9.99').parentElement; + + expect(span).toBeInTheDocument(); + expect(span).toHaveClass('font-semibold'); + expect(span).toHaveClass('text-[--ods-color-text]'); + expect(span).toHaveClass('text-[16px]'); + expect(span).toHaveClass('leading-[20px]'); + }); + + it('should render price with WITH_TAX preset', () => { + render(); + const span = screen.getByText('€9.99').parentElement; + + expect(screen.getByText('(', { exact: false })).toBeInTheDocument(); + expect(screen.getByText(')', { exact: false })).toBeInTheDocument(); + + expect(span).toBeInTheDocument(); + expect(span).toHaveClass('text-[--ods-color-neutral-500]'); + expect(span).toHaveClass('text-[14px]'); + expect(span).toHaveClass('leading-[18px]'); + }); + + it('should render label when provided with BASE preset', () => { + render(); + expect(screen.getByText('excl. VAT')).toBeInTheDocument(); + expect(screen.getByText('excl. VAT')).toHaveClass('mr-1'); + }); + + it('should render label without mr-1 when using WITH_TAX preset', () => { + render( + , + ); + expect(screen.getByText('incl. tax')).toBeInTheDocument(); + expect(screen.queryByText('incl. tax')?.classList.contains('mr-1')).toBe( + false, + ); + }); + + it('should render intervalUnitText only with BASE preset', () => { + render(); + expect(screen.getByText('/mo')).toBeInTheDocument(); + }); + + it('should NOT render intervalUnitText with non-BASE preset', () => { + render( + , + ); + expect(screen.queryByText('/mo')).not.toBeInTheDocument(); + }); + + it('should apply correct classnames to price span (always has mr-1)', () => { + render(); + const priceSpan = screen.getByText('€9.99'); + + expect(priceSpan).toBeInTheDocument(); + expect(priceSpan).toHaveClass('mr-1'); + }); + + it('should render all parts together correctly', () => { + render( + , + ); + + expect(screen.getByText('(', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('€9.99')).toBeInTheDocument(); + expect(screen.getByText('incl. VAT')).toBeInTheDocument(); + expect(screen.queryByText('/mo', { exact: false })).not.toBeInTheDocument(); + expect(screen.getByText(')', { exact: false })).toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/price/price-text/index.ts b/packages/manager-ui-kit/src/components/price/price-text/index.ts new file mode 100644 index 000000000000..d348155a98dd --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/price-text/index.ts @@ -0,0 +1,3 @@ +export { PriceText } from './PriceText.component'; + +export { PriceTextPreset } from './PriceText.props'; diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/price/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/price/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/price/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/price/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/price/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/price/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/price/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/content/price/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/price/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/content/price/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/price/translations/Messages_pt_PT.json diff --git a/packages/manager-ui-kit/src/components/price/translations/index.ts b/packages/manager-ui-kit/src/components/price/translations/index.ts new file mode 100644 index 000000000000..1f35ca778dd1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/price/translations/index.ts @@ -0,0 +1,14 @@ +import { buildTranslationManager } from '../../../utils/translation-helper'; + +const translationLoaders = { + de_DE: () => import('./Messages_de_DE.json'), + en_GB: () => import('./Messages_en_GB.json'), + es_ES: () => import('./Messages_es_ES.json'), + fr_CA: () => import('./Messages_fr_CA.json'), + fr_FR: () => import('./Messages_fr_FR.json'), + it_IT: () => import('./Messages_it_IT.json'), + pl_PL: () => import('./Messages_pl_PL.json'), + pt_PT: () => import('./Messages_pt_PT.json'), +}; + +buildTranslationManager(translationLoaders, 'price'); diff --git a/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.component.tsx b/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.component.tsx new file mode 100644 index 000000000000..3a2065f3936c --- /dev/null +++ b/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.component.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { Spinner, SPINNER_SIZE } from '@ovhcloud/ods-react'; +import { RedirectionGuardProps } from './RedirectionGuard.props'; + +export function RedirectionGuard({ + route, + condition, + isLoading, + children, + isError, + errorComponent, +}: RedirectionGuardProps): JSX.Element { + if (isLoading) { + return ( + + ); + } + + if (isError && errorComponent) { + return <>{errorComponent}; + } + + return condition ? : <>{children}; +} diff --git a/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.props.ts b/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.props.ts new file mode 100644 index 000000000000..02a291b8849b --- /dev/null +++ b/packages/manager-ui-kit/src/components/redirection-guard/RedirectionGuard.props.ts @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +export type RedirectionGuardProps = { + children: ReactNode; + condition: boolean; + isLoading: boolean; + route: string; + isError?: boolean; + errorComponent?: ReactNode; +}; diff --git a/packages/manager-ui-kit/src/components/redirection-guard/__tests__/RedirectionGuard.spec.tsx b/packages/manager-ui-kit/src/components/redirection-guard/__tests__/RedirectionGuard.spec.tsx new file mode 100644 index 000000000000..722af35bddd9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/redirection-guard/__tests__/RedirectionGuard.spec.tsx @@ -0,0 +1,61 @@ +import { vi } from 'vitest'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Navigate } from 'react-router-dom'; +import { RedirectionGuard } from '../RedirectionGuard.component'; + +vi.mock('react-router-dom', () => ({ + Navigate: vi.fn(() => null), +})); + +describe('RedirectionGuard', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('should render children when condition is false', () => { + render( + +
Test Child
+
, + ); + + expect(screen.getByText('Test Child')).toBeInTheDocument(); + expect(Navigate).not.toHaveBeenCalled(); + }); + + it('should navigate when condition is true', () => { + render( + +
Test Child
+
, + ); + + expect(Navigate).toHaveBeenCalledWith({ to: '/test' }, {}); + }); + + it('should render spinner when isLoading is true', () => { + render( + +
Test Child
+
, + ); + expect(screen.getByTestId('redirectionGuard_spinner')).toBeInTheDocument(); + }); + + it('should render errorComponent when isError is true', () => { + render( + Test Error
} + route="/test" + > +
Test Child
+ , + ); + expect(screen.getByText('Test Error')).toBeInTheDocument(); + expect(Navigate).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/redirection-guard/index.ts b/packages/manager-ui-kit/src/components/redirection-guard/index.ts new file mode 100644 index 000000000000..79374ef51834 --- /dev/null +++ b/packages/manager-ui-kit/src/components/redirection-guard/index.ts @@ -0,0 +1,3 @@ +export { RedirectionGuard } from './RedirectionGuard.component'; + +export type { RedirectionGuardProps } from './RedirectionGuard.props'; diff --git a/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.component.tsx b/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.component.tsx new file mode 100644 index 000000000000..86ab42079bd8 --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.component.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { useTranslation } from 'react-i18next'; +import { ServiceStateBadgeProps } from './ServiceStateBadge.props'; +import { Badge } from '../badge'; + +const STATES = { + active: { label: 'service_state_active', color: 'success' }, + deleted: { label: 'service_state_deleted', color: 'critical' }, + suspended: { label: 'service_state_suspended', color: 'warning' }, + toActivate: { label: 'service_state_toActivate', color: 'information' }, + toDelete: { label: 'service_state_toDelete', color: 'information' }, + toSuspend: { label: 'service_state_toSuspend', color: 'information' }, +} as const; + +export const ServiceStateBadge = ({ + state, + ...rest +}: ServiceStateBadgeProps) => { + const { t } = useTranslation(NAMESPACES.SERVICE); + + const { label, color } = STATES[state] ?? { + label: state, + color: 'information', + }; + + return ( + + {t(label)} + + ); +}; diff --git a/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.props.ts b/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.props.ts new file mode 100644 index 000000000000..7811c732826b --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/ServiceStateBadge.props.ts @@ -0,0 +1,17 @@ +import { ComponentProps } from 'react'; +import { Badge } from '../badge'; + +export type ResourceStatus = + | 'active' + | 'deleted' + | 'suspended' + | 'toActivate' + | 'toDelete' + | 'toSuspend'; + +export type ServiceStateBadgeProps = Omit< + ComponentProps, + 'color' | 'label' +> & { + state: ResourceStatus; +}; diff --git a/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.snapshot.test.tsx b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.snapshot.test.tsx new file mode 100644 index 000000000000..a245e3d45821 --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.snapshot.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { render } from '@testing-library/react'; +import { ResourceStatus } from '../ServiceStateBadge.props'; +import { ServiceStateBadge } from '../ServiceStateBadge.component'; +import { SERVICE_STATES } from './ServiceStateBadge.spec.util'; + +vitest.mock('../../hooks/iam'); + +const renderComponent = ( + props: React.ComponentProps, +) => { + return render(); +}; + +describe('should display manager state with the good color', () => { + it.each(SERVICE_STATES)( + `should display manager $state badge for $color`, + ({ state, label, color }) => { + const { container } = renderComponent({ state }); + expect(container).toMatchSnapshot(); + }, + ); + + it(`should display loading Badge`, () => { + const { container } = renderComponent({ + isLoading: true, + state: 'unknown' as ResourceStatus, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.tsx b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.tsx new file mode 100644 index 000000000000..14cbe67567b5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { render } from '@testing-library/react'; +import { ServiceStateBadge } from '../ServiceStateBadge.component'; +import { SERVICE_STATES } from './ServiceStateBadge.spec.util'; + +vitest.mock('../../hooks/iam'); + +const renderComponent = ( + props: React.ComponentProps, +) => { + return render(); +}; + +describe('should display manager state with the good color', () => { + it.each(SERVICE_STATES)( + `should display manager $state badge for $color`, + ({ state, label, color }) => { + const container = renderComponent({ state }); + const badge = container.getByTestId('badge'); + expect(badge).toBeDefined(); + expect(badge.textContent).toBe(label); + expect(badge.className.includes(color)).toBe(true); + }, + ); +}); diff --git a/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.util.ts b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.util.ts new file mode 100644 index 000000000000..2a3badcc17fa --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/ServiceStateBadge.spec.util.ts @@ -0,0 +1,44 @@ +import { ResourceStatus } from '../ServiceStateBadge.props'; + +export const SERVICE_STATES = [ + { + state: 'active', + label: 'service_state_active', + color: 'success', + } as const, + { + state: 'deleted', + label: 'service_state_deleted', + color: 'critical', + } as const, + { + state: 'deleted', + label: 'service_state_deleted', + color: 'critical', + } as const, + { + state: 'suspended', + label: 'service_state_suspended', + color: 'warning', + } as const, + { + state: 'toActivate', + label: 'service_state_toActivate', + color: 'information', + } as const, + { + state: 'toDelete', + label: 'service_state_toDelete', + color: 'information', + } as const, + { + state: 'toSuspend', + label: 'service_state_toSuspend', + color: 'information', + } as const, + { + state: 'unknown' as ResourceStatus, + label: 'unknown', + color: 'information', + } as const, +]; diff --git a/packages/manager-ui-kit/src/components/service-state-badge/__tests__/__snapshots__/ServiceStateBadge.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/__snapshots__/ServiceStateBadge.snapshot.test.tsx.snap new file mode 100644 index 000000000000..0f7ab004d922 --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/__tests__/__snapshots__/ServiceStateBadge.snapshot.test.tsx.snap @@ -0,0 +1,98 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should display manager state with the good color > should display loading Badge 1`] = ` +
+
+
+`; + +exports[`should display manager state with the good color > should display manager 'active' badge for 'success' 1`] = ` +
+ + service_state_active + +
+`; + +exports[`should display manager state with the good color > should display manager 'deleted' badge for 'critical' 1`] = ` +
+ + service_state_deleted + +
+`; + +exports[`should display manager state with the good color > should display manager 'deleted' badge for 'critical' 2`] = ` +
+ + service_state_deleted + +
+`; + +exports[`should display manager state with the good color > should display manager 'suspended' badge for 'warning' 1`] = ` +
+ + service_state_suspended + +
+`; + +exports[`should display manager state with the good color > should display manager 'toActivate' badge for 'information' 1`] = ` +
+ + service_state_toActivate + +
+`; + +exports[`should display manager state with the good color > should display manager 'toDelete' badge for 'information' 1`] = ` +
+ + service_state_toDelete + +
+`; + +exports[`should display manager state with the good color > should display manager 'toSuspend' badge for 'information' 1`] = ` +
+ + service_state_toSuspend + +
+`; + +exports[`should display manager state with the good color > should display manager 'unknown' badge for 'information' 1`] = ` +
+ + unknown + +
+`; diff --git a/packages/manager-ui-kit/src/components/service-state-badge/index.ts b/packages/manager-ui-kit/src/components/service-state-badge/index.ts new file mode 100644 index 000000000000..666fcb996418 --- /dev/null +++ b/packages/manager-ui-kit/src/components/service-state-badge/index.ts @@ -0,0 +1,3 @@ +export { ServiceStateBadge } from './ServiceStateBadge.component'; + +export type { ServiceStateBadgeProps } from './ServiceStateBadge.props'; diff --git a/packages/manager-ui-kit/src/components/step/Step.component.tsx b/packages/manager-ui-kit/src/components/step/Step.component.tsx new file mode 100644 index 000000000000..1b17298e5b36 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/Step.component.tsx @@ -0,0 +1,53 @@ +import { v4 as uuidV4 } from 'uuid'; +import StepProps from './Step.props'; +import { StepIndicator } from './step-indicator/StepIndicator.component'; +import { StepHeader } from './step-header/StepHeader.component'; +import { StepBody } from './step-body/StepBody.component'; +import { StepFooter } from './step-footer/StepFooter.component'; +import { StepContext } from './StepContext'; + +export const Step = ({ + id = uuidV4(), + title = '', + subtitle = '', + open, + checked, + locked, + order, + children, + next, + edit, + skip, +}: StepProps): JSX.Element => { + return ( + +
+ +
+ + {open && ( + <> + {children} + {!locked && } + + )} +
+
+
+ ); +}; + +export default Step; diff --git a/packages/manager-ui-kit/src/components/step/Step.props.tsx b/packages/manager-ui-kit/src/components/step/Step.props.tsx new file mode 100644 index 000000000000..9718ed26489f --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/Step.props.tsx @@ -0,0 +1,34 @@ +export type Next = { + action: (id: string) => void; + label: string | JSX.Element; + disabled?: boolean; +}; + +export type Edit = { + action: (id: string) => void; + label: string | JSX.Element; + disabled?: boolean; +}; + +export type Skip = { + action: (id: string) => void; + label: string | JSX.Element; + disabled?: boolean; + hint?: string; +}; + +export type StepProps = { + id?: string; + title?: string | JSX.Element; + subtitle?: string | JSX.Element; + open: boolean; + checked: boolean; + locked: boolean; + order: number; + next?: Next; + edit?: Edit; + skip?: Skip; + children?: JSX.Element | JSX.Element[]; +}; + +export default StepProps; diff --git a/packages/manager-ui-kit/src/components/step/StepContext.tsx b/packages/manager-ui-kit/src/components/step/StepContext.tsx new file mode 100644 index 000000000000..eeaade2ab2da --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/StepContext.tsx @@ -0,0 +1,11 @@ +import { createContext } from 'react'; +import { StepProps } from './Step.props'; + +export const StepContext = createContext({ + open: true, + checked: false, + locked: false, + order: 0, +}); + +export default StepContext; diff --git a/packages/manager-ui-kit/src/components/step/__tests__/Step.snapshot.test.tsx b/packages/manager-ui-kit/src/components/step/__tests__/Step.snapshot.test.tsx new file mode 100644 index 000000000000..6ad446b5dc4c --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/__tests__/Step.snapshot.test.tsx @@ -0,0 +1,129 @@ +import { expect, it, vitest } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +import { Step } from '../Step.component'; + +describe('Step Component - Snapshot Tests', () => { + const defaultProps = { + id: 'test-id', + title: 'Test Step', + subtitle: 'This is a test step', + open: true, + checked: false, + locked: false, + order: 1, + children:

Step Content

, + next: null, + edit: null, + skip: null, + }; + + it('renders the Step with Title and Subtitle', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('renders the checked and locked Step', () => { + const { container } = render( + + <>Test Body + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Next button enabled', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Next button disabled', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Skip button', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Skip button disabled', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Edit Button', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the Step with Edit Button disabled', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders the closed Step', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/step/__tests__/Step.spec.tsx b/packages/manager-ui-kit/src/components/step/__tests__/Step.spec.tsx new file mode 100644 index 000000000000..4b8ec2c50c5b --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/__tests__/Step.spec.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { expect, it, vitest } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +import { Step } from '../Step.component'; +import { StepContext } from '../StepContext'; + +// Step component only handles the visibility of child components depending on conditions +// And to test different conditions, mocks provide an easy way of identification with testid +vitest.mock('../step-indicator/StepIndicator.component', () => ({ + StepIndicator: () =>
, +})); +vitest.mock('../step-header/StepHeader.component', () => ({ + StepHeader: () =>
, +})); +vitest.mock('../step-body/StepBody.component', () => ({ + StepBody: ({ children }) =>
{children}
, +})); +vitest.mock('../step-footer/StepFooter.component', () => ({ + StepFooter: () =>
, +})); + +describe('Step Component', () => { + const defaultProps = { + id: 'test-id', + title: 'Test Step', + subtitle: 'This is a test step', + open: false, + checked: false, + locked: false, + order: 1, + children:

Step Content

, + next: null, + edit: null, + skip: null, + }; + + it('renders the step indicator', () => { + render(); + expect(screen.getByTestId('step-indicator')).toBeInTheDocument(); + }); + + it('renders the step header', () => { + render(); + expect(screen.getByTestId('step-header')).toBeInTheDocument(); + }); + + it('renders the step body and footer when open is true and locked is false', () => { + render(); + expect(screen.getByTestId('step-body')).toBeInTheDocument(); + expect(screen.getByTestId('step-footer')).toBeInTheDocument(); + }); + + it('does not render the step body or footer when open is false', () => { + render(); + expect(screen.queryByTestId('step-body')).not.toBeInTheDocument(); + expect(screen.queryByTestId('step-footer')).not.toBeInTheDocument(); + }); + + it('does not render the step footer when locked is true', () => { + render(); + expect(screen.queryByTestId('step-footer')).not.toBeInTheDocument(); + }); + + it('provides correct context values to children', () => { + render( + + + {(context) => ( +
+ {context.id === defaultProps.id && + context.title === defaultProps.title && + context.subtitle === defaultProps.subtitle && + context.open === true && + context.checked === defaultProps.checked && + context.locked === defaultProps.locked && + context.order === defaultProps.order && + context.next === defaultProps.next && + context.edit === defaultProps.edit && + context.skip === defaultProps.skip + ? 'Context is correct' + : 'Context is incorrect'} +
+ )} +
+
, + ); + + expect(screen.getByTestId('context-values')).toHaveTextContent( + 'Context is correct', + ); + }); +}); diff --git a/packages/manager-ui-kit/src/components/step/__tests__/__snapshots__/Step.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/step/__tests__/__snapshots__/Step.snapshot.test.tsx.snap new file mode 100644 index 000000000000..70e33ea5a89b --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/__tests__/__snapshots__/Step.snapshot.test.tsx.snap @@ -0,0 +1,473 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Step Component - Snapshot Tests > renders the Step with Edit Button 1`] = ` +
+
+
+ +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Edit Button disabled 1`] = ` +
+
+
+ +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Next button disabled 1`] = ` +
+
+
+ + 1 + +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+ +
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Next button enabled 1`] = ` +
+
+
+ + 1 + +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+ +
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Skip button 1`] = ` +
+
+
+ + 1 + +
+
+
+
+

+ Test Step +

+ + (Optional) + +
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+ +
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Skip button disabled 1`] = ` +
+
+
+ + 1 + +
+
+
+
+

+ Test Step +

+ + (Optional) + +
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+ +
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the Step with Title and Subtitle 1`] = ` +
+
+
+ + 1 + +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+

+ Step Content +

+
+
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the checked and locked Step 1`] = ` +
+
+
+ +
+
+
+
+

+ Test Step +

+
+
+
+ This is a test step +
+
+ Test Body +
+
+
+
+`; + +exports[`Step Component - Snapshot Tests > renders the closed Step 1`] = ` +
+
+
+ +
+
+
+
+

+ Test Step +

+
+
+
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/step/index.ts b/packages/manager-ui-kit/src/components/step/index.ts new file mode 100644 index 000000000000..4a025a7ce449 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/index.ts @@ -0,0 +1,3 @@ +export { Step } from './Step.component'; + +export type { StepProps } from './Step.props'; diff --git a/packages/manager-ui-kit/src/components/step/step-body/StepBody.component.tsx b/packages/manager-ui-kit/src/components/step/step-body/StepBody.component.tsx new file mode 100644 index 000000000000..50fae8cb7386 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-body/StepBody.component.tsx @@ -0,0 +1,27 @@ +import { clsx } from 'clsx'; +import { PropsWithChildren, Suspense, useContext } from 'react'; +import { Spinner, SPINNER_SIZE } from '@ovhcloud/ods-react'; +import { StepContext } from '../StepContext'; +import { StepProps } from '../Step.props'; + +export const StepBody = ({ children }: PropsWithChildren) => { + const { subtitle, locked } = useContext(StepContext); + return ( + <> + {subtitle &&
{subtitle}
} +
+ }> + {children} + +
+ + ); +}; + +export default StepBody; diff --git a/packages/manager-ui-kit/src/components/step/step-body/__tests__/StepBody.component.spec.tsx b/packages/manager-ui-kit/src/components/step/step-body/__tests__/StepBody.component.spec.tsx new file mode 100644 index 000000000000..c1165d812903 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-body/__tests__/StepBody.component.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { vitest, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import StepBody from '../StepBody.component'; +import { StepContext } from '../../StepContext'; + +// Mocking Suspense fallback to avoid rendering real spinner +vitest.mock('@ovhcloud/ods-react', () => ({ + Spinner: vitest.fn(() =>
), + SPINNER_SIZE: { md: 'md' }, +})); + +describe('StepBody Component', () => { + const renderWithStepContext = (value, children: JSX.Element | string) => + render( + + {
{children}
}
+
, + ); + + it('renders the subtitle when provided', () => { + renderWithStepContext( + { subtitle: 'This is a subtitle', locked: false }, + 'Step Content', + ); + expect(screen.getByText(/This is a subtitle/i)).toBeInTheDocument(); + }); + + it('does not render the subtitle when not provided', () => { + renderWithStepContext({ subtitle: '', locked: false }, 'Step Content'); + expect(screen.queryByTestId('subtitle')).not.toBeInTheDocument(); + }); + + it('renders the children content', () => { + renderWithStepContext({ subtitle: '', locked: false }, 'Step Content'); + expect(screen.getByTestId('children')).toBeInTheDocument(); + }); + + it('disables interaction with body when locked is true', () => { + renderWithStepContext({ subtitle: '', locked: true }, 'Step Content'); + expect(screen.getByTestId('content')).toHaveClass( + 'mt-5 cursor-not-allowed pointer-events-none opacity-50', + ); + }); + + it('enables the interaction with body when locked is false', () => { + renderWithStepContext({ subtitle: '', locked: false }, 'Step Content'); + expect(screen.getByTestId('content')).toHaveClass('mt-5'); + expect(screen.getByTestId('content')).not.toHaveClass( + 'cursor-not-allowed pointer-events-none opacity-50', + ); + }); +}); diff --git a/packages/manager-ui-kit/src/components/step/step-footer/StepFooter.component.tsx b/packages/manager-ui-kit/src/components/step/step-footer/StepFooter.component.tsx new file mode 100644 index 000000000000..99a0db4ab070 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-footer/StepFooter.component.tsx @@ -0,0 +1,41 @@ +import { useContext } from 'react'; +import { Button, BUTTON_SIZE, BUTTON_VARIANT } from '@ovhcloud/ods-react'; +import { StepContext } from '../StepContext'; +import { StepProps } from '../Step.props'; + +export const StepFooter = () => { + const { id, next, locked, skip } = useContext(StepContext); + + return ( +
+ {next?.action && !locked && ( + + )} + {skip?.action && ( + + )} +
+ ); +}; + +export default StepFooter; diff --git a/packages/manager-ui-kit/src/components/step/step-footer/__tests__/StepFooter.component.spec.tsx b/packages/manager-ui-kit/src/components/step/step-footer/__tests__/StepFooter.component.spec.tsx new file mode 100644 index 000000000000..61617e5a31ee --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-footer/__tests__/StepFooter.component.spec.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { vitest } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import StepFooter from '../StepFooter.component'; +import { StepContext } from '../../StepContext'; + +// Mocking Button component to easily test different variants and size without depending on ODS +vitest.mock('@ovhcloud/ods-react', () => ({ + Button: vitest.fn(({ children, variant = '', size, onClick, disabled }) => ( + + )), + BUTTON_SIZE: { md: 'md' }, + BUTTON_VARIANT: { ghost: 'ghost' }, +})); + +describe('StepFooter Component', () => { + const renderWithStepContext = (value) => + render( + + + , + ); + + it('renders the Next button when next action is provided and locked is false', () => { + const next = { action: vitest.fn(), label: 'Next Step', disabled: false }; + renderWithStepContext({ id: 'test-id', next, locked: false, skip: null }); + expect(screen.getByTestId('button--md')).toBeInTheDocument(); + expect(screen.getByTestId('button--md')).toHaveTextContent('Next Step'); + }); + + it('does not render the Next button when locked is true', () => { + const next = { action: vitest.fn(), label: 'Next Step', disabled: false }; + renderWithStepContext({ id: 'test-id', next, locked: true, skip: null }); + expect(screen.queryByTestId('button--md')).not.toBeInTheDocument(); + }); + + it('does not render the Next button when next action is not provided', () => { + renderWithStepContext({ + id: 'test-id', + next: null, + locked: false, + skip: null, + }); + expect(screen.queryByTestId('button--md')).not.toBeInTheDocument(); + }); + + it('renders the Skip button when skip action is provided', () => { + const skip = { action: vitest.fn(), label: 'Skip', disabled: false }; + renderWithStepContext({ id: 'test-id', next: null, locked: false, skip }); + expect(screen.getByTestId('button-ghost-md')).toBeInTheDocument(); + expect(screen.getByTestId('button-ghost-md')).toHaveTextContent('Skip'); + }); + + it('does not render the Skip button when skip action is not provided', () => { + renderWithStepContext({ + id: 'test-id', + next: null, + locked: false, + skip: null, + }); + expect(screen.queryByTestId('button-ghost-md')).not.toBeInTheDocument(); + }); + + it('calls next.action when Next button is clicked and enabled', () => { + const next = { action: vitest.fn(), label: 'Next Step', disabled: false }; + renderWithStepContext({ id: 'test-id', next, locked: false, skip: null }); + fireEvent.click(screen.getByTestId('button--md')); + expect(next.action).toHaveBeenCalledWith('test-id'); + }); + + it('does not call next.action when Next button is disabled', () => { + const next = { action: vitest.fn(), label: 'Next Step', disabled: true }; + renderWithStepContext({ id: 'test-id', next, locked: false, skip: null }); + fireEvent.click(screen.getByTestId('button--md')); + expect(next.action).not.toHaveBeenCalled(); + }); + + it('calls skip.action when Skip button is clicked and enabled', () => { + const skip = { action: vitest.fn(), label: 'Skip', disabled: false }; + renderWithStepContext({ id: 'test-id', next: null, locked: false, skip }); + fireEvent.click(screen.getByTestId('button-ghost-md')); + expect(skip.action).toHaveBeenCalledWith('test-id'); + }); + + it('does not call skip.action when Skip button is disabled', () => { + const skip = { action: vitest.fn(), label: 'Skip', disabled: true }; + renderWithStepContext({ id: 'test-id', next: null, locked: false, skip }); + fireEvent.click(screen.getByTestId('button-ghost-md')); + expect(skip.action).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/step/step-header/StepHeader.component.tsx b/packages/manager-ui-kit/src/components/step/step-header/StepHeader.component.tsx new file mode 100644 index 000000000000..a56da52ba24e --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-header/StepHeader.component.tsx @@ -0,0 +1,46 @@ +import { useContext } from 'react'; +import { clsx } from 'clsx'; +import { Button, BUTTON_VARIANT, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { StepContext } from '../StepContext'; +import { StepProps } from '../Step.props'; + +export const StepHeader = () => { + const { id, title, edit, locked, open, skip } = + useContext(StepContext); + return ( +
+
+ {title} + {skip?.hint && ( + + {skip.hint} + + )} +
+ {edit?.action && locked && ( +
+ +
+ )} +
+ ); +}; + +export default StepHeader; diff --git a/packages/manager-ui-kit/src/components/step/step-header/__tests__/StepHeader.component.spec.tsx b/packages/manager-ui-kit/src/components/step/step-header/__tests__/StepHeader.component.spec.tsx new file mode 100644 index 000000000000..73764732c336 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-header/__tests__/StepHeader.component.spec.tsx @@ -0,0 +1,109 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { StepHeader } from '../StepHeader.component'; +import { StepContext } from '../../StepContext'; + +// Mock the dependencies +vi.mock('../useStepContext', () => ({ + useStepContext: vi.fn(), +})); + +const mockEditAction = vi.fn(); + +describe('StepHeader Component', () => { + const defaultContext = { + id: 'step-1', + open: false, + title: 'Test Title', + edit: undefined, + locked: true, + skip: undefined, + }; + + const renderWithStepContext = (value) => + render( + + + , + ); + + beforeEach(() => { + mockEditAction.mockClear(); + }); + + it('renders the title correctly', () => { + renderWithStepContext(defaultContext); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.queryByTestId('edit')).not.toBeInTheDocument(); + }); + + it('displays skip hint when provided', () => { + renderWithStepContext({ + ...defaultContext, + skip: { hint: 'This is optional' }, + }); + expect(screen.getByText('This is optional')).toBeInTheDocument(); + }); + + it('does not render edit button if edit is not defined', () => { + renderWithStepContext(); + }); + + it('renders edit button when edit.action and locked are truthy', () => { + renderWithStepContext({ + ...defaultContext, + edit: { + action: mockEditAction, + label: 'Edit', + disabled: false, + }, + locked: true, + }); + expect(screen.getByTestId('edit')).toBeInTheDocument(); + expect(screen.getByTestId('edit-cta')).toHaveTextContent('Edit'); + }); + + it('disables edit button when edit.disabled is true', () => { + renderWithStepContext({ + ...defaultContext, + edit: { + action: mockEditAction, + label: 'Edit', + disabled: true, + }, + locked: true, + }); + expect(screen.getByTestId('edit-cta')).toBeDisabled(); + }); + + it('calls edit.action when edit button is clicked and not disabled', async () => { + renderWithStepContext({ + ...defaultContext, + edit: { + action: mockEditAction, + label: 'Edit', + disabled: false, + }, + locked: true, + }); + await fireEvent.click(screen.getByTestId('edit-cta')); + expect(mockEditAction).toHaveBeenCalledWith('step-1'); + }); + + it('applies correct styles when step is open', () => { + renderWithStepContext({ + ...defaultContext, + open: true, + }); + const titleElement = screen.getByText('Test Title').parentElement; + expect(titleElement).toHaveClass('text-[--ods-color-text]'); + }); + + it('applies correct styles when step is closed', () => { + renderWithStepContext({ + ...defaultContext, + }); + const titleElement = screen.getByText('Test Title').parentElement; + expect(titleElement).toHaveClass('text-[--ods-color-neutral-500]'); + }); +}); diff --git a/packages/manager-ui-kit/src/components/step/step-indicator/StepIndicator.component.tsx b/packages/manager-ui-kit/src/components/step/step-indicator/StepIndicator.component.tsx new file mode 100644 index 000000000000..1556c6523b6c --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-indicator/StepIndicator.component.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; +import { clsx } from 'clsx'; +import { Icon, ICON_NAME } from '@ovhcloud/ods-react'; +import { StepContext } from '../StepContext'; +import { StepProps } from '../Step.props'; + +export const StepIndicator = () => { + const { checked, open, order } = useContext(StepContext); + return ( +
+ {checked ? ( + + ) : ( + + {order} + + )} +
+ ); +}; + +export default StepIndicator; diff --git a/packages/manager-ui-kit/src/components/step/step-indicator/__tests__/StepIndicator.component.spec.tsx b/packages/manager-ui-kit/src/components/step/step-indicator/__tests__/StepIndicator.component.spec.tsx new file mode 100644 index 000000000000..0056c7b11215 --- /dev/null +++ b/packages/manager-ui-kit/src/components/step/step-indicator/__tests__/StepIndicator.component.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { vitest, expect, it } from 'vitest'; +import StepIndicator from '../StepIndicator.component'; +import { StepContext } from '../../StepContext'; + +// Mocking Icon component to avoid rendering real icon +vitest.mock('@ovhcloud/ods-react', () => ({ + Icon: vitest.fn(({ name, className }) => ( +
+ {name} +
+ )), + ICON_NAME: { check: 'check' }, + clsx: vitest.fn((...args) => args.join(' ')), +})); + +describe('StepIndicator Component', () => { + const renderWithStepContext = (value) => + render( + + + , + ); + + it('renders the Icon component when checked is true', () => { + renderWithStepContext({ checked: true, open: false, order: 1 }); + const icon = screen.getByTestId('icon'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveTextContent('check'); + expect(icon).toHaveClass( + 'block p-[12px] text-[20px] text-[--ods-color-primary-500]', + ); + }); + + it('renders the Order element when checked is false and open is false', () => { + renderWithStepContext({ checked: false, open: false, order: 1 }); + const span = screen.getByText(/1/i); + expect(span).toBeInTheDocument(); + expect(span).toHaveClass( + 'border-[--ods-color-neutral-500] text-[--ods-color-neutral-500]', + ); + }); + + it('renders the Order element with primary color when checked is false and open is true', () => { + renderWithStepContext({ checked: false, open: true, order: 1 }); + const span = screen.getByText(/1/i); + expect(span).toBeInTheDocument(); + expect(span).toHaveClass( + 'border-[--ods-color-primary-500] text-[--ods-color-text]', + ); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tabs/Tabs.component.tsx b/packages/manager-ui-kit/src/components/tabs/Tabs.component.tsx new file mode 100644 index 000000000000..10af06997f87 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/Tabs.component.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { Tabs, TabList, Tab, TabsValueChangeEvent } from '@ovhcloud/ods-react'; +import { TabsProps } from './Tabs.props'; + +export function TabsComponent({ + items = [], + titleElement = ({ item }) => <>{`${item}`}, + contentElement = ({ item }) => <>{`${item}`}, + className, + onChange, +}: TabsProps): JSX.Element { + const [selectedTabItem, setselectedTabItem] = useState( + items?.[0] as string, + ); + + const TitleComponent = titleElement; + const ContentComponent = contentElement; + + return ( +
+ { + setselectedTabItem(value?.value); + onChange?.(value as Item); + }} + value={selectedTabItem} + > + + {items.map((item) => ( + + + + ))} + + +
+ +
+
+ ); +} + +export default TabsComponent; diff --git a/packages/manager-ui-kit/src/components/tabs/Tabs.props.ts b/packages/manager-ui-kit/src/components/tabs/Tabs.props.ts new file mode 100644 index 000000000000..c8cedde5c8b0 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/Tabs.props.ts @@ -0,0 +1,13 @@ +export type TabsProps = { + items?: Item[]; + titleElement?: ({ + item, + isSelected, + }: { + item: Item; + isSelected?: boolean; + }) => JSX.Element; + contentElement?: ({ item }: { item: Item }) => JSX.Element; + className?: string; + onChange?: (item: Item) => void; +}; diff --git a/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.snapshot.spec.tsx b/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.snapshot.spec.tsx new file mode 100644 index 000000000000..2d51db0056d1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.snapshot.spec.tsx @@ -0,0 +1,104 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { TabsComponent } from '../Tabs.component'; + +describe('TabsComponent', () => { + const mockItems = ['tab1', 'tab2', 'tab3']; + + describe('Snapshot tests', () => { + it('should match snapshot with default props', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with empty items array', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with custom className', () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with custom titleElement', () => { + const customTitleElement = ({ + item, + isSelected, + }: { + item?: string; + isSelected?: boolean; + }) => ( + + Custom {item} + + ); + + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with custom contentElement', () => { + const customContentElement = ({ item }: { item?: string }) => ( +
+ Custom content for {item} +
+ ); + + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with single item', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with custom item types', () => { + interface CustomItem { + id: string; + label: string; + content: string; + } + + const customItems: CustomItem[] = [ + { id: '1', label: 'First', content: 'Content 1' }, + { id: '2', label: 'Second', content: 'Content 2' }, + ]; + + const customTitleElement = ({ + item, + isSelected, + }: { + item?: CustomItem; + isSelected?: boolean; + }) => ( + + {item?.label} + + ); + + const customContentElement = ({ item }: { item?: CustomItem }) => ( +
{item?.content}
+ ); + + const { container } = render( + + items={customItems} + titleElement={customTitleElement} + contentElement={customContentElement} + />, + ); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.spec.tsx b/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.spec.tsx new file mode 100644 index 000000000000..6f67183e1db5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/__tests__/Tabs.spec.tsx @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '@/setupTest'; +import { TabsComponent } from '../Tabs.component'; + +describe('TabsComponent', () => { + const mockItems = ['tab1', 'tab2', 'tab3']; + const mockOnChange = vi.fn(); + + describe('Basic rendering', () => { + it('should render the component with default items', () => { + render(); + + // Check that tabs are rendered + expect(screen.getAllByText('tab1')[0]).toBeInTheDocument(); + expect(screen.getByText('tab2')).toBeInTheDocument(); + expect(screen.getByText('tab3')).toBeInTheDocument(); + + // Check that the first tab content is displayed by default + expect(screen.getAllByText('tab1')).toHaveLength(2); // One in tab, one in content + }); + + it('should use the first item as the default selected tab', () => { + render(); + + // The first tab should be selected by default + const firstTab = screen.getAllByText('tab1')[0].closest('button'); + expect(firstTab).toHaveAttribute('aria-selected', 'true'); + }); + + it('should display the content of the first tab by default', () => { + render(); + + // Should show content for the first tab + const contentArea = screen.getAllByText('tab1', { selector: 'div' })[0]; + expect(contentArea).toBeInTheDocument(); + }); + + it('should apply the custom CSS class', () => { + const customClass = 'custom-tabs-class'; + const { container } = render( + , + ); + + const div = container.querySelector('div'); + expect(div).toHaveClass(customClass); + }); + }); + + describe('Optional props handling', () => { + it('should handle an empty items array', () => { + render(); + + // Should render without crashing + expect(screen.getByRole('tablist')).toBeInTheDocument(); + }); + + it('should use default functions for titleElement and contentElement', () => { + render(); + + // Should render default tab titles + expect(screen.getAllByText('tab1')[0]).toBeInTheDocument(); + expect(screen.getByText('tab2')).toBeInTheDocument(); + expect(screen.getByText('tab3')).toBeInTheDocument(); + + // Should render default content + expect(screen.getAllByText('tab1')).toHaveLength(2); + }); + }); + + describe('Event handling', () => { + it('should call onChange when a tab is selected', async () => { + render(); + + // Click on the second tab + const secondTab = screen.getByText('tab2'); + await userEvent.click(secondTab); + + // Should call onChange with the selected item + expect(mockOnChange).toBeCalled(); + }); + + it('should not throw if onChange is not provided', () => { + render(); + + const secondTab = screen.getByText('tab2'); + + // The component should not throw even if onChange is not defined + expect(() => { + userEvent.click(secondTab); + }).not.toThrow(); + }); + }); + + describe('Accessibility and structure', () => { + it('should have the correct HTML structure', () => { + render(); + expect(screen.getByRole('tablist')).toBeInTheDocument(); + const tabs = screen.getAllByRole('tab'); + expect(tabs).toHaveLength(3); + }); + }); + + describe('Custom element props', () => { + it('should pass isSelected prop to titleElement', () => { + const customTitleElement = vi.fn( + ({ item, isSelected }: { item?: string; isSelected?: boolean }) => ( + + Custom {item} + + ), + ); + + render( + , + ); + + // Check that the first tab (selected by default) has isSelected=true + const firstTabTitle = screen.getByTestId('custom-title-tab1'); + expect(firstTabTitle).toHaveAttribute('data-selected', 'true'); + + // Check that other tabs have isSelected=false + const secondTabTitle = screen.getByTestId('custom-title-tab2'); + expect(secondTabTitle).toHaveAttribute('data-selected', 'false'); + }); + + it('should render custom contentElement with selected item', () => { + const customContentElement = vi.fn(({ item }: { item?: string }) => ( +
+ Custom content for {item} +
+ )); + + render( + , + ); + + // Should render content for the first tab (selected by default) + expect(screen.getByTestId('custom-content-tab1')).toBeInTheDocument(); + expect(screen.getByText('Custom content for tab1')).toBeInTheDocument(); + }); + + it('should update content when switching tabs', async () => { + const customContentElement = vi.fn(({ item }: { item?: string }) => ( +
+ Custom content for {item} +
+ )); + + render( + , + ); + + // Initially should show content for first tab + expect(screen.getByTestId('custom-content-tab1')).toBeInTheDocument(); + expect(screen.getByText('Custom content for tab1')).toBeInTheDocument(); + + // Click on second tab + const secondTab = screen.getByText('tab2'); + await userEvent.click(secondTab); + + // Should now show content for second tab + expect(screen.getByTestId('custom-content-tab2')).toBeInTheDocument(); + expect(screen.getByText('Custom content for tab2')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tabs/__tests__/__snapshots__/Tabs.snapshot.spec.tsx.snap b/packages/manager-ui-kit/src/components/tabs/__tests__/__snapshots__/Tabs.snapshot.spec.tsx.snap new file mode 100644 index 000000000000..5e1f86d598f9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/__tests__/__snapshots__/Tabs.snapshot.spec.tsx.snap @@ -0,0 +1,533 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TabsComponent > Snapshot tests > should match snapshot with custom className 1`] = ` +
+
+
+
+ + + +
+
+
+ tab1 +
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with custom contentElement 1`] = ` +
+
+
+
+ + + +
+
+
+
+ Custom content for + tab1 +
+
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with custom item types 1`] = ` +
+
+
+
+ + +
+
+
+
+ Content 1 +
+
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with custom titleElement 1`] = ` +
+
+
+
+ + + +
+
+
+ tab1 +
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with default props 1`] = ` +
+
+
+
+ + + +
+
+
+ tab1 +
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with empty items array 1`] = ` +
+
+
+
+
+
+ undefined +
+
+
+`; + +exports[`TabsComponent > Snapshot tests > should match snapshot with single item 1`] = ` +
+
+
+
+ +
+
+
+ single-tab +
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/tabs/index.ts b/packages/manager-ui-kit/src/components/tabs/index.ts new file mode 100644 index 000000000000..40a52232fb19 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tabs/index.ts @@ -0,0 +1,2 @@ +export { TabsComponent } from './Tabs.component'; +export type { TabsProps } from './Tabs.props'; diff --git a/packages/manager-ui-kit/src/components/tags-list/TagsList.component.tsx b/packages/manager-ui-kit/src/components/tags-list/TagsList.component.tsx new file mode 100644 index 000000000000..803da72d95cf --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/TagsList.component.tsx @@ -0,0 +1,51 @@ +import { useMemo, useState, lazy, FC, Suspense } from 'react'; +import { TagsStack } from './tags-stack/TagsStack.component'; +import { filterTags } from './TagsList.utils'; +import { TagsListProps } from './TagsList.props'; + +const TagsModal = lazy(() => import('./tags-modal/TagsModal.component')); + +export const TagsList: FC = ({ + tags, + displayInternalTags = false, + maxLines, + modalHeading, + onEditTags, +}) => { + const [open, setOpen] = useState(false); + const filteredTags: string[] = useMemo( + () => filterTags({ tags, displayInternalTags }), + [tags, displayInternalTags], + ); + + return ( + <> + { + setOpen(true); + }} + /> + {open && modalHeading && ( + + { + onEditTags(); + setOpen(false); + }, + })} + onCancel={() => setOpen(false)} + onOpenChange={(detail) => { + setOpen(detail?.open ?? false); + }} + /> + + )} + + ); +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/TagsList.props.ts b/packages/manager-ui-kit/src/components/tags-list/TagsList.props.ts new file mode 100644 index 000000000000..760c2ea64f0e --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/TagsList.props.ts @@ -0,0 +1,8 @@ +export type TagsListProps = { + tags: { [key: string]: string }; + displayInternalTags?: boolean; + maxLines?: number; + modalHeading?: string; + onEditTags?: () => void; + lineNumber?: number; +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/TagsList.utils.ts b/packages/manager-ui-kit/src/components/tags-list/TagsList.utils.ts new file mode 100644 index 000000000000..68a7b4d42b83 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/TagsList.utils.ts @@ -0,0 +1,13 @@ +export const filterTags = ({ + tags, + displayInternalTags, +}: { + tags: { [key: string]: string }; + displayInternalTags: boolean; +}): string[] => + Object.keys(tags).reduce((accumulator: string[], key: string) => { + if (displayInternalTags || !key.startsWith('ovh:')) { + accumulator.push(`${key}:${tags[key]}`); + } + return accumulator; + }, []); diff --git a/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.spec.tsx b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.spec.tsx new file mode 100644 index 000000000000..9a132c3ff76b --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.spec.tsx @@ -0,0 +1,94 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + render, + screen, + fireEvent, + waitFor, + act, + within, +} from '@testing-library/react'; +import { TagsList } from '../TagsList.component'; +import * as TagsStackUtils from '../tags-stack/TagsStack.utils'; + +vi.mock('../tags-stack/TagsStack.utils', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + getVisibleTagCount: vi.fn(), + }; +}); + +describe('TagsList Component', () => { + const mockTags = { + tag1: 'tag1', + tag2: 'tag2', + tag3: 'tag3', + tag4: 'tag4', + 'ovh:tag1': 'ovh:tag1', + 'ovh:tag2': 'ovh:tag2', + 'ovh:tag3': 'ovh:tag3', + }; + const modalHeader = 'Test Resource'; + const onEditTags = vi.fn(); + + it('renders all tags in TagsList without modal', () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(6); + render( + , + ); + Object.entries(mockTags).forEach(([key, value]) => { + expect(screen.getByText(`${key}:${value}`)).toBeInTheDocument(); + }); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + + it('renders Tags with modal (opens and close tags modal)', async () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(2); + const { baseElement } = render( + , + ); + const moreTagsButton = screen.getByRole('link'); + fireEvent.click(moreTagsButton); + await waitFor(() => { + expect( + screen.getByText(new RegExp(modalHeader, 'gi')), + ).toBeInTheDocument(); + const closeButton = within(baseElement).getByTestId('secondary-button'); + fireEvent.click(closeButton); + expect( + screen.queryByText(new RegExp(modalHeader, 'gi')), + ).not.toBeInTheDocument(); + }); + }); + + it('renders Tags Modal and calls onEditTags callback', async () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(2); + render( + , + ); + const moreTagsButton = screen.getByRole('link'); + fireEvent.click(moreTagsButton); + await waitFor(() => { + const editTagsButton = screen.getByTestId('primary-button'); + fireEvent.click(editTagsButton); + expect(onEditTags).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.test.tsx b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.test.tsx new file mode 100644 index 000000000000..f20002eb13dd --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { TagsList } from '../TagsList.component'; +import * as tagsStackUtils from '../tags-stack/TagsStack.utils'; + +vi.mock('../tags-stack/TagsStack.utils', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + getVisibleTagCount: vi.fn(), + }; +}); + +// Since JSDOM does not provide complete support for Layout APIs like offsetWidth +// (ref: https://github.com/jsdom/jsdom#unimplemented-parts-of-the-web-platform) +// Few scenarios like displaying the ellipsis, actual number of tags that are displayed in the available space +// are not tested. + +// TODO: Add Visual regression tests for the missing test scenarios + +describe('TagsList Component - Snapshot Tests', () => { + const mockTags = { + key1: 'Lorem ipsum dolor', + key2: 'Lorem ipsum consectetur sit amet consectetur', + key3: 'Lorem ipsum', + key4: 'Lorem ipsum dolor', + key5: 'Lorem ipsum dolor sit consectetur', + key6: 'Lorem ipsumsit amet consectetur adipiscing elit quisque', + key7: 'Lorem ipsum', + key8: 'Lorem ipsum', + key9: 'Lorem ipsum', + 'ovh:key1': 'Lorem ipsum', + 'ovh:key2': 'Lorem ipsum dolor', + }; + + const modalHeader = 'Test Resource'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('displays all tags', () => { + vi.mocked(tagsStackUtils.getVisibleTagCount).mockReturnValue(8); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('displays fewer tags along with Link', () => { + vi.mocked(tagsStackUtils.getVisibleTagCount).mockReturnValue(2); + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('displays modal on click of "More tags" link', async () => { + vi.mocked(tagsStackUtils.getVisibleTagCount).mockReturnValue(1); + + const { baseElement } = render( + , + ); + const moreTagsLink = screen.getByRole('link'); + fireEvent.click(moreTagsLink); + await waitFor(() => { + expect(baseElement).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.utils.spec.tsx b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.utils.spec.tsx new file mode 100644 index 000000000000..2e7708309fea --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/__tests__/TagsList.utils.spec.tsx @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { filterTags } from '../TagsList.utils'; + +describe('filterTags', () => { + const mockTags = { + env: 'production', + version: '1.0.0', + 'ovh:internal': 'secret-value', + 'ovh:debug': 'debug-info', + team: 'frontend', + 'ovh:temp': 'temporary-data', + }; + + it('should return empty array when tags object is empty', () => { + const result = filterTags({ tags: {}, displayInternalTags: false }); + expect(result).toEqual([]); + }); + + it('should filter out internal tags (starting with ovh:) when displayInternalTags is false', () => { + const result = filterTags({ tags: mockTags, displayInternalTags: false }); + expect(result).toEqual([ + 'env:production', + 'version:1.0.0', + 'team:frontend', + ]); + expect(result).not.toContain('ovh:internal:secret-value'); + expect(result).not.toContain('ovh:debug:debug-info'); + expect(result).not.toContain('ovh:temp:temporary-data'); + }); + + it('should include internal tags when displayInternalTags is true', () => { + const result = filterTags({ tags: mockTags, displayInternalTags: true }); + expect(result).toEqual([ + 'env:production', + 'version:1.0.0', + 'ovh:internal:secret-value', + 'ovh:debug:debug-info', + 'team:frontend', + 'ovh:temp:temporary-data', + ]); + }); + + it('should return all tags when there are no internal tags and displayInternalTags is false', () => { + const tagsWithoutInternal = { + env: 'staging', + version: '2.0.0', + team: 'backend', + }; + const result = filterTags({ + tags: tagsWithoutInternal, + displayInternalTags: false, + }); + + expect(result).toEqual(['env:staging', 'version:2.0.0', 'team:backend']); + }); + + it('should return empty array when all tags are internal and displayInternalTags is false', () => { + const onlyInternalTags = { + 'ovh:config': 'value1', + 'ovh:secret': 'value2', + 'ovh:data': 'value3', + }; + const result = filterTags({ + tags: onlyInternalTags, + displayInternalTags: false, + }); + expect(result).toEqual([]); + }); + + it('should handle prototype pollution edge cases', () => { + const pollutedTags = Object.create({ + 'ovh:polluted': 'should-not-appear', + }); + pollutedTags['normal-tag'] = 'normal-value'; + + const result = filterTags({ + tags: pollutedTags, + displayInternalTags: false, + }); + + expect(result).toEqual(['normal-tag:normal-value']); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-list/__tests__/__snapshots__/TagsList.test.tsx.snap b/packages/manager-ui-kit/src/components/tags-list/__tests__/__snapshots__/TagsList.test.tsx.snap new file mode 100644 index 000000000000..8d0cd82881da --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/__tests__/__snapshots__/TagsList.test.tsx.snap @@ -0,0 +1,136 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TagsList Component - Snapshot Tests > displays all tags 1`] = ` +
+
+ + key1:Lorem ipsum dolor + + + key2:Lorem ipsum consectetur sit amet consectetur + + + key3:Lorem ipsum + + + key4:Lorem ipsum dolor + + + key5:Lorem ipsum dolor sit consectetur + + + key6:Lorem ipsumsit amet consectetur adipiscing elit quisque + + + key7:Lorem ipsum + + + key8:Lorem ipsum + + + key9:Lorem ipsum + +
+
+`; + +exports[`TagsList Component - Snapshot Tests > displays fewer tags along with Link 1`] = ` +
+
+ + key1:Lorem ipsum dolor + + + key2:Lorem ipsum consectetur sit amet consectetur + + + key3:Lorem ipsum + + + + ​ + + + +
+
+`; + +exports[`TagsList Component - Snapshot Tests > displays modal on click of "More tags" link 1`] = ` + +
+
+ + key1:Lorem ipsum dolor + + + key2:Lorem ipsum consectetur sit amet consectetur + + + + ​ + + + +
+
+ +`; diff --git a/packages/manager-ui-kit/src/components/tags-list/index.ts b/packages/manager-ui-kit/src/components/tags-list/index.ts new file mode 100644 index 000000000000..1ca33d652533 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/index.ts @@ -0,0 +1,3 @@ +export { TagsList } from './TagsList.component'; + +export type { TagsListProps } from './TagsList.props'; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.component.tsx b/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.component.tsx new file mode 100644 index 000000000000..adbd862b6838 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.component.tsx @@ -0,0 +1,86 @@ +import React, { + forwardRef, + ElementRef, + useState, + useCallback, + ForwardedRef, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + BUTTON_VARIANT, + BUTTON_SIZE, + Input, + MODAL_COLOR, + ModalContent, +} from '@ovhcloud/ods-react'; +import { Modal } from '../../modal'; +import './translations'; +import { TagsStack } from '../tags-stack/TagsStack.component'; +import { TagsModalProps } from './TagsModal.props'; + +export const TagsModal = forwardRef( + ( + { + open = false, + heading, + tags, + onEditTags, + onCancel, + onOpenChange, + }: TagsModalProps, + ref: ForwardedRef>, + ) => { + const { t } = useTranslation('tags-modal'); + const [search, setSearch] = useState(''); + const [results, setResults] = useState(tags); + + const handleSearch = useCallback(() => { + setResults(search ? tags.filter((tag) => tag.includes(search)) : tags); + }, [search, tags]); + + return ( + +
+ ) => { + setSearch(event.target.value); + }} + /> + +
+
+ {results && } +
+
+ ); + }, +); + +export default TagsModal; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.props.tsx b/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.props.tsx new file mode 100644 index 000000000000..1f5304a8c583 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-modal/TagsModal.props.tsx @@ -0,0 +1,10 @@ +import { ModalOpenChangeDetail } from '@ovhcloud/ods-react'; + +export type TagsModalProps = { + heading: string; + open: boolean; + tags: string[]; + onCancel: () => void; + onEditTags?: () => void; + onOpenChange?: (detail?: ModalOpenChangeDetail) => void; +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-modal/__tests__/TagsModal.spec.tsx b/packages/manager-ui-kit/src/components/tags-list/tags-modal/__tests__/TagsModal.spec.tsx new file mode 100644 index 000000000000..f0c94944fec1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-modal/__tests__/TagsModal.spec.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import { TagsModal } from '../TagsModal.component'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('TagsModal', () => { + const mockTags = ['tag1:tag1', 'tag2:tag2', 'tag3:tage3', 'tag4:tag4']; + const heading = 'Test Resource'; + const onCancel = vi.fn(); + const onEditTags = vi.fn(); + + it('renders the Tags Modal', () => { + render( + , + ); + + expect(screen.queryByText(new RegExp(heading, 'gi'))).toBeInTheDocument(); + expect(screen.queryByText('back').tagName).toBe('BUTTON'); + expect(screen.queryByText('edit_tags').tagName).toBe('BUTTON'); + }); + + it('renders the Tags Modal without "Edit Tags" button', () => { + render( + , + ); + + expect(screen.queryByText('edit_tags')).not.toBeInTheDocument(); + }); + + it('calls "onClose" callback when close button is clicked', () => { + render( + , + ); + const closeButton = screen.getByText('back'); + fireEvent.click(closeButton); + expect(onCancel).toHaveBeenCalledOnce(); + }); + + it('calls "onEditTags" callback when Edit Tags button is clicked', () => { + render( + , + ); + const editTagsButton = screen.getByText('edit_tags'); + fireEvent.click(editTagsButton); + expect(onEditTags).toHaveBeenCalledOnce(); + }); + + it('filters tags correctly when searching', async () => { + render( + {}} + onEditTags={() => {}} + />, + ); + + const input = screen.getByPlaceholderText('search_placeholder'); + fireEvent.change(input, { target: { value: 'tag1' } }); + + const searchButton = screen.getByText('search'); + fireEvent.click(searchButton); + expect(screen.getAllByText(/tag1/i).length).toBe(1); + expect(screen.queryAllByText(/tag2/i).length).toBe(0); + + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(searchButton); + expect(screen.queryAllByText(/tag2/i).length).toBe(1); + }); +}); diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/tags-modal/translations/index.ts b/packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/tags-modal/translations/index.ts rename to packages/manager-ui-kit/src/components/tags-list/tags-modal/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.component.tsx b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.component.tsx new file mode 100644 index 000000000000..9d89d944de30 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.component.tsx @@ -0,0 +1,96 @@ +import { useRef, useState, useEffect, MouseEvent, FC } from 'react'; +import { Badge, BADGE_COLOR, Icon, ICON_NAME, Link } from '@ovhcloud/ods-react'; +import { getVisibleTagCount } from './TagsStack.utils'; +import { HTMLBadgeElement } from './TagsStack.type'; +import { BADGE_SPACINGS, MORE_TAGS_ICON_WIDTH } from './TagsStack.constants'; +import { TagsStackProps } from './TagsStack.props'; + +export const TagsStack: FC = ({ tags, maxLines, onClick }) => { + const [visibleTagCount, setVisibleTagCount] = useState(0); + const [maxWidth, setMaxWidth] = useState(); + const containerRef = useRef(null); + const tagRef = useRef(null); + + useEffect(() => { + // Display all tags if maxLines is not set + if (!maxLines) { + setVisibleTagCount(tags.length - 1); + } else if (tags.length > 1 && tagRef.current && containerRef.current) { + const resizeObserver = new ResizeObserver(() => { + const visibleTags = getVisibleTagCount( + tags, + tagRef.current as HTMLBadgeElement, + containerRef.current as HTMLDivElement, + maxLines, + ); + + setVisibleTagCount(visibleTags); + }); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + } + return () => {}; + }, [tags, maxLines]); + + useEffect(() => { + if (containerRef.current) { + setMaxWidth( + containerRef.current.offsetWidth - + BADGE_SPACINGS - + MORE_TAGS_ICON_WIDTH, + ); + } + }, [containerRef]); + + if (!tags.length) { + return null; + } + + return ( +
+ + {tags[0]} + + {tags.slice(1, visibleTagCount + 1).map((tag) => { + return ( + + {tag} + + ); + })} + + {visibleTagCount + 1 < tags.length && ( + ) => { + if (onClick) onClick(); + e.preventDefault(); + }} + > + + + )} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.constants.ts b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.constants.ts new file mode 100644 index 000000000000..fef29dc81a58 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.constants.ts @@ -0,0 +1,15 @@ +const BADGE_MARGIN_RIGHT = 4; + +const BADGE_MARGIN_LEFT = 0; + +const BADGE_PADDING_LEFT = 8; + +const BADGE_PADDING_RIGHT = 8; + +export const BADGE_SPACINGS = + BADGE_MARGIN_RIGHT + + BADGE_MARGIN_LEFT + + BADGE_PADDING_LEFT + + BADGE_PADDING_RIGHT; + +export const MORE_TAGS_ICON_WIDTH = 12; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.props.ts b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.props.ts new file mode 100644 index 000000000000..7fcd5334156c --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.props.ts @@ -0,0 +1,5 @@ +export type TagsStackProps = { + tags: string[]; + maxLines?: number; + onClick?: () => void; +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.type.ts b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.type.ts new file mode 100644 index 000000000000..d2c268bf19cd --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.type.ts @@ -0,0 +1,4 @@ +import { ElementRef } from 'react'; +import { Badge } from '@ovhcloud/ods-react'; + +export type HTMLBadgeElement = ElementRef; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.utils.ts b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.utils.ts new file mode 100644 index 000000000000..162cbf5e19f2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/TagsStack.utils.ts @@ -0,0 +1,80 @@ +import { HTMLBadgeElement } from './TagsStack.type'; +import { MORE_TAGS_ICON_WIDTH } from './TagsStack.constants'; + +const getBadgeSpacings = (badgeStyles: CSSStyleDeclaration) => { + return ( + parseFloat(badgeStyles.marginRight) + + parseFloat(badgeStyles.marginLeft) + + parseFloat(badgeStyles.paddingLeft) + + parseFloat(badgeStyles.paddingRight) + ); +}; + +const getRenderedBadgeWidth = ({ + tag, + fontSize, + fontFamily, +}: { + tag: string; + fontSize: number; + fontFamily: string; +}) => { + const canvas = document.createElement('canvas') as HTMLCanvasElement; + const context = canvas.getContext('2d') as CanvasRenderingContext2D; + context.font = `${fontSize} ${fontFamily}`; + return context.measureText(tag).width; +}; + +export const getVisibleTagCount = ( + tags: string[], + badge: HTMLBadgeElement, + container: HTMLDivElement, + maxLines: number, +): number => { + const { offsetWidth: containerWidth } = container; + + const currentBadgeStyles = window.getComputedStyle(badge); + + const BADGE_SPACINGS = getBadgeSpacings(currentBadgeStyles); + + const BADGE_FONT_SIZE = parseFloat(currentBadgeStyles.fontSize); + const BADGE_FONT_FAMILY = currentBadgeStyles.fontFamily; + + let currentLine = 1; + // first badge is already displayed thus removing it's space from available space + let availableWidth = containerWidth - badge.offsetWidth - BADGE_SPACINGS; + let count = 0; + let index = 1; + + // calculating the number of visible tags using flex behaviour + // 1. calculate the space required to display the tag + // 2. If tag can be displayed in available space, allocate the space for current tag and update availableWidth + // 3. Else if tag cannot be displayed in availableWidth in current line, push the tag to next line and update availableWidth in next line + // 4. If tag requires entire line, then allocate the entire line for the current tag and increment currentLine by 1 + while (currentLine <= maxLines && index < tags.length) { + const textWidth = + getRenderedBadgeWidth({ + tag: tags[index] as string, + fontSize: BADGE_FONT_SIZE, + fontFamily: BADGE_FONT_FAMILY, + }) + BADGE_SPACINGS; + + if (textWidth > availableWidth) { + if (currentLine !== maxLines) { + currentLine += 1; + // allocate space for displaying more tags link for the last line + availableWidth = + containerWidth - + textWidth - + (currentLine === maxLines ? MORE_TAGS_ICON_WIDTH : 0); + } else { + break; + } + } else { + availableWidth -= textWidth; + } + count += 1; + index += 1; + } + return count; +}; diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.spec.tsx b/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.spec.tsx new file mode 100644 index 000000000000..945e5fbb454a --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.spec.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { TagsStack } from '../TagsStack.component'; +import * as TagsStackUtils from '../TagsStack.utils'; + +vi.mock('../TagsStack.utils', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + getVisibleTagCount: vi.fn(), + }; +}); + +const mockTags = ['tag1:tag1', 'tag2:tag2', 'tag3:tage3', 'tag4:tag4']; + +describe('TagsStack', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders nothing if tags are empty', () => { + const { container, rerender } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders all tags when maxLines is not provided', () => { + render(); + + mockTags.forEach((tag) => { + expect(screen.getByText(tag)).toBeInTheDocument(); + }); + }); + + it('calls getVisibleTagCount when maxLines is provided', () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(2); + + render(); + + expect(screen.getByText(/tag1/i)).toBeInTheDocument(); + expect(screen.getByText(/tag2/i)).toBeInTheDocument(); + expect(screen.getByText(/tag3/i)).toBeInTheDocument(); + expect(screen.queryByText(/tag4/i)).not.toBeInTheDocument(); + + // Should show "more" icon + expect(screen.getByRole('link')).toBeInTheDocument(); + }); + + it('does not show more icon if all tags are visible', () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(3); + render(); + + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + + it('calls onClick when more icon is clicked', () => { + vi.mocked(TagsStackUtils.getVisibleTagCount).mockReturnValue(1); + + const handleClick = vi.fn(); + render(); + + const link = screen.getByRole('link'); + link.click(); + + expect(handleClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.utils.spec.ts b/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.utils.spec.ts new file mode 100644 index 000000000000..30aeee26f0f3 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-list/tags-stack/__tests__/TagsStack.utils.spec.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { getVisibleTagCount } from '../TagsStack.utils'; +import { HTMLBadgeElement } from '../TagsStack.type'; + +// mock canvas element context for determining the width of the Badge +const mockConvasElementContext = { + measureText: vi.fn().mockReturnValue({ width: 60 }), + font: '', +}; +global.HTMLCanvasElement.prototype.getContext = vi + .fn() + .mockReturnValue(mockConvasElementContext); + +// mock window.getComputedStyle that determing sample badge's styles +Object.defineProperty(window, 'getComputedStyle', { + value: () => ({ + marginRight: '10px', + marginLeft: '10px', + paddingLeft: '10px', + paddingRight: '10px', + fontSize: '12px', + fontFamily: 'Arial', + }), + writable: true, +}); + +// Assume the sample badge's width as 60px +const mockBadge = { + offsetWidth: 60, +} as HTMLBadgeElement; + +// Assuming every badge will be 60px and padding-y to be 20px and margin-y to be 20px. +// Space required for each badge display is 100px +describe('getVisibleTagCount', () => { + const mockTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5']; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return 0 when no tags are provided', () => { + const mockContainer = { + offsetWidth: 500, + } as HTMLDivElement; + + const result = getVisibleTagCount([], mockBadge, mockContainer, 2); + expect(result).toBe(0); + }); + + it('should fit all tags when there is enough space in one line', () => { + const mockContainer = { + offsetWidth: 1000, // Large enough for all tags + } as HTMLDivElement; + + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 1); + expect(result).toBe(4); // tag1 is already displayed, so 4 more can fit + }); + + it('should respect maxLines limit and break to next line when needed', () => { + const mockContainer = { + offsetWidth: 100, // Limited space + } as HTMLDivElement; + + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 3); + expect(result).toBe(2); + }); + + it('should break to next line when a tag is too wide for current line', () => { + const mockContainer = { + offsetWidth: 90, // Very limited space + } as HTMLDivElement; + + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 3); + expect(result).toBe(2); + }); + + it('should account for MORE_TAGS_ICON_WIDTH on the last line', () => { + const mockContainer = { + offsetWidth: 200, + } as HTMLDivElement; + + // each line can accomodate 2 badges, so without icon space, the function is expected to return 3. + // Since icon has to be displayed, there is not enough space for 3rd badge to be displayed + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 2); + expect(result).toBe(2); + }); + + it('should handle single line constraint', () => { + const mockContainer = { + offsetWidth: 180, + } as HTMLDivElement; + + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 1); + expect(result).toBeGreaterThanOrEqual(0); + }); + + it('should return 0 when container width is smaller than first badge', () => { + const mockContainer = { + offsetWidth: 50, // Smaller than badge + } as HTMLDivElement; + + const result = getVisibleTagCount(mockTags, mockBadge, mockContainer, 1); + expect(result).toBe(0); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-tile/TagsTile.component.tsx b/packages/manager-ui-kit/src/components/tags-tile/TagsTile.component.tsx new file mode 100644 index 000000000000..cba50f6bf34d --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/TagsTile.component.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { TagsList, TagsListProps } from '../tags-list'; +import './translations'; +import { Tile } from '../tile'; +import { Link, LinkType } from '../Link'; +import { TagsTileProps } from './TagsTile.props'; + +export const TagsTile: React.FC = ({ + tags, + displayInternalTags = false, + lineNumber = 5, + onEditTags, +}) => { + const { t } = useTranslation('tags-tile'); + const isEmptyTags = !tags || Object.keys(tags).length === 0; + + return ( + + + + +
+ {isEmptyTags && {t('tags_tile_empty')}} + {!isEmptyTags && ( + + )} +
+ { + onEditTags?.(); + e.preventDefault(); + }} + > + {isEmptyTags ? t('tags_tile_add_tag') : t('manage_tags')} + +
+
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tags-tile/TagsTile.props.ts b/packages/manager-ui-kit/src/components/tags-tile/TagsTile.props.ts new file mode 100644 index 000000000000..de8ad3e0d0d2 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/TagsTile.props.ts @@ -0,0 +1,6 @@ +import { TagsListProps } from '../tags-list'; + +export interface TagsTileProps extends Omit { + onEditTags?: () => void; + lineNumber?: number; +} diff --git a/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.snapshot.test.tsx b/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.snapshot.test.tsx new file mode 100644 index 000000000000..3862e6995138 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.snapshot.test.tsx @@ -0,0 +1,106 @@ +import { vitest } from 'vitest'; +import { TagsTile } from '../TagsTile.component'; +import { render } from '../../../../setupTest'; + +describe('TagsTile Snapshot Tests', () => { + const mockOnEditTags = vitest.fn(); + + afterEach(() => { + vitest.clearAllMocks(); + }); + + describe('Empty State', () => { + it('should render empty tags tile', () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render with null tags', () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render without onEditTags callback', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('With Tags', () => { + const mockTags = { + environment: 'production', + team: 'frontend', + version: '1.0.0', + }; + + it('should render tags tile with tags', () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render with custom lineNumber', () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render with displayInternalTags enabled', () => { + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('Complex Scenarios', () => { + it('should render with many tags', () => { + const manyTags = { + env: 'production', + team: 'frontend', + version: '1.0.0', + region: 'eu-west-1', + owner: 'john-doe', + project: 'my-project', + cost_center: 'engineering', + department: 'IT', + }; + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should render with single tag', () => { + const singleTag = { environment: 'staging' }; + + const { container } = render( + , + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.spec.tsx b/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.spec.tsx new file mode 100644 index 000000000000..727b022bc6db --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/__tests__/TagsTile.spec.tsx @@ -0,0 +1,113 @@ +import { screen, fireEvent } from '@testing-library/react'; +import { vitest } from 'vitest'; +import { TagsTile } from '../TagsTile.component'; +import { render } from '../../../../setupTest'; +import fr_FR from '../translations/Messages_fr_FR.json'; + +describe('TagsTile component', () => { + const mockOnEditTags = vitest.fn(); + + afterEach(() => { + vitest.clearAllMocks(); + }); + + describe('when tags are empty', () => { + it('should display empty state message', () => { + render(); + + expect(screen.getByText(fr_FR.tags_tile_empty)).toBeInTheDocument(); + }); + + it('should display "Add a tag" link when no tags', () => { + render(); + + expect(screen.getByText(fr_FR.tags_tile_add_tag)).toBeInTheDocument(); + }); + + it('should call onEditTags when clicking add tag link', () => { + render(); + + const addTagLink = screen.getByText(fr_FR.tags_tile_add_tag); + fireEvent.click(addTagLink); + + expect(mockOnEditTags).toHaveBeenCalledTimes(1); + }); + }); + + describe('when tags are provided', () => { + const mockTags = { + environment: 'production', + team: 'frontend', + version: '1.0.0', + }; + + it('should display the tags tile title', () => { + render(); + + expect(screen.getByText(fr_FR.tags_tile_title)).toBeInTheDocument(); + }); + + it('should display "Manage tags" link when tags exist', () => { + render(); + + expect(screen.getByText(fr_FR.manage_tags)).toBeInTheDocument(); + }); + + it('should call onEditTags when clicking manage tags link', () => { + render(); + + const manageTagsLink = screen.getByText(fr_FR.manage_tags); + fireEvent.click(manageTagsLink); + + expect(mockOnEditTags).toHaveBeenCalledTimes(1); + }); + + it('should not display empty state message', () => { + render(); + + expect(screen.queryByText(fr_FR.tags_tile_empty)).not.toBeInTheDocument(); + }); + }); + + describe('props handling', () => { + it('should work without onEditTags callback', () => { + render(); + + const addTagLink = screen.getByText(fr_FR.tags_tile_add_tag); + fireEvent.click(addTagLink); + + // Should not throw error + expect(addTagLink).toBeInTheDocument(); + }); + + it('should handle null tags', () => { + render(); + + expect(screen.getByText(fr_FR.tags_tile_empty)).toBeInTheDocument(); + }); + + it('should handle undefined tags', () => { + render(); + + expect(screen.getByText(fr_FR.tags_tile_empty)).toBeInTheDocument(); + }); + }); + + describe('accessibility', () => { + it('should render link with href attribute', () => { + render(); + + const link = screen.getByText(fr_FR.tags_tile_add_tag); + expect(link).toHaveAttribute('href', '#'); + }); + + it('should prevent default link behavior on click', () => { + render(); + + const link = screen.getByText(fr_FR.tags_tile_add_tag); + const event = fireEvent.click(link); + + expect(event).toBe(false); // preventDefault was called + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tags-tile/__tests__/__snapshots__/TagsTile.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/tags-tile/__tests__/__snapshots__/TagsTile.snapshot.test.tsx.snap new file mode 100644 index 000000000000..28917ca33a77 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/__tests__/__snapshots__/TagsTile.snapshot.test.tsx.snap @@ -0,0 +1,634 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TagsTile Snapshot Tests > Complex Scenarios > should render with many tags 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+
+ + env:production + + + team:frontend + + + version:1.0.0 + + + region:eu-west-1 + + + owner:john-doe + + + project:my-project + + + cost_center:engineering + + + department:IT + +
+
+ + Manager les tags + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > Complex Scenarios > should render with single tag 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+
+ + environment:staging + +
+
+ + Manager les tags + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > Empty State > should render empty tags tile 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+ + Aucun tag n'est associé + +
+ + Ajouter un tag + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > Empty State > should render with null tags 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+ + Aucun tag n'est associé + +
+ + Ajouter un tag + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > Empty State > should render without onEditTags callback 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+ + Aucun tag n'est associé + +
+ + Ajouter un tag + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > With Tags > should render tags tile with tags 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+
+ + environment:production + + + team:frontend + + + version:1.0.0 + +
+
+ + Manager les tags + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > With Tags > should render with custom lineNumber 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+
+ + environment:production + + + team:frontend + + + version:1.0.0 + +
+
+ + Manager les tags + + +
+
+
+
+
+
+
+`; + +exports[`TagsTile Snapshot Tests > With Tags > should render with displayInternalTags enabled 1`] = ` +
+
+
+

+ Tags +

+
+
+
+
+
+ + Tags assignés + +
+
+
+
+
+ + environment:production + + + team:frontend + + + version:1.0.0 + +
+
+ + Manager les tags + + +
+
+
+
+
+
+
+`; diff --git a/packages/manager-ui-kit/src/components/tags-tile/index.tsx b/packages/manager-ui-kit/src/components/tags-tile/index.tsx new file mode 100644 index 000000000000..56d1c625fee5 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/index.tsx @@ -0,0 +1,2 @@ +export { TagsTile } from './TagsTile.component'; +export type { TagsTileProps } from './TagsTile.props'; diff --git a/packages/manager-ui-kit/src/components/tags-tile/tags-tile.component.tsx b/packages/manager-ui-kit/src/components/tags-tile/tags-tile.component.tsx new file mode 100644 index 000000000000..cba50f6bf34d --- /dev/null +++ b/packages/manager-ui-kit/src/components/tags-tile/tags-tile.component.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { TagsList, TagsListProps } from '../tags-list'; +import './translations'; +import { Tile } from '../tile'; +import { Link, LinkType } from '../Link'; +import { TagsTileProps } from './TagsTile.props'; + +export const TagsTile: React.FC = ({ + tags, + displayInternalTags = false, + lineNumber = 5, + onEditTags, +}) => { + const { t } = useTranslation('tags-tile'); + const isEmptyTags = !tags || Object.keys(tags).length === 0; + + return ( + + + + +
+ {isEmptyTags && {t('tags_tile_empty')}} + {!isEmptyTags && ( + + )} +
+ { + onEditTags?.(); + e.preventDefault(); + }} + > + {isEmptyTags ? t('tags_tile_add_tag') : t('manage_tags')} + +
+
+
+ ); +}; diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/tags-tile/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/tags-tile/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/tags-tile/translations/index.ts b/packages/manager-ui-kit/src/components/tags-tile/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/tags-tile/translations/index.ts rename to packages/manager-ui-kit/src/components/tags-tile/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/text/Text.component.tsx b/packages/manager-ui-kit/src/components/text/Text.component.tsx new file mode 100644 index 000000000000..03abc23dcadb --- /dev/null +++ b/packages/manager-ui-kit/src/components/text/Text.component.tsx @@ -0,0 +1,37 @@ +import { + Text as OdsText, + Tooltip, + TooltipTrigger, + TooltipContent, + TOOLTIP_POSITION, +} from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import './translations'; + +import { useAuthorizationIam } from '../../hooks/iam'; +import { TextProps } from './Text.props'; + +export const Text = ({ + children, + iamActions = [], + urn = '', + tooltipPosition = TOOLTIP_POSITION.bottom, + ...restProps +}: TextProps) => { + const { t } = useTranslation('iam'); + const { isAuthorized } = useAuthorizationIam(iamActions, urn); + + if (isAuthorized || !(iamActions && urn)) { + return {children}; + } + return ( + + + {t('iam_hidden_text').toUpperCase()} + + + {t('common_iam_get_message')} + + + ); +}; diff --git a/packages/manager-ui-kit/src/components/text/Text.props.ts b/packages/manager-ui-kit/src/components/text/Text.props.ts new file mode 100644 index 000000000000..7cc4959b65a9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/text/Text.props.ts @@ -0,0 +1,9 @@ +import { TextProp, TOOLTIP_POSITION } from '@ovhcloud/ods-react'; +import { PropsWithChildren } from 'react'; + +export type TextProps = PropsWithChildren<{ + iamActions?: string[]; + urn?: string; + tooltipPosition?: TOOLTIP_POSITION; +}> & + TextProp; diff --git a/packages/manager-ui-kit/src/components/text/__tests__/Text.spec.tsx b/packages/manager-ui-kit/src/components/text/__tests__/Text.spec.tsx new file mode 100644 index 000000000000..c690e94acd7f --- /dev/null +++ b/packages/manager-ui-kit/src/components/text/__tests__/Text.spec.tsx @@ -0,0 +1,64 @@ +import { vitest } from 'vitest'; +import type { MockInstance } from 'vitest'; +import { screen, act, fireEvent } from '@testing-library/react'; +import { Text } from '../index'; +import { render } from '@/setupTest'; +import fr_FR from '../translations/Messages_fr_FR.json'; +import { useAuthorizationIam } from '../../../hooks/iam'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +const mockedHook = useAuthorizationIam as unknown as MockInstance; + +describe('Text tests', () => { + afterEach(() => { + vitest.resetAllMocks(); + }); + + describe('should display manager text', () => { + it('with true value for useAuthorizationIam', () => { + mockedHook.mockReturnValue({ + isAuthorized: true, + isLoading: true, + isFetched: true, + }); + render( + +
foo-manager-text
+
, + ); + expect(screen.getAllByText('foo-manager-text')).not.toBeNull(); + }); + }); + describe('should display error manager text', () => { + it('with false value for useAuthorizationIam', () => { + mockedHook.mockReturnValue({ + isAuthorized: false, + isLoading: true, + isFetched: true, + }); + render( + +
foo-manager-text
+
, + ); + expect(screen.findByText(fr_FR.iam_hidden_text)).not.toBeNull(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/text/index.ts b/packages/manager-ui-kit/src/components/text/index.ts new file mode 100644 index 000000000000..5903a7358663 --- /dev/null +++ b/packages/manager-ui-kit/src/components/text/index.ts @@ -0,0 +1,2 @@ +export { Text } from './Text.component'; +export type { TextProps } from './Text.props'; diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_de_DE.json b/packages/manager-ui-kit/src/components/text/translations/Messages_de_DE.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_de_DE.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_de_DE.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_en_GB.json b/packages/manager-ui-kit/src/components/text/translations/Messages_en_GB.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_en_GB.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_en_GB.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_es_ES.json b/packages/manager-ui-kit/src/components/text/translations/Messages_es_ES.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_es_ES.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_es_ES.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_fr_CA.json b/packages/manager-ui-kit/src/components/text/translations/Messages_fr_CA.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_fr_CA.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_fr_CA.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_fr_FR.json b/packages/manager-ui-kit/src/components/text/translations/Messages_fr_FR.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_fr_FR.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_fr_FR.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_it_IT.json b/packages/manager-ui-kit/src/components/text/translations/Messages_it_IT.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_it_IT.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_it_IT.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_pl_PL.json b/packages/manager-ui-kit/src/components/text/translations/Messages_pl_PL.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_pl_PL.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_pl_PL.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/Messages_pt_PT.json b/packages/manager-ui-kit/src/components/text/translations/Messages_pt_PT.json similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/Messages_pt_PT.json rename to packages/manager-ui-kit/src/components/text/translations/Messages_pt_PT.json diff --git a/packages/manager-react-components/src/components/ManagerText/translations/index.ts b/packages/manager-ui-kit/src/components/text/translations/index.ts similarity index 100% rename from packages/manager-react-components/src/components/ManagerText/translations/index.ts rename to packages/manager-ui-kit/src/components/text/translations/index.ts diff --git a/packages/manager-ui-kit/src/components/tile/__tests__/Tile.test.tsx b/packages/manager-ui-kit/src/components/tile/__tests__/Tile.test.tsx new file mode 100644 index 000000000000..6259705f99b1 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/__tests__/Tile.test.tsx @@ -0,0 +1,106 @@ +import { describe, it, expect, vitest } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { Link } from '@ovhcloud/ods-react'; +import { Tile } from '../index'; +import { ActionMenu } from '../../action-menu'; + +vitest.mock('../../../hooks/iam', () => ({ + useAuthorizationIam: vitest.fn().mockReturnValue({ + isAuthorized: true, + isLoading: false, + isFetched: true, + }), +})); + +describe('Tile Snapshot tests', () => { + it('renders simple tile', () => { + const { container } = render( + + + + + + + + + + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders tile term with Tooltip', () => { + const { baseElement, container } = render( + + + + + + , + ); + + const tooltipElement = container.querySelector( + 'span[data-scope="tooltip"]', + ); + userEvent.hover(tooltipElement); + expect(baseElement).toMatchSnapshot(); + }); + + it('renders tile term with Actions Menu', () => { + const actionMenu = ( + + ); + const { container } = render( + + + + + + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders multiple dd elements', () => { + const { container } = render( + + + + + + Link + + + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/__tests__/__snapshots__/Tile.test.tsx.snap b/packages/manager-ui-kit/src/components/tile/__tests__/__snapshots__/Tile.test.tsx.snap new file mode 100644 index 000000000000..0fb5b0103e7b --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/__tests__/__snapshots__/Tile.test.tsx.snap @@ -0,0 +1,298 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Tile Snapshot tests > renders multiple dd elements 1`] = ` +
+
+
+

+ Simple Tile +

+
+
+
+
+
+ + Sample Term + +
+
+
+ + Sample Description + +
+
+ + Link + +
+
+
+
+
+
+
+`; + +exports[`Tile Snapshot tests > renders simple tile 1`] = ` +
+
+
+

+ Simple Tile +

+
+
+
+
+
+ + Sample Term + +
+
+
+ + Sample Description + +
+
+
+
+
+
+ + Sample Term + +
+
+
+ + Sample Description + +
+
+
+
+
+
+`; + +exports[`Tile Snapshot tests > renders tile term with Actions Menu 1`] = ` +
+
+
+

+ Simple Tile +

+
+
+
+
+
+ + Sample Term + +
+ +
+
+ + Sample Description + +
+
+
+
+
+
+`; + +exports[`Tile Snapshot tests > renders tile term with Tooltip 1`] = ` + +
+
+
+

+ Simple Tile +

+
+
+
+
+
+ + Sample Term + + +
+
+
+ + Sample Description + +
+
+
+
+
+
+
+ +
+ +`; diff --git a/packages/manager-ui-kit/src/components/tile/index.ts b/packages/manager-ui-kit/src/components/tile/index.ts new file mode 100644 index 000000000000..cf21ff6fcaac --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/index.ts @@ -0,0 +1 @@ +export * as Tile from './namespace'; diff --git a/packages/manager-ui-kit/src/components/tile/namespace.ts b/packages/manager-ui-kit/src/components/tile/namespace.ts new file mode 100644 index 000000000000..b66c03bd8efd --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/namespace.ts @@ -0,0 +1,3 @@ +export { TileRoot as Root } from './tile-root'; + +export * from './tile-item'; diff --git a/packages/manager-ui-kit/src/components/tile/tile-divider/TileDivider.component.tsx b/packages/manager-ui-kit/src/components/tile/tile-divider/TileDivider.component.tsx new file mode 100644 index 000000000000..6931ba6137e4 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-divider/TileDivider.component.tsx @@ -0,0 +1,5 @@ +import { Divider, DIVIDER_SPACING, DividerProp } from '@ovhcloud/ods-react'; + +export const TileDivider = (props: DividerProp) => ( + +); diff --git a/packages/manager-ui-kit/src/components/tile/tile-divider/__tests__/TileDivider.spec.tsx b/packages/manager-ui-kit/src/components/tile/tile-divider/__tests__/TileDivider.spec.tsx new file mode 100644 index 000000000000..48df0290a38e --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-divider/__tests__/TileDivider.spec.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TileDivider } from '../TileDivider.component'; + +const renderDivider = ({ ...props }) => { + render(); +}; + +describe('TileDivider', () => { + it('should render the Divider component', () => { + renderDivider({}); + const divider = screen.getByRole('separator'); + expect(divider).toBeInTheDocument(); + }); + + it('should forward additional HTML attributes correctly', () => { + renderDivider({ + id: 'custom-divider', + className: 'extra-class', + }); + + const divider = screen.getByTestId('tile-divider'); + expect(divider).toHaveAttribute('id', 'custom-divider'); + expect(divider).toHaveClass('extra-class'); + }); + + it('should render without errors when no props are passed', () => { + expect(() => render()).not.toThrow(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/index.ts b/packages/manager-ui-kit/src/components/tile/tile-item/index.ts new file mode 100644 index 000000000000..708816794dcc --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/index.ts @@ -0,0 +1 @@ +export * as Item from './namespace'; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/namespace.ts b/packages/manager-ui-kit/src/components/tile/tile-item/namespace.ts new file mode 100644 index 000000000000..3a8997688b20 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/namespace.ts @@ -0,0 +1,5 @@ +export { TileItemRoot as Root } from './tile-item-root/TileItemRoot.component'; + +export { TileItemTerm as Term } from './tile-item-term/TileItemTerm.component'; + +export { TileItemDescription as Description } from './tile-item-description/TileItemDescription.component'; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.component.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.component.tsx new file mode 100644 index 000000000000..c70a0f10f64c --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.component.tsx @@ -0,0 +1,18 @@ +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { TileDivider } from '../../tile-divider/TileDivider.component'; +import { TileItemDescriptionProps } from './TileItemDescription.props'; + +export const TileItemDescription = ({ + label, + children, + divider = true, + ...rest +}: TileItemDescriptionProps) => { + return ( +
+ {label && {label}} + {children} + {divider && } +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.props.ts b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.props.ts new file mode 100644 index 000000000000..77058c5a8c30 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/TileItemDescription.props.ts @@ -0,0 +1,9 @@ +import { ComponentProps } from 'react'; + +export type TileItemDescriptionProps = Omit< + ComponentProps<'dd'>, + 'className' +> & { + label?: string; + divider?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/__tests__/TileItemDescription.spec.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/__tests__/TileItemDescription.spec.tsx new file mode 100644 index 000000000000..9058f138759c --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-description/__tests__/TileItemDescription.spec.tsx @@ -0,0 +1,70 @@ +import { describe, it, expect, vitest } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TileItemDescription } from '../TileItemDescription.component'; + +// Since HR element does not specific text or role, it's hard to get this element. +// So mocking the ODS divider with custom testid to easily get the element +vitest.mock('../../../tile-divider/TileDivider.component', () => ({ + TileDivider: () =>
, +})); + +describe('TileItemDescription', () => { + it('should render inside a
element with preset margin', () => { + render(); + + const dd = screen.getByRole('definition'); + expect(dd).toBeInTheDocument(); + expect(dd.tagName).toBe('DD'); + expect(dd).toHaveClass('m-0'); + }); + + it('should render the description inside a Text component with preset=span', () => { + render(); + + const textElement = screen.getByText('This is a network setting.'); + expect(textElement).toBeInTheDocument(); + expect(textElement.tagName).toBe('SPAN'); + }); + + it('should not render description when not provided and renders divider by default', () => { + render(); + const dd = screen.getByRole('definition'); + expect(dd.getElementsByTagName('span').length).toBe(0); + expect(dd.children.length).toBeLessThanOrEqual(1); + expect(dd.children[0].tagName).toBe('HR'); + }); + + it('should render children if provided', () => { + render( + + /var/www + , + ); + + const codeElement = screen.getByText('/var/www'); + expect(codeElement.tagName).toBe('CODE'); + expect(screen.getByRole('definition')).toContainElement(codeElement); + }); + + it('should conditionally show/hide divider based on prop', () => { + const { rerender } = render( + , + ); + + expect(screen.getByTestId('tile-divider')).toBeInTheDocument(); + + rerender(); + expect(screen.queryByTestId('tile-divider')).not.toBeInTheDocument(); + }); + + it('should render description and children together', () => { + render( + + Server A + , + ); + + expect(screen.getByText('Hosted on')).toBeInTheDocument(); + expect(screen.getByText('Server A')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/TileItemRoot.component.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/TileItemRoot.component.tsx new file mode 100644 index 000000000000..17609899c0ed --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/TileItemRoot.component.tsx @@ -0,0 +1,12 @@ +import { ComponentProps } from 'react'; + +export const TileItemRoot = ({ + children, + ...rest +}: Omit, 'className'>) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/__tests__/TileItemRoot.spec.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/__tests__/TileItemRoot.spec.tsx new file mode 100644 index 000000000000..92b10f4b2028 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-root/__tests__/TileItemRoot.spec.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TileItemRoot } from '../TileItemRoot.component'; + +describe('TileItemRoot', () => { + it('should render without children', () => { + render(); + const container = screen.getByTestId('tile-root-item'); + expect(container).toBeInTheDocument(); + expect(container.tagName).toBe('DIV'); + expect(container).toHaveClass('flex', 'flex-col', 'gap-1'); + }); + + it('should render with multiple children', () => { + render( + +
Label
+
Value
+ , + ); + + expect(screen.getByText('Label')).toBeInTheDocument(); + expect(screen.getByText('Value')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.component.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.component.tsx new file mode 100644 index 000000000000..f5b3dd9b293c --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.component.tsx @@ -0,0 +1,37 @@ +import { + Text, + TEXT_PRESET, + Tooltip, + TooltipTrigger, + TooltipContent, + Icon, +} from '@ovhcloud/ods-react'; +import { TileItemTermProps } from './TileItemTerm.props'; + +export const TileItemTerm = ({ + label, + tooltip, + actions, + ...rest +}: TileItemTermProps) => { + return ( +
+
+ + {label} + + {tooltip && ( + + + + + + {tooltip} + + + )} +
+ {actions && actions} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.props.ts b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.props.ts new file mode 100644 index 000000000000..e36a556ce678 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/TileItemTerm.props.ts @@ -0,0 +1,7 @@ +import { JSX, ComponentProps } from 'react'; + +export type TileItemTermProps = Omit, 'className'> & { + label: string; + tooltip?: string; + actions?: JSX.Element; +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/__tests__/TileItemTerm.spec.tsx b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/__tests__/TileItemTerm.spec.tsx new file mode 100644 index 000000000000..9b887767f32d --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-item/tile-item-term/__tests__/TileItemTerm.spec.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { TileItemTerm } from '../TileItemTerm.component'; + +describe('TileItemTerm', () => { + it('should render the term text inside a Text component with label preset', () => { + render(); + + const textElement = screen.getByText('Domain Name'); + expect(textElement).toBeInTheDocument(); + + expect(textElement.tagName).toBe('SPAN'); + expect(textElement).toHaveClass('font-bold'); + }); + + it('should render inside a
element', () => { + render(); + + const dt = screen.getByTestId('tile-item-term'); + expect(dt.tagName).toBe('DT'); + expect(dt).toHaveClass('flex', 'justify-between'); + }); + + it('should not render actions when not provided', () => { + render(); + + const dt = screen.getByTestId('tile-item-term'); + expect(dt.children.length).toBe(1); + }); + + it('should render actions when provided (e.g. button or link)', () => { + render( + Edit} + />, + ); + + const actionButton = screen.getByTestId('action-button'); + expect(actionButton).toBeInTheDocument(); + expect(actionButton).toHaveTextContent('Edit'); + + const dt = screen.getByTestId('tile-item-term'); + expect(dt).toContainElement(actionButton); + }); + + it('should render tooltip when provided', () => { + const tooltipMessage = 'Tooltip for IP address'; + const { container } = render( + Edit} + />, + ); + const tooltipElement = container.querySelector( + 'span[data-scope="tooltip"]', + ); + expect(tooltipElement).toBeInTheDocument(); + + userEvent.hover(tooltipElement); + expect(screen.getByText(tooltipMessage)).toBeInTheDocument(); + }); + + it('should render tooltip when not provided', () => { + const { container } = render( + Edit} + />, + ); + expect( + container.querySelector('span[data-scope="tooltip"]'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.component.tsx b/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.component.tsx new file mode 100644 index 000000000000..55da25fef991 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Card, CARD_COLOR, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { TileRootProps } from './TileRoot.props'; +import { TileDivider } from '../tile-divider/TileDivider.component'; + +export const TileRoot = ({ + className, + title, + color = CARD_COLOR.neutral, + children, + ...props +}: TileRootProps) => { + return ( + +
+ {title} + +
{children}
+
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.props.ts b/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.props.ts new file mode 100644 index 000000000000..c0995c62ba16 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-root/TileRoot.props.ts @@ -0,0 +1,8 @@ +import { ComponentProps } from 'react'; +import { Card, CARD_COLOR } from '@ovhcloud/ods-react'; + +export interface TileRootProps extends ComponentProps { + className?: string; + title: string; + color?: CARD_COLOR; +} diff --git a/packages/manager-ui-kit/src/components/tile/tile-root/__tests__/TileRoot.spec.tsx b/packages/manager-ui-kit/src/components/tile/tile-root/__tests__/TileRoot.spec.tsx new file mode 100644 index 000000000000..f1c93ff15230 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-root/__tests__/TileRoot.spec.tsx @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { CARD_COLOR } from '@ovhcloud/ods-react'; +import { TileRoot } from '../TileRoot.component'; + +describe('TileRoot', () => { + const defaultProps = { + title: 'Test Title', + children: ( + <> +
Label
+
Value
+ + ), + }; + + it('should render the component with title and children', () => { + render(); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Label')).toBeInTheDocument(); + expect(screen.getByText('Value')).toBeInTheDocument(); + }); + + it('should render the Card with correct default color (neutral)', () => { + render(); + const card = screen.getByTestId('card'); + expect(card.className).toContain(`${CARD_COLOR.neutral}`); + }); + + it('should allow custom color to be passed', () => { + render( + , + ); + const card = screen.getByTestId('card'); + expect(card.className).toContain(`${CARD_COLOR.information}`); + }); + + it('should render the TileDivider inside the section', () => { + render(); + const separator = screen.getByRole('separator'); + expect(separator).toBeInTheDocument(); + }); + + it('should apply default className and merge custom className', () => { + render( + , + ); + + const card = screen.getByTestId('card'); + + expect(card).toHaveClass('w-full'); + expect(card).toHaveClass('flex-col'); + expect(card).toHaveClass('p-[1rem]'); + expect(card).toHaveClass('custom-class'); + expect(card).toHaveClass('extra-padding'); + }); + + it('should render title using Text component with heading4 preset', () => { + render(); + const titleElement = screen.getByText('Test Title'); + + expect(titleElement.tagName).toBe('H4'); + }); + + it('should structure content inside
with flex column layout', () => { + render(); + + const section = screen.getByTestId('card').firstChild; + expect(section).toHaveClass('flex', 'flex-col', 'w-full'); + }); + + it('should wrap children in
with preset styles (m-0, flex-col)', () => { + render(); + const dlList = screen.getByTestId('card').getElementsByTagName('dl'); + expect(dlList.length).toBe(1); + expect(dlList[0]).toHaveClass('flex', 'flex-col', 'm-0'); + }); +}); diff --git a/packages/manager-ui-kit/src/components/tile/tile-root/index.ts b/packages/manager-ui-kit/src/components/tile/tile-root/index.ts new file mode 100644 index 000000000000..cc8d212549a7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tile/tile-root/index.ts @@ -0,0 +1,3 @@ +export { TileRoot } from './TileRoot.component'; + +export type { TileRootProps } from './TileRoot.props'; diff --git a/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.component.tsx b/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.component.tsx new file mode 100644 index 000000000000..ac0471cf3cd7 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.component.tsx @@ -0,0 +1,105 @@ +import { useMemo, useState, useCallback } from 'react'; +import isEqual from 'lodash.isequal'; +import { TabsComponent } from '../tabs'; +import { + TilesInputGroupProps, + TilesInputGroupState, +} from './TilesInputGroup.props'; + +import { TilesInputComponent } from '../tiles-input'; + +function TilesInputGroupComponent({ + id, + items, + value, + onInput, + label, + tileClass, + stack, + group, +}: TilesInputGroupProps): JSX.Element { + const [state, seTilesInputGroupState] = useState>({ + selectedGroup: group?.value, + selectedStack: stack?.value, + }); + + const groups = useMemo(() => { + const newGroups = new Map(); + if (group && typeof group.by === 'function') { + if (group.showAllTab) { + newGroups.set('ALL' as G, [...items]); + } + + items.forEach((item) => { + const groupId = group.by(item); + if (!newGroups.has(groupId)) { + newGroups.set(groupId, []); + } + const groupItems = newGroups.get(groupId); + if (groupItems) { + groupItems.push(item); + } + }); + } + + return newGroups; + }, [items, group]); + + const handleGroupChange = useCallback( + (g: G) => { + if (!isEqual(state.selectedGroup, g)) { + seTilesInputGroupState((prev) => ({ ...prev, selectedGroup: g })); + if (group?.onChange) { + group.onChange(g); + } + } + }, + [state.selectedGroup, group?.onChange], + ); + + return ( + <> + {group ? ( + + items={[...groups?.keys()]} + titleElement={({ item }: { item: G }) => ( + <>{group.label(item, groups.get(item) || [])} + )} + contentElement={({ item }: { item: G }) => ( + { + if (stack?.onChange) stack?.onChange(s); + }, + } + : undefined + } + /> + )} + onChange={handleGroupChange} + /> + ) : ( + + id={id} + items={items} + value={value} + onInput={onInput} + label={label} + tileClass={tileClass} + stack={stack} + /> + )} + + ); +} + +export { TilesInputGroupComponent }; diff --git a/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.props.ts b/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.props.ts new file mode 100644 index 000000000000..8f355ab0f3b9 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input-group/TilesInputGroup.props.ts @@ -0,0 +1,19 @@ +import { TilesInputProps } from '../tiles-input/TilesInput.props'; + +export type TilesInputGroupProps = TilesInputProps< + T, + S +> & { + group?: { + by: (item: T) => G; + label: (group: G, items: T[]) => JSX.Element | string; + value?: G; + showAllTab: boolean; + onChange?: (group: G) => void; + }; +}; + +export type TilesInputGroupState = { + selectedGroup: G | undefined; + selectedStack: S | undefined; +}; diff --git a/packages/manager-ui-kit/src/components/tiles-input-group/index.ts b/packages/manager-ui-kit/src/components/tiles-input-group/index.ts new file mode 100644 index 000000000000..a24f83e11824 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input-group/index.ts @@ -0,0 +1,5 @@ +export { TilesInputGroupComponent } from './TilesInputGroup.component'; +export type { + TilesInputGroupProps, + TilesInputGroupState, +} from './TilesInputGroup.props'; diff --git a/packages/manager-ui-kit/src/components/tiles-input/TilesInput.component.tsx b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.component.tsx new file mode 100644 index 000000000000..2003c1f49b60 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.component.tsx @@ -0,0 +1,169 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Card } from '@ovhcloud/ods-react'; +import { clsx } from 'clsx'; +import isEqual from 'lodash.isequal'; +import { hashCode } from '../../utils'; +import { TilesInputProps, TilesInputState } from './TilesInput.props'; +import { stackItems } from './TilesInput.utils'; + +export const TilesInputComponent = ({ + items, + value, + onInput, + label, + tileClass, + stack, + id, +}: TilesInputProps): JSX.Element => { + const [state, setState] = useState>({ + stacks: stackItems(items, stack?.by), + selectedStack: stack?.value, + activeClass: `cursor-pointer font-bold bg-[--ods-color-blue-100] border-[--ods-color-blue-600] ${tileClass?.active}`, + inactiveClass: `cursor-pointer border-[--ods-color-blue-100] hover:bg-[--ods-color-blue-100] hover:border-[--ods-color-blue-600] ${tileClass?.inactive}`, + }); + + const set = { + selectedStack: (s: S) => { + setState((prev) => ({ ...prev, selectedStack: s })); + }, + value: (t: T) => onInput(t), + }; + + const is = { + stack: { + checked: useCallback( + (s: S | undefined) => { + if (s === undefined) return false; + const stackItem = state.stacks?.get(s); + return stackItem && stackItem.length > 1 + ? isEqual(state.selectedStack, s) + : stackItem && + stackItem.length === 1 && + isEqual(stackItem[0], value); + }, + [state.stacks, state.selectedStack, value], + ), + singleton: useCallback( + (s: S | undefined) => { + if (s === undefined) return false; + const stackItem = state.stacks.get(s); + return stackItem?.length === 1; + }, + [state.stacks], + ), + }, + }; + + // Update stacks from props + useEffect(() => { + setState((prev) => ({ ...prev, stacks: stackItems(items, stack?.by) })); + }, [items, stack]); + + // Update active/inactive class from props + useEffect(() => { + if (tileClass) { + setState((prev) => ({ + ...prev, + activeClass: `cursor-pointer font-bold bg-[--ods-color-blue-100] border-[--ods-color-blue-600] ${tileClass?.active}`, + inactiveClass: `cursor-pointer border-[--ods-color-blue-100] hover:bg-[--ods-color-blue-100] hover:border-[--ods-color-blue-600] ${tileClass?.inactive}`, + })); + } + }, [tileClass]); + + // Warn parent on stack change + useEffect(() => { + if ( + typeof stack?.onChange === 'function' && + state.selectedStack !== undefined + ) { + stack.onChange(state.selectedStack); + } + }, [state.selectedStack, stack?.onChange]); + + // Update selected stack from value + useEffect(() => { + if (stack && value) { + set.selectedStack(stack.by(value)); + } + }, [value, stack]); + + // Update value from selected stack + useEffect(() => { + if (stack && state.selectedStack !== undefined && value) { + const stackItem = state.stacks.get(state.selectedStack); + if (stackItem?.length && !isEqual(state.selectedStack, stack.by(value))) { + set.value(stackItem[0] as T); + } + } + }, [state.selectedStack, state.stacks, stack, value]); + + return ( +
+
    + {stack + ? [...state.stacks.keys()].map((key) => { + const stackItem = state.stacks.get(key); + if (!stackItem) return null; + + return ( +
  • + + is.stack.singleton(key) + ? set.value(stackItem[0] as T) + : key !== undefined && set.selectedStack(key) + } + className={`${clsx( + is.stack.checked(key) + ? state.activeClass + : state.inactiveClass, + )} w-full px-[24px] py-[16px]`} + > + {is.stack.singleton(key) + ? label(stackItem[0] as T) + : key !== undefined && stack?.label(key, stackItem)} + +
  • + ); + }) + : items.map((item: T) => ( +
  • + set.value(item)} + className={`${clsx( + isEqual(value, item) + ? state.activeClass + : state.inactiveClass, + )} w-full px-[24px] py-[16px]`} + > + {label(item)} + +
  • + ))} +
+ {state.selectedStack !== undefined && + (() => { + const selectedStackItems = state.stacks.get(state.selectedStack); + return selectedStackItems && selectedStackItems.length > 1; + })() && ( + <> +
+ + {stack?.title( + state.selectedStack, + state.stacks.get(state.selectedStack) || [], + )} + +
+ + + )} +
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/tiles-input/TilesInput.props.ts b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.props.ts new file mode 100644 index 000000000000..503631ff821b --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.props.ts @@ -0,0 +1,25 @@ +export type TilesInputProps = { + id?: (() => string) | string; + items: T[]; + value: T | null; + onInput: (value: T) => void; + label: (item: T) => JSX.Element | string; + tileClass?: { + active?: string; + inactive?: string; + }; + stack?: { + by: (item: T) => S; + label: (stack: S, items: T[]) => JSX.Element | string; + title: (stack: S, items: T[]) => JSX.Element | string; + value?: S; + onChange?: (stack: S) => void; + }; +}; + +export type TilesInputState = { + stacks: Map; + selectedStack: S | undefined; + activeClass: string; + inactiveClass: string; +}; diff --git a/packages/manager-ui-kit/src/components/tiles-input/TilesInput.utils.ts b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.utils.ts new file mode 100644 index 000000000000..b411da5ca022 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input/TilesInput.utils.ts @@ -0,0 +1,44 @@ +import isEqual from 'lodash.isequal'; + +function getItemsByKey(items: I[], cb: (item: I) => U): I[] { + if (!items) { + return []; + } + + return [ + ...items + .reduce((map: Map, item: I) => { + if (!map.has(cb(item))) map.set(cb(item), item); + return map; + }, new Map()) + .values(), + ]; +} + +function stackItems( + items: I[], + cb?: (item: I) => U, +): Map { + const stacks = new Map(); + + if (cb) { + const uniques = getItemsByKey(items, cb); + uniques.forEach((unique) => { + const key = cb(unique); + stacks.set(key, []); + const stackItem = stacks.get(key); + if (stackItem && items) { + const filteredItems = items.filter( + (item) => item && isEqual(key, cb(item)), + ); + stackItem.push(...filteredItems); + } + }); + } else { + stacks.set(undefined, items || []); + } + + return stacks; +} + +export { stackItems }; diff --git a/packages/manager-ui-kit/src/components/tiles-input/index.ts b/packages/manager-ui-kit/src/components/tiles-input/index.ts new file mode 100644 index 000000000000..92dbc10f3604 --- /dev/null +++ b/packages/manager-ui-kit/src/components/tiles-input/index.ts @@ -0,0 +1,2 @@ +export { TilesInputComponent } from './TilesInput.component'; +export type { TilesInputProps, TilesInputState } from './TilesInput.props'; diff --git a/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.component.tsx b/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.component.tsx new file mode 100644 index 000000000000..a4346a0100ca --- /dev/null +++ b/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.component.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Message, + MessageBody, + MessageIcon, + TEXT_PRESET, + MESSAGE_COLOR, + ICON_NAME, + Input, + INPUT_TYPE, + FormField, + FormFieldLabel, +} from '@ovhcloud/ods-react'; +import { Modal } from '../modal'; +import { Text } from '../text'; +import './translations/translations'; +import { UpdateNameModalProps } from './UpdateNameModal.props'; + +export const UpdateNameModal: React.FC = ({ + headline, + description, + inputLabel, + defaultValue, + isLoading, + onClose, + updateDisplayName, + error, + cancelButtonLabel, + confirmButtonLabel, + pattern, + patternMessage, + isOpen = true, +}) => { + const { t } = useTranslation('update-name-modal'); + const [displayName, setDisplayName] = useState(defaultValue); + const [isPatternError, setIsPatternError] = useState(false); + + useEffect(() => { + setDisplayName(defaultValue); + }, [defaultValue]); + + useEffect(() => { + const regex = new RegExp(pattern || ''); + setIsPatternError(!displayName?.match(regex)); + }, [displayName, pattern]); + + const handleClose = () => { + if (onClose) { + onClose(); + } + }; + + return ( + updateDisplayName(displayName || ''), + }} + secondaryButton={{ + label: cancelButtonLabel || t('updateModalCancelButton'), + onClick: handleClose, + }} + > +
+ {!!error && ( + + + {t('updateModalError', { error })} + + )} + {description && ( + + {description} + + )} + + + {inputLabel} + + setDisplayName(e.target.value)} + aria-describedby={ + patternMessage ? 'update-name-modal-pattern-message' : undefined + } + /> + {patternMessage && ( + + {patternMessage} + + )} + +
+
+ ); +}; diff --git a/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.props.ts b/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.props.ts new file mode 100644 index 000000000000..56873c799924 --- /dev/null +++ b/packages/manager-ui-kit/src/components/update-name-modal/UpdateNameModal.props.ts @@ -0,0 +1,15 @@ +export type UpdateNameModalProps = { + headline: string; + description?: string; + inputLabel: string; + defaultValue?: string; + onClose?: () => void; + updateDisplayName: (newDisplayName: string) => void; + isLoading?: boolean; + error?: string; + cancelButtonLabel?: string; + confirmButtonLabel?: string; + pattern?: string; + patternMessage?: string; + isOpen?: boolean; +}; diff --git a/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.snapshot.test.tsx b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.snapshot.test.tsx new file mode 100644 index 000000000000..66e2af00f70b --- /dev/null +++ b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.snapshot.test.tsx @@ -0,0 +1,324 @@ +import { vitest } from 'vitest'; +import { render, cleanup } from '@/setupTest'; +import { UpdateNameModal } from '../UpdateNameModal.component'; + +const mockUpdateDisplayName = vitest.fn(); +const mockOnClose = vitest.fn(); + +describe('UpdateNameModal Snapshot Tests', () => { + afterEach(async () => { + cleanup(); + vitest.resetAllMocks(); + // Remove scroll lock attribute that modal adds to body + document.body.removeAttribute('data-scroll-lock'); + // Wait for any pending async operations to complete + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + describe('Basic Modal States', () => { + it('should render basic modal with required props', () => { + const { container } = render( + , + ); + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with all optional props', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal when closed', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Loading States', () => { + it('should render modal in loading state', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with loading state and default value', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Error States', () => { + it('should render modal with error message', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with error and description', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Pattern Validation States', () => { + it('should render modal with pattern validation', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with pattern validation and default value', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with complex pattern validation', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Custom Button Labels', () => { + it('should render modal with custom button labels', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with custom confirm button only', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with custom cancel button only', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Edge Cases', () => { + it('should render modal with empty default value', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with null default value', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal without onClose handler', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with long text content', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with special characters in text', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); + + describe('Combined States', () => { + it('should render modal with loading, error, and pattern validation', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + + it('should render modal with all features enabled', () => { + const { container } = render( + , + ); + + expect(container.parentElement).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.spec.tsx b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.spec.tsx new file mode 100644 index 000000000000..84005be0ed79 --- /dev/null +++ b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/UpdateNameModal.spec.tsx @@ -0,0 +1,122 @@ +import { vitest, describe, it, expect, beforeEach } from 'vitest'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { render } from '@/setupTest'; +import { UpdateNameModal } from '../UpdateNameModal.component'; +import { UpdateNameModalProps } from '../UpdateNameModal.props'; + +const mockUpdateDisplayName = vitest.fn(); +const mockOnClose = vitest.fn(); + +const defaultProps: UpdateNameModalProps = { + headline: 'Update Name', + inputLabel: 'Display Name', + updateDisplayName: mockUpdateDisplayName, +}; + +const renderComponent = (props: Partial = {}) => { + return render(); +}; + +describe('UpdateNameModal', () => { + beforeEach(() => { + vitest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render with required props', () => { + renderComponent(); + + expect(screen.getByText('Update Name')).toBeInTheDocument(); + expect(screen.getByText('Display Name')).toBeInTheDocument(); + expect(screen.getByText('Annuler')).toBeInTheDocument(); + expect(screen.getByText('Confirmer')).toBeInTheDocument(); + }); + + it('should render with all optional props', () => { + renderComponent({ + description: 'Please enter a new name', + defaultValue: 'John Doe', + cancelButtonLabel: 'Cancel', + confirmButtonLabel: 'Update', + pattern: '^[a-zA-Z\\s]+$', + patternMessage: 'Only letters and spaces allowed', + }); + + expect(screen.getByText('Please enter a new name')).toBeInTheDocument(); + expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Update')).toBeInTheDocument(); + expect( + screen.getByText('Only letters and spaces allowed'), + ).toBeInTheDocument(); + }); + + it('should not render when isOpen is false', () => { + renderComponent({ isOpen: false }); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should render error message when error prop is provided', () => { + renderComponent({ error: 'Name already exists' }); + expect(screen.getByText(/Name already exists/)).toBeInTheDocument(); + }); + + it('should not render description when description prop is not provided', () => { + renderComponent(); + expect( + screen.queryByText('Please enter a new name'), + ).not.toBeInTheDocument(); + }); + + it('should not render pattern message when patternMessage prop is not provided', () => { + renderComponent(); + expect( + screen.queryByText('Only letters and spaces allowed'), + ).not.toBeInTheDocument(); + }); + }); + + describe('Integration Tests', () => { + it('should handle complete user workflow', async () => { + renderComponent({ + description: 'Please enter your new name', + defaultValue: 'Old Name', + pattern: '^[a-zA-Z\\s]+$', + patternMessage: 'Only letters and spaces allowed', + onClose: mockOnClose, + }); + + expect( + screen.getByText('Please enter your new name'), + ).toBeInTheDocument(); + expect(screen.getByDisplayValue('Old Name')).toBeInTheDocument(); + + const input = screen.getByLabelText('Display Name'); + fireEvent.change(input, { target: { value: 'New Name' } }); + + await waitFor(() => { + expect(input).not.toHaveAttribute('aria-invalid', 'true'); + }); + + const confirmButton = screen.getByText('Confirmer'); + fireEvent.click(confirmButton); + + expect(mockUpdateDisplayName).toHaveBeenCalledWith('New Name'); + }); + + it('should handle error state workflow', () => { + renderComponent({ + error: 'Name already exists', + onClose: mockOnClose, + }); + + expect(screen.getByText(/Name already exists/)).toBeInTheDocument(); + + const cancelButton = screen.getByText('Annuler'); + fireEvent.click(cancelButton); + + expect(mockOnClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/manager-ui-kit/src/components/update-name-modal/__tests__/__snapshots__/UpdateNameModal.snapshot.test.tsx.snap b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/__snapshots__/UpdateNameModal.snapshot.test.tsx.snap new file mode 100644 index 000000000000..e5b16018cc79 --- /dev/null +++ b/packages/manager-ui-kit/src/components/update-name-modal/__tests__/__snapshots__/UpdateNameModal.snapshot.test.tsx.snap @@ -0,0 +1,2512 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`UpdateNameModal Snapshot Tests > Basic Modal States > should render basic modal with required props 1`] = ` + +
+
+
+ +
+ +`; + +exports[`UpdateNameModal Snapshot Tests > Basic Modal States > should render modal when closed 1`] = ` + +
+