diff --git a/.eslintignore b/.eslintignore index 1e490c52e9d5..d5b2a13f623d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,3 +17,4 @@ packages/manager/core/generator packages/components/ovh-at-internet/src/ovh-at-internet.ts packages/manager/apps/container packages/manager/apps/pci-databases-analytics/src/components/ui +packages/manager/apps/pci-ai-notebooks/src/components/ui diff --git a/.prettierignore b/.prettierignore index 587d59146e0e..d278b1fb052e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,4 +6,6 @@ loader react vue packages/manager/apps/pci-databases-analytics/src/components/ui -packages/manager/apps/pci-databases-analytics/src/lib/utils.ts \ No newline at end of file +packages/manager/apps/pci-databases-analytics/src/lib/utils.ts +packages/manager/apps/pci-ai-notebooks/src/components/ui +packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts diff --git a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts index 9f9785a191df..9fffb392f1f4 100644 --- a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts +++ b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts @@ -50,6 +50,7 @@ export const features = [ 'ai-endpoints', 'key-management-service', 'pci-savings-plan', + 'pci-ai-notebooks' ]; export function getPciProjectMenu( @@ -354,6 +355,7 @@ export function getPciProjectMenu( 'training', 'ai-apps', 'ai-endpoints', + 'pci-ai-notebooks', ) ) { pciMenu.push({ @@ -373,6 +375,11 @@ export function getPciProjectMenu( title: 'AI Notebooks', href: getURL('public-cloud', `#/pci/projects/${projectId}/notebooks`), }, + isFeaturesAvailable('pci-ai-notebooks') && { + id: 'notebooks', + title: 'AI Notebooks React', + href: getURL('public-cloud', `#/pci/projects/${projectId}/ai/notebooks`), + }, isFeaturesAvailable('training') && { id: 'training', title: 'AI Training', diff --git a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts index 1543f86b98e6..510cd13296f8 100644 --- a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts +++ b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts @@ -496,7 +496,7 @@ pciNode.children = [ idAttr: 'pci-ai-link', universe: PUBLICCLOUD_UNIVERSE_ID, translation: 'sidebar_pci_ai', - features: ['notebooks', 'ai-apps', 'training', 'ai-dashboard'], + features: ['notebooks', 'ai-apps', 'training', 'ai-dashboard', 'pci-ai-notebooks'], forceVisibility: true, children: [ { @@ -525,6 +525,19 @@ pciNode.children = [ features: ['notebooks'], forceVisibility: true, }, + { + id: 'pci-ai-notebooks', + idAttr: 'pci-ai-notebooks-link', + universe: PUBLICCLOUD_UNIVERSE_ID, + translation: 'sidebar_pci_ai_notebooks', + serviceType: 'CLOUD_PROJECT_AI_NOTEBOOK', + routing: { + application: 'public-cloud', + hash: '#/pci/projects/{projectId}/ai/notebooks', + }, + features: ['pci-ai-notebooks'], + forceVisibility: true, + }, { id: 'pci-ai-training', idAttr: 'pci-ai-training-link', diff --git a/packages/manager/apps/pci-ai-notebooks/.gitignore b/packages/manager/apps/pci-ai-notebooks/.gitignore new file mode 100644 index 000000000000..32ef74fe6562 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/.gitignore @@ -0,0 +1,2 @@ +coverage +yarn-error.log \ No newline at end of file diff --git a/packages/manager/apps/pci-ai-notebooks/README.md b/packages/manager/apps/pci-ai-notebooks/README.md new file mode 100644 index 000000000000..1c5efd0baccd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/README.md @@ -0,0 +1,3 @@ +# @ovh-ux/manager-pci-ai-notebooks-app + +> pci-ai-notebooks project diff --git a/packages/manager/apps/pci-ai-notebooks/components.json b/packages/manager/apps/pci-ai-notebooks/components.json new file mode 100644 index 000000000000..5fa88201dfd4 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/packages/manager/apps/pci-ai-notebooks/index.html b/packages/manager/apps/pci-ai-notebooks/index.html new file mode 100644 index 000000000000..5a905c968d6e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/index.html @@ -0,0 +1,23 @@ + + + + + + + + OVHcloud + + + + + + +
+ + + diff --git a/packages/manager/apps/pci-ai-notebooks/package.json b/packages/manager/apps/pci-ai-notebooks/package.json new file mode 100644 index 000000000000..172706c50c41 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/package.json @@ -0,0 +1,100 @@ +{ + "name": "@ovh-ux/manager-pci-ai-notebooks-app", + "version": "1.0.0", + "private": true, + "description": "pci-ai-notebooks", + "repository": { + "type": "git", + "url": "git+https://github.com/ovh/manager.git", + "directory": "packages/manager/apps/pci-ai-notebooks" + }, + "license": "BSD-3-Clause", + "author": "OVH SAS", + "type": "module", + "scripts": { + "build": "tsc --project tsconfig.build.json && vite build", + "coverage": "vitest run --coverage", + "dev": "tsc && vite", + "start": "lerna exec --stream --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run build --if-present", + "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run dev --if-present", + "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run dev:watch --if-present", + "test": "vitest", + "test-ui": "vitest --ui --coverage.enabled=true" + }, + "dependencies": { + "@hookform/resolvers": "^3.3.4", + "@ovh-ux/manager-config": "^7.4.0", + "@ovh-ux/manager-core-api": "^0.8.0", + "@ovh-ux/manager-react-shell-client": "^0.7.2", + "@ovh-ux/request-tagger": "^0.3.0", + "@ovh-ux/shell": "^3.8.0", + "@ovhcloud/ods-common-core": "^17.1.0", + "@ovhcloud/ods-components": "^17.1.0", + "@ovhcloud/ods-theme-blue-jeans": "^17.1.0", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.51.21", + "@tanstack/react-table": "^8.20.1", + "chart.js": "^4.4.2", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.0.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "cmdk": "^0.2.1", + "date-fns": "^3.6.0", + "duration-fns": "^3.0.2", + "i18next": "^23.8.2", + "i18next-http-backend": "^2.4.2", + "lucide-react": "^0.334.0", + "next-themes": "^0.2.1", + "qs": "^6.11.2", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-day-picker": "^8.10.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.50.1", + "react-i18next": "^14.0.5", + "react-router": "^6.21.3", + "react-router-dom": "^6.3.0", + "sonner": "^1.4.0", + "tailwind-merge": "^2.2.1", + "tailwindcss": "^3.4.4", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@ovh-ux/manager-vite-config": "^0.8.0", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^13.2.1", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.6", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/ui": "^1.4.0", + "typescript": "^5.3.3", + "vite": "4.3.1", + "vitest": "^1.4.0" + }, + "regions": [ + "CA", + "EU" + ] +} diff --git a/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs b/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs new file mode 100644 index 000000000000..12a703d900da --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png b/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png new file mode 100644 index 000000000000..413028afad19 Binary files /dev/null and b/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png differ diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json new file mode 100644 index 000000000000..5928e33d7bbe --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "B", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "Byte", + "unitLong-B_other": "Byte", + "unitLong-KB_one": "Kilobyte", + "unitLong-KB_other": "Kilobyte", + "unitLong-MB_one": "Megabyte", + "unitLong-MB_other": "Megabyte", + "unitLong-GB_one": "Gigabyte", + "unitLong-GB_other": "Gigabyte", + "unitLong-TB_one": "Terabyte", + "unitLong-TB_other": "Terabyte", + "unitLong-PB_one": "Petabyte", + "unitLong-PB_other": "Petabyte" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json new file mode 100644 index 000000000000..ff5ca5c4de65 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "B", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "byte", + "unitLong-B_other": "bytes", + "unitLong-KB_one": "kilobyte", + "unitLong-KB_other": "kilobytes", + "unitLong-MB_one": "megabyte", + "unitLong-MB_other": "megabytes", + "unitLong-GB_one": "gigabyte", + "unitLong-GB_other": "gigabytes", + "unitLong-TB_one": "terabyte", + "unitLong-TB_other": "terabytes", + "unitLong-PB_one": "petabyte", + "unitLong-PB_other": "petabytes" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json new file mode 100644 index 000000000000..ff5ca5c4de65 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "B", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "byte", + "unitLong-B_other": "bytes", + "unitLong-KB_one": "kilobyte", + "unitLong-KB_other": "kilobytes", + "unitLong-MB_one": "megabyte", + "unitLong-MB_other": "megabytes", + "unitLong-GB_one": "gigabyte", + "unitLong-GB_other": "gigabytes", + "unitLong-TB_one": "terabyte", + "unitLong-TB_other": "terabytes", + "unitLong-PB_one": "petabyte", + "unitLong-PB_other": "petabytes" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json new file mode 100644 index 000000000000..21953a45b499 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "o", + "unitShort-KB": "Ko", + "unitShort-MB": "Mo", + "unitShort-GB": "Go", + "unitShort-TB": "To", + "unitShort-PB": "Po", + "unitLong-B_one": "octet", + "unitLong-B_other": "octets", + "unitLong-KB_one": "kilooctet", + "unitLong-KB_other": "kilooctets", + "unitLong-MB_one": "mégaoctet", + "unitLong-MB_other": "mégaoctets", + "unitLong-GB_one": "gigaoctet", + "unitLong-GB_other": "gigaoctets", + "unitLong-TB_one": "téraoctet", + "unitLong-TB_other": "téraoctets", + "unitLong-PB_one": "pétaoctet", + "unitLong-PB_other": "pétaoctets" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json new file mode 100644 index 000000000000..21953a45b499 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "o", + "unitShort-KB": "Ko", + "unitShort-MB": "Mo", + "unitShort-GB": "Go", + "unitShort-TB": "To", + "unitShort-PB": "Po", + "unitLong-B_one": "octet", + "unitLong-B_other": "octets", + "unitLong-KB_one": "kilooctet", + "unitLong-KB_other": "kilooctets", + "unitLong-MB_one": "mégaoctet", + "unitLong-MB_other": "mégaoctets", + "unitLong-GB_one": "gigaoctet", + "unitLong-GB_other": "gigaoctets", + "unitLong-TB_one": "téraoctet", + "unitLong-TB_other": "téraoctets", + "unitLong-PB_one": "pétaoctet", + "unitLong-PB_other": "pétaoctets" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json new file mode 100644 index 000000000000..375d7b8399d5 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "b", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "byte", + "unitLong-B_other": "byte", + "unitLong-KB_one": "kilobyte", + "unitLong-KB_other": "kilobyte", + "unitLong-MB_one": "megabyte", + "unitLong-MB_other": "megabyte", + "unitLong-GB_one": "gigabyte", + "unitLong-GB_other": "gigabyte", + "unitLong-TB_one": "terabyte", + "unitLong-TB_other": "terabyte", + "unitLong-PB_one": "petabyte", + "unitLong-PB_other": "petabyte" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json new file mode 100644 index 000000000000..1511808fc990 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "B", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "bajt", + "unitLong-B_other": "bajty", + "unitLong-KB_one": "kilobajt", + "unitLong-KB_other": "kilobajty", + "unitLong-MB_one": "megabajt", + "unitLong-MB_other": "megabajty", + "unitLong-GB_one": "gigabajt", + "unitLong-GB_other": "gigabajty", + "unitLong-TB_one": "terabajt", + "unitLong-TB_other": "terabajty", + "unitLong-PB_one": "petabajt", + "unitLong-PB_other": "petabajty" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json new file mode 100644 index 000000000000..ff5ca5c4de65 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json @@ -0,0 +1,20 @@ +{ + "unitShort-B": "B", + "unitShort-KB": "KB", + "unitShort-MB": "MB", + "unitShort-GB": "GB", + "unitShort-TB": "TB", + "unitShort-PB": "PB", + "unitLong-B_one": "byte", + "unitLong-B_other": "bytes", + "unitLong-KB_one": "kilobyte", + "unitLong-KB_other": "kilobytes", + "unitLong-MB_one": "megabyte", + "unitLong-MB_other": "megabytes", + "unitLong-GB_one": "gigabyte", + "unitLong-GB_other": "gigabytes", + "unitLong-TB_one": "terabyte", + "unitLong-TB_other": "terabytes", + "unitLong-PB_one": "petabyte", + "unitLong-PB_other": "petabytes" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json new file mode 100644 index 000000000000..8e10000b4500 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Hoppla!", + "manager_error_page_button_cancel": "Abbrechen", + "manager_error_page_detail_code": "Fehlercode: ", + "manager_error_page_action_reload_label": "Erneut versuchen", + "manager_error_page_action_home_label": "Zurück zur Startseite", + "manager_error_page_default": "Beim Laden der Seite ist ein Fehler aufgetreten." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json new file mode 100644 index 000000000000..b17691e2bc6d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Oops!", + "manager_error_page_button_cancel": "Cancel", + "manager_error_page_detail_code": "Error code: ", + "manager_error_page_action_reload_label": "Try again", + "manager_error_page_action_home_label": "Back to homepage", + "manager_error_page_default": "An error has occurred loading the page." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json new file mode 100644 index 000000000000..15fc5f79256d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "¡Vaya!", + "manager_error_page_button_cancel": "Cancelar", + "manager_error_page_detail_code": "Código de error: ", + "manager_error_page_action_reload_label": "Volver a intentarlo", + "manager_error_page_action_home_label": "Volver a la página de inicio", + "manager_error_page_default": "Se ha producido un error al cargar la página." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json new file mode 100644 index 000000000000..2c575c63588e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json @@ -0,0 +1,8 @@ +{ + "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/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json new file mode 100644 index 000000000000..2c575c63588e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json @@ -0,0 +1,8 @@ +{ + "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/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json new file mode 100644 index 000000000000..fa5055b8cec5 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Ops!", + "manager_error_page_button_cancel": "Annullare", + "manager_error_page_detail_code": "Codice di errore: ", + "manager_error_page_action_reload_label": "Riprova", + "manager_error_page_action_home_label": "Torna alla home page", + "manager_error_page_default": "Si è verificato un errore durante il caricamento della pagina." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json new file mode 100644 index 000000000000..eceb9bcca2ca --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Ojej...", + "manager_error_page_button_cancel": "Anuluj", + "manager_error_page_detail_code": "Kod błędu: ", + "manager_error_page_action_reload_label": "Spróbuj ponownie", + "manager_error_page_action_home_label": "Powrót do strony głównej", + "manager_error_page_default": "Wystąpił błąd podczas ładowania strony." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json new file mode 100644 index 000000000000..25fac9551c86 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Oops!", + "manager_error_page_button_cancel": "Anular", + "manager_error_page_detail_code": "Código de erro: ", + "manager_error_page_action_reload_label": "Tentar novamente", + "manager_error_page_action_home_label": "Voltar para a página inicial", + "manager_error_page_default": "Ocorreu um erro ao carregar a página." +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json new file mode 100644 index 000000000000..f8078fff32f7 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guides", + "searchBarPlaceholder": "Guide suchen", + "noResult": "Keine Guides gefunden", + "listHeading": "Guides" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json new file mode 100644 index 000000000000..9cff75aef0ed --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guides", + "searchBarPlaceholder": "Search for a guide", + "noResult": "No guides found", + "listHeading": "Guides" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json new file mode 100644 index 000000000000..9fc61d66b542 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guías", + "searchBarPlaceholder": "Buscar una guía", + "noResult": "No se ha encontrado ninguna guía.", + "listHeading": "Guías" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json new file mode 100644 index 000000000000..6801e5860037 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guides", + "searchBarPlaceholder": "Chercher un guide", + "noResult": "Aucun guide trouvé", + "listHeading": "Guides" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json new file mode 100644 index 000000000000..6801e5860037 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guides", + "searchBarPlaceholder": "Chercher un guide", + "noResult": "Aucun guide trouvé", + "listHeading": "Guides" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json new file mode 100644 index 000000000000..f9896a7300fb --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Guide", + "searchBarPlaceholder": "Cercare una guida", + "noResult": "Nessuna guida trovata", + "listHeading": "Guide" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json new file mode 100644 index 000000000000..8a262eb97567 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Przewodniki", + "searchBarPlaceholder": "Znajdź przewodnik", + "noResult": "Nie znaleziono żadnego przewodnika", + "listHeading": "Przewodniki" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json new file mode 100644 index 000000000000..ef8f9264f6ec --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json @@ -0,0 +1,6 @@ +{ + "buttonLabel": "Manuais", + "searchBarPlaceholder": "Procurar um manual", + "noResult": "Nenhum manual encontrado", + "listHeading": "Manuais" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json new file mode 100644 index 000000000000..633c03205bda --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json @@ -0,0 +1,3 @@ +{ + "title": "AI Notebook" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json new file mode 100644 index 000000000000..acbae21d513e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json @@ -0,0 +1,3 @@ +{ + "title": "Auth" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json new file mode 100644 index 000000000000..ea70d8bc588f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json @@ -0,0 +1,22 @@ +{ + "keyFieldLabel": "Clé", + "valueFieldLabel": "Valeur", + "existingKeyError": "Cette clé existe déjà", + "numberOfConfiguredLabels_zero": "{{count}} labels configurés", + "numberOfConfiguredLabels_one": "{{count}} label configuré sur {{max}}", + "numberOfConfiguredLabels_other": "{{count}} labels configurés sur {{max}}", + "duplicateKeyError": "Cette clé SSH est déjà configurée pour ce notebook", + "configuredKeyFieldLabel": "Clé SSH déjà paramétrée", + "sshKeyFieldPlaceholder": "Sélectionner une clé SSH", + "sshKeyFieldLabel": "Nom de la clé SSH", + "sskKeyValueFieldLabel": "Clé publique SSH", + "numberOfConfiguredKeys_zero": "{{count}} clés SSH configurées", + "numberOfConfiguredKeys_one": "{{count}} clé SSH configurée sur {{max}}", + "numberOfConfiguredKeys_other": "{{count}} clés SSH configurées sur {{max}}", + "addSshKeyToastErrorTitle": "Une erreur est survenue lors de la création de votre clé SSH", + "formConnectionPoolToastSuccessTitle": "Succés", + "addSshKeySuccessDescription": "Nouvelle clé SSH créée", + "addSshKeyTitle": "Configurer un nouvelle clé SSH", + "formSshKeyButtonCancel": "Annuler", + "formSshKeyButtonConfirm": "Confirmer" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json new file mode 100644 index 000000000000..62ed5166351e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json @@ -0,0 +1,15 @@ +{ + "tableHeadType": "Type", + "tableHeadDescription": "Description", + "tableHeadVcores": "Vcores", + "tableHeadMemory": "RAM", + "tableHeadStorage": "Stockage éphémère", + "tableHeadPrice": "Prix", + "gpuMemorySpec": "{{memory}} RAM", + "cpuCores_one": "CPU: {{count}} vCore", + "cpuCores_other": "CPU: {{count}} vCores", + "ramSpec": "RAM: {{ram}}", + "localStorageSpec": "Stockage local éphémère: {{disk}} SSD", + "networkSpec": "Réseau public: {{network}}/s", + "resourcePrice": "Prix ressources" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json new file mode 100644 index 000000000000..c95e596fd3c4 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json @@ -0,0 +1,4 @@ +{ + "selectAVersion": "Sélectionner une version", + "noVersionFound": "Aucune version trouvée" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json new file mode 100644 index 000000000000..ceef19bfe421 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json @@ -0,0 +1,19 @@ +{ + "containerFieldLabel": "Container", + "containerFieldPlaceholder": "Séléctionner un container", + "gitBranchFieldLabel": "Branch Git", + "mountDirectoryFieldLabel": "Répertoire de montage", + "permissionsFieldLabel": "Permission", + "permissionDescription": "En `Lecture Seule` aucune synchronisation n'est lancée en fin de job. Le job se finalise ainsi plus rapidement.", + "permission_RO": "Lecture Seule", + "permission_RW": "Lecture & Ecriture", + "permission_RWD": "Lecture & Ecriture & Suppression", + "cachingFieldLabel": "Cache", + "cachingDescription": "Les conteneurs en cache peuvent être réutilisés par d'autres jobs sans synchronisation additionnelle. Les conteneurs sont retirés du cache après une période d'inactivité.", + "duplicateMountPathError": "Ce répertoire de montage existe déjà", + "mountPathError": "Ce répertoire de montage est réservé", + "mountPathErrorFormat": "Format incorrect (ex : /files/test)", + "numberOfConfiguredVolumes_zero": "{{count}} volumes configurés", + "numberOfConfiguredVolumes_one": "{{count}} volume configuré sur {{max}}", + "numberOfConfiguredVolumes_other": "{{count}} volumes configurés sur {{max}}" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json new file mode 100644 index 000000000000..0c243601e95a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json @@ -0,0 +1,17 @@ +{ + "title": "AI Notebook", + "createNewNotebook": "Créer un notebook", + "tableHeaderName": "Name", + "tableHeaderLocation": "Région", + "tableHeaderEnvironment": "Environnement", + "tableHeaderResources": "Ressources", + "tableHeaderPrivacy": "Confidentialité ", + "networkSecureTitle": "Privé", + "networkPublicTitle": "Public", + "tableHeaderDuration": "Durée de fonctionnement", + "tableHeaderStatus": "Statut", + "tableActionManage": "Gérer", + "tableActionStart": "Démarrer", + "tableActionStop": "Arrêter", + "tableActionDelete": "Supprimer" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json new file mode 100644 index 000000000000..61c1938c535c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json @@ -0,0 +1,58 @@ +{ + "title": "Créer un notebook", + "breadcrumb": "Créer un notebook", + "summaryTitle": "Votre commande", + "summaryFieldRegionLabel": "Région:", + "fieldDimensionLabel": "Dimmensionnez votre notebook", + "fieldRegionLabel": "Localisation", + "fieldFlavorLabel": "Ressources", + "fieldFlavorDescription": "Séléctionner le type de ressources", + "fieldFlavorQuantityDescription": "Sélectionner la quantité de ressources", + "fieldFlavorQuantityInformation": "Quantité maximale pour", + "summaryFieldFlavorLabel": "Ressources:", + "summaryFieldFlavorCores_one": "{{count}} vCore", + "summaryFieldFlavorCores_other": "{{count}} vCores", + "summaryFieldFlavorMemory": "{{memory}} RAM", + "summaryFieldStorage": "{{disk}} SSD", + "summaryFieldNetwork": "{{network}}/s", + "fieldCaracteristicLabel": "Choisissez les caractéristiques de votre notebook", + "fieldFrameworkLabel": "Frameworks", + "summaryFieldFrameworkLabel": "Framework:", + "fieldEditorLabel": "Editors", + "summaryFieldEditorLabel": "Editor:", + "fieldConfigurationLabel": "Configurez vos options", + "fieldConfigurationNameLabel": "Nom du notebook", + "fieldConfigurationPrivacyLabel": "Confidentialité", + "privateAccess": "Accès restreint", + "privateAccessDescription": "L’accès à votre notebook est réservé aux utilisateurs Public Cloud OVHcloud créés dans votre projet.", + "publicAccess": "Accès public", + "publicAccessDescription1": "Votre notebook est accessible à tout le monde grâce à votre URL. N’importe qui peut accéder à votre code et aux données qui y sont attachées.", + "publicAccessDescription2": "Soyez prudent·e avec les données sensibles.", + "summaryFieldNameLabel": "Nom:", + "summaryFieldPrivacyLabel": "Confidentialité:", + "summaryFieldPublicLabel": "Public", + "summaryFieldPrivateLabel": "Private", + "formButtonAdvancedConfiguration": "Configuration avancée", + "fieldVolumesLabel": "Volumes", + "summaryFieldVolumesLabel": "Volumes:", + "fieldVolumeDescription": "Faire un petit laius sur les volumes, à quoi ils servent, comment les configurer et tout ci tout ca", + "noVolumeDescription": "Vous n'avez pas de volumes configurés. Configurer un S3 ou Git / Configurer un container Swift", + "summaryFieldVolumes_one": "{{count}} volume configuré", + "summaryFieldVolumes_other": "{{count}} volumes configurés", + "fieldConfigurationLabelsLabel": "Labels", + "summaryFieldLabelsLabel": "Labels:", + "summaryFieldLabels_one": "{{count}} label configuré", + "summaryFieldLabels_other": "{{count}} labels configurés", + "fieldConfigurationSSHKeysLabel": "Clés publiques SSH", + "sshkeyAddButtonLabel": "Configurer une clé SSH", + "sshKeyConfigurationHelper": "Configurer une clé SSH que vous pourrez utiliser sur tout l'univers Public Cloud d'OVHcloud.", + "summaryFieldSSHLabel": "Clés SSH:", + "summaryFieldSshKey_one": "{{count}} clé configurée", + "summaryFieldSshKey_other": "{{count}} clés configurées", + "orderButton": "Commander", + "cliCode": "Equivalent CLI", + "errorGetCommandCli": "Une erreur est survenue lors de la génération de code équivalent pour la CLI", + "cliEquivalentModalTitle": "Création d’un notebook équivalent", + "cliEquivalentModalDescription": "Créer le même notebook via la CLI", + "cliEquivalentModalToastMessage": "Le code a été copié" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json new file mode 100644 index 000000000000..afeff6751635 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json @@ -0,0 +1,26 @@ +{ + "deleteNotebookTitle": "Supprimer le notebook", + "deleteNotebookDescription": "Etes-vous sur de vouloir supprimer le notebook {{name}} ?", + "notebookButtonCancel": "Annuler", + "deleteNotebookButtonConfirm": "Supprimer", + "notebookToastErrorTitle": "Echec", + "notebookToastSuccessTitle": "Succès", + "deleteNotebookToastSuccessDescription": "Le notebook {{name}} a été supprimé", + "startNotebookTitle": "Démarrer le notebook", + "startNotebookDescription": "Etes-vous sur de vouloir démarrer le notebook {{name}} ?", + "startNotebookButtonConfirm": "Démarrer", + "startNotebookToastSuccessDescription": "Le notebook {{name}} a été démarré", + "stopNotebookTitle": "Arrêter le notebook", + "stopNotebookDescription1": "Lorsque vous stoppez un AI Notebook :", + "stopNotebookDescriptionList1": "Les ressources de calculs sont libérées (vous ne payez plus pour les CPUs et GPUs)", + "stopNotebookDescriptionList2": "Nous sauvegardons votre Espace de travail (dossier /worskpace) de manière sécurisée (10 Gio gratuits, ensuite au coût de l'Object Storage).", + "stopNotebookDescriptionList3": "Tout ce qui est sur le stockage local éphémère sera perdu.", + "stopNotebookDescription2": "Une fois stoppé, vous pourrez soit", + "stopNotebookDescription2Bis": "supprimer", + "stopNotebookDescription2Ter": "votre AI Notebook soit le", + "stopNotebookDescription2Quater": "relancer", + "stopNotebookDescription3": "Nous restaurerons votre Espace de Travail et réallouerons des ressources de calculs.", + "stopNotebookConfirmation": "Etes-vous sur de vouloir arrêter le notebook {{name}} ?", + "stopNotebookButtonConfirm": "Arrêter", + "stopNotebookToastSuccessDescription": "Le notebook {{name}} a été arrêté" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json new file mode 100644 index 000000000000..de9edf57924f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json @@ -0,0 +1,11 @@ +{ + "status-DELETING": "En cours de suppression", + "status-RESTARTING": "Redémarrage en cours", + "status-STARTING": "Démarrage", + "status-STOPPING": "En cours d'extinction", + "status-FAILED": "Erreur", + "status-ERROR": "Erreur", + "status-STOPPED": "Arrêté", + "status-SYNC_FAILED": "Echec de synchronisation", + "status-RUNNING": "En service" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json new file mode 100644 index 000000000000..5b317922445e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json @@ -0,0 +1,6 @@ +{ + "pricingHt": "{{ price }} HT", + "pricingInHour": "/ heure", + "pricingTtc": "{{ price }} TTC", + "pricingLabel": "Prix" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json new file mode 100644 index 000000000000..24b6c3bde262 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Straßburg (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Straßburg", + "region_SBG_micro": "Straßburg ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "London", + "region_ERI_micro": "London ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburg", + "region_LIM_micro": "Limburg ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Warschau", + "region_WAW_micro": "Warschau ({{ micro }})", + "region_DE": "Frankfurt", + "region_DE_micro": "Frankfurt ({{ micro }})", + "region_UK": "London", + "region_UK_micro": "London ({{ micro }})", + "region_SGP": "Singapur", + "region_SGP_micro": "Singapur ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "USA", + "region_US_micro": "USA ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Brüssel", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Brüssel ({{ micro }})", + "region_localize": "Lokalisieren", + "region_location_SBG": "Mitteleuropa (Frankreich)", + "region_location_WAW": "Mitteleuropa (Polen)", + "region_location_BHS": "Nordamerika (Kanada)", + "region_location_ERI": "Westeuropa (Vereinigtes Königreich)", + "region_location_GRA": "Westeuropa (Frankreich)", + "region_location_GS": "Westeuropa", + "region_location_MAD": "Westeuropa", + "region_location_BRU": "Westeuropa", + "region_location_LIM": "Mitteleuropa (Deutschland)", + "region_location_RBX": "Westeuropa (Frankreich)", + "region_location_DE": "Mitteleuropa (Deutschland)", + "region_location_UK": "Westeuropa (Vereinigtes Königreich)", + "region_location_SGP": "Südostasien (Singapur)", + "region_location_SYD": "Ozeanien (Australien)", + "region_location_US": "USA", + "region_continent_SBG": "Mitteleuropa", + "region_continent_WAW": "Mitteleuropa", + "region_continent_BHS": "Nordamerika", + "region_continent_GRA": "Westeuropa", + "region_continent_RBX": "Westeuropa", + "region_continent_GS": "Westeuropa", + "region_continent_MAD": "Westeuropa", + "region_continent_BRU": "Westeuropa", + "region_continent_DE": "Mitteleuropa", + "region_continent_UK": "Westeuropa", + "region_continent_SGP": "Südostasien", + "region_continent_SYD": "Ozeanien", + "region_continent_US": "USA", + "region_continent_SHA": "Westeuropa", + "region_continent_all": "Alle Standorte" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json new file mode 100644 index 000000000000..f91fd8920068 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Strasbourg (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Strasbourg", + "region_SBG_micro": "Strasbourg ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "London", + "region_ERI_micro": "London ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburg", + "region_LIM_micro": "Limburg ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Warsaw", + "region_WAW_micro": "Warsaw ({{ micro }})", + "region_DE": "Frankfurt", + "region_DE_micro": "Frankfurt ({{ micro }})", + "region_UK": "London", + "region_UK_micro": "London ({{ micro }})", + "region_SGP": "Singapore", + "region_SGP_micro": "Singapore ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "United States of America", + "region_US_micro": "United States ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Brussels", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Brussels ({{ micro }})", + "region_localize": "Locate", + "region_location_SBG": "Central Europe (France)", + "region_location_WAW": "Central Europe (Poland)", + "region_location_BHS": "North America (Canada)", + "region_location_ERI": "Western Europe (United Kingdom)", + "region_location_GRA": "Western Europe (France)", + "region_location_GS": "Western Europe", + "region_location_MAD": "Western Europe", + "region_location_BRU": "Western Europe", + "region_location_LIM": "Central Europe (Germany)", + "region_location_RBX": "Western Europe (France)", + "region_location_DE": "Central Europe (Germany)", + "region_location_UK": "Western Europe (United Kingdom)", + "region_location_SGP": "South-East Asia (Singapore)", + "region_location_SYD": "Oceania (Australia)", + "region_location_US": "United States of America", + "region_continent_SBG": "Central Europe", + "region_continent_WAW": "Central Europe", + "region_continent_BHS": "North America", + "region_continent_GRA": "Western Europe", + "region_continent_RBX": "Western Europe", + "region_continent_GS": "Western Europe", + "region_continent_MAD": "Western Europe", + "region_continent_BRU": "Western Europe", + "region_continent_DE": "Central Europe", + "region_continent_UK": "Western Europe", + "region_continent_SGP": "South-East Asia", + "region_continent_SYD": "Oceania", + "region_continent_US": "United States of America", + "region_continent_SHA": "Western Europe", + "region_continent_all": "All locations" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json new file mode 100644 index 000000000000..d78d3808005d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Estrasburgo (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Estrasburgo", + "region_SBG_micro": "Estrasburgo ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londres", + "region_ERI_micro": "Londres ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburgo", + "region_LIM_micro": "Limburgo ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Varsovia", + "region_WAW_micro": "Varsovia ({{ micro }})", + "region_DE": "Fráncfort", + "region_DE_micro": "Fráncfort ({{ micro }})", + "region_UK": "Londres", + "region_UK_micro": "Londres ({{ micro }})", + "region_SGP": "Singapur", + "region_SGP_micro": "Singapur ({{ micro }})", + "region_SYD": "Sídney", + "region_SYD_micro": "Sídney ({{ micro }})", + "region_US": "Estados Unidos", + "region_US_micro": "Estados Unidos ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Bruselas", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Bruselas ({{ micro }})", + "region_localize": "Localizar", + "region_location_SBG": "Europa Central (Francia)", + "region_location_WAW": "Europa Central (Polonia)", + "region_location_BHS": "Norteamérica (Canadá)", + "region_location_ERI": "Europa Occidental (Reino Unido)", + "region_location_GRA": "Europa Occidental (Francia)", + "region_location_GS": "Western Europe", + "region_location_MAD": "Western Europe", + "region_location_BRU": "Western Europe", + "region_location_LIM": "Europa Central (Alemania)", + "region_location_RBX": "Europa Occidental (Francia)", + "region_location_DE": "Europa Central (Alemania)", + "region_location_UK": "Europa Occidental (Reino Unido)", + "region_location_SGP": "Sudeste asiático (Singapur)", + "region_location_SYD": "Oceanía (Australia)", + "region_location_US": "Estados Unidos", + "region_continent_SBG": "Europa Central", + "region_continent_WAW": "Europa Central", + "region_continent_BHS": "Norteamérica", + "region_continent_GRA": "Europa Occidental", + "region_continent_RBX": "Europa Occidental", + "region_continent_GS": "Western Europe", + "region_continent_MAD": "Western Europe", + "region_continent_BRU": "Western Europe", + "region_continent_DE": "Europa Central", + "region_continent_UK": "Europa Occidental", + "region_continent_SGP": "Sudeste Asiático", + "region_continent_SYD": "Oceanía", + "region_continent_US": "Estados Unidos", + "region_continent_SHA": "Europa Occidental", + "region_continent_all": "Todas las localizaciones" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json new file mode 100644 index 000000000000..8d6147ad82fb --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Strasbourg (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Strasbourg", + "region_SBG_micro": "Strasbourg ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londres", + "region_ERI_micro": "Londres ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburg", + "region_LIM_micro": "Limburg ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Varsovie", + "region_WAW_micro": "Varsovie ({{ micro }})", + "region_DE": "Francfort", + "region_DE_micro": "Francfort ({{ micro }})", + "region_UK": "Londres", + "region_UK_micro": "Londres ({{ micro }})", + "region_SGP": "Singapour", + "region_SGP_micro": "Singapour ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "États-Unis", + "region_US_micro": "États-Unis ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Bruxelles", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Bruxelles ({{ micro }})", + "region_localize": "Localiser", + "region_location_SBG": "Europe centrale (France)", + "region_location_WAW": "Europe centrale (Pologne)", + "region_location_BHS": "Amérique du Nord (Canada)", + "region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)", + "region_location_GRA": "Europe de l'Ouest (France)", + "region_location_GS": "Western Europe", + "region_location_MAD": "Western Europe", + "region_location_BRU": "Western Europe", + "region_location_LIM": "Europe centrale (Allemagne)", + "region_location_RBX": "Europe de l'Ouest (France)", + "region_location_DE": "Europe centrale (Allemagne)", + "region_location_UK": "Europe de l'Ouest (Grande-Bretagne)", + "region_location_SGP": "Asie du Sud-Est (Singapour)", + "region_location_SYD": "Océanie (Australie)", + "region_location_US": "États-Unis", + "region_continent_SBG": "Europe centrale", + "region_continent_WAW": "Europe centrale", + "region_continent_BHS": "Amérique du Nord", + "region_continent_GRA": "Europe de l'Ouest", + "region_continent_RBX": "Europe de l'Ouest", + "region_continent_GS": "Western Europe", + "region_continent_MAD": "Western Europe", + "region_continent_BRU": "Western Europe", + "region_continent_DE": "Europe centrale", + "region_continent_UK": "Europe de l'Ouest", + "region_continent_SGP": "Asie du Sud-Est", + "region_continent_SYD": "Océanie", + "region_continent_US": "États-Unis", + "region_continent_SHA": "Europe de l'Ouest", + "region_continent_all": "Toutes les localisations" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json new file mode 100644 index 000000000000..8d6147ad82fb --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Strasbourg (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Strasbourg", + "region_SBG_micro": "Strasbourg ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londres", + "region_ERI_micro": "Londres ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburg", + "region_LIM_micro": "Limburg ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Varsovie", + "region_WAW_micro": "Varsovie ({{ micro }})", + "region_DE": "Francfort", + "region_DE_micro": "Francfort ({{ micro }})", + "region_UK": "Londres", + "region_UK_micro": "Londres ({{ micro }})", + "region_SGP": "Singapour", + "region_SGP_micro": "Singapour ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "États-Unis", + "region_US_micro": "États-Unis ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Bruxelles", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Bruxelles ({{ micro }})", + "region_localize": "Localiser", + "region_location_SBG": "Europe centrale (France)", + "region_location_WAW": "Europe centrale (Pologne)", + "region_location_BHS": "Amérique du Nord (Canada)", + "region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)", + "region_location_GRA": "Europe de l'Ouest (France)", + "region_location_GS": "Western Europe", + "region_location_MAD": "Western Europe", + "region_location_BRU": "Western Europe", + "region_location_LIM": "Europe centrale (Allemagne)", + "region_location_RBX": "Europe de l'Ouest (France)", + "region_location_DE": "Europe centrale (Allemagne)", + "region_location_UK": "Europe de l'Ouest (Grande-Bretagne)", + "region_location_SGP": "Asie du Sud-Est (Singapour)", + "region_location_SYD": "Océanie (Australie)", + "region_location_US": "États-Unis", + "region_continent_SBG": "Europe centrale", + "region_continent_WAW": "Europe centrale", + "region_continent_BHS": "Amérique du Nord", + "region_continent_GRA": "Europe de l'Ouest", + "region_continent_RBX": "Europe de l'Ouest", + "region_continent_GS": "Western Europe", + "region_continent_MAD": "Western Europe", + "region_continent_BRU": "Western Europe", + "region_continent_DE": "Europe centrale", + "region_continent_UK": "Europe de l'Ouest", + "region_continent_SGP": "Asie du Sud-Est", + "region_continent_SYD": "Océanie", + "region_continent_US": "États-Unis", + "region_continent_SHA": "Europe de l'Ouest", + "region_continent_all": "Toutes les localisations" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json new file mode 100644 index 000000000000..2d2b67b9b45d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Strasburgo (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Strasburgo", + "region_SBG_micro": "Strasburgo ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londra", + "region_ERI_micro": "Londra ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburgo", + "region_LIM_micro": "Limburgo ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Varsavia", + "region_WAW_micro": "Varsavia ({{ micro }})", + "region_DE": "Francoforte", + "region_DE_micro": "Francoforte ({{ micro }})", + "region_UK": "Londra", + "region_UK_micro": "Londra ({{ micro }})", + "region_SGP": "Singapore", + "region_SGP_micro": "Singapore ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "Stati Uniti", + "region_US_micro": "Stati Uniti ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Bruxelles", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Bruxelles ({{ micro }})", + "region_localize": "Localizza", + "region_location_SBG": "Europa centrale (Francia)", + "region_location_WAW": "Europa centrale (Polonia)", + "region_location_BHS": "Nord America (Canada)", + "region_location_ERI": "Europa Occidentale (Gran Bretagna)", + "region_location_GRA": "Europa Occidentale (Francia)", + "region_location_GS": "Europa Occidentale", + "region_location_MAD": "Europa Occidentale", + "region_location_BRU": "Europa Occidentale", + "region_location_LIM": "Europa centrale (Germania)", + "region_location_RBX": "Europa Occidentale (Francia)", + "region_location_DE": "Europa centrale (Germania)", + "region_location_UK": "Europa Occidentale (Gran Bretagna)", + "region_location_SGP": "Sud-est asiatico (Singapore)", + "region_location_SYD": "Oceania (Australia)", + "region_location_US": "Stati Uniti", + "region_continent_SBG": "Europa centrale", + "region_continent_WAW": "Europa centrale", + "region_continent_BHS": "Nord America ", + "region_continent_GRA": "Europa Occidentale", + "region_continent_RBX": "Europa Occidentale", + "region_continent_GS": "Europa Occidentale", + "region_continent_MAD": "Europa Occidentale", + "region_continent_BRU": "Europa Occidentale", + "region_continent_DE": "Europa centrale", + "region_continent_UK": "Europa Occidentale", + "region_continent_SGP": "Asia Pacifica", + "region_continent_SYD": "Oceania", + "region_continent_US": "Stati Uniti", + "region_continent_SHA": "Europa Occidentale", + "region_continent_all": "Tutte le Region" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json new file mode 100644 index 000000000000..6f3791ace50e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Strasburg (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Strasburg", + "region_SBG_micro": "Strasburg ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londyn", + "region_ERI_micro": "Londyn ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburg", + "region_LIM_micro": "Limburg ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Warszawa", + "region_WAW_micro": "Warszawa ({{ micro }})", + "region_DE": "Frankfurt", + "region_DE_micro": "Frankfurt ({{ micro }})", + "region_UK": "Londyn", + "region_UK_micro": "Londyn ({{ micro }})", + "region_SGP": "Singapur", + "region_SGP_micro": "Singapur ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "Stany Zjednoczone", + "region_US_micro": "Stany Zjednoczone ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madryt", + "region_BRU": "Bruksela", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{micro}})", + "region_MAD_micro": "Madryt ({{micro}})", + "region_BRU_micro": "Bruksela ({{micro}})", + "region_localize": "Lokalizacja", + "region_location_SBG": "Europa Środkowa (Francja)", + "region_location_WAW": "Europa Środkowa (Polska)", + "region_location_BHS": "Ameryka Północna (Kanada)", + "region_location_ERI": "Europa Zachodnia (Wielka Brytania)", + "region_location_GRA": "Europa Zachodnia (Francja)", + "region_location_GS": "Europa Zachodnia", + "region_location_MAD": "Europa Zachodnia", + "region_location_BRU": "Europa Zachodnia", + "region_location_LIM": "Europa Środkowa (Niemcy)", + "region_location_RBX": "Europa Zachodnia (Francja)", + "region_location_DE": "Europa Środkowa (Niemcy)", + "region_location_UK": "Europa Zachodnia (Wielka Brytania)", + "region_location_SGP": "Azja Południowo-Wschodnia (Singapur)", + "region_location_SYD": "Oceania (Australia)", + "region_location_US": "Stany Zjednoczone", + "region_continent_SBG": "Europa Środkowa", + "region_continent_WAW": "Europa Środkowa", + "region_continent_BHS": "Ameryka Północna", + "region_continent_GRA": "Europa Zachodnia", + "region_continent_RBX": "Europa Zachodnia", + "region_continent_GS": "Europa Zachodnia", + "region_continent_MAD": "Europa Zachodnia", + "region_continent_BRU": "Europa Zachodnia", + "region_continent_DE": "Europa Środkowa", + "region_continent_UK": "Europa Zachodnia", + "region_continent_SGP": "Azja Południowo-Wschodnia", + "region_continent_SYD": "Oceania", + "region_continent_US": "Stany Zjednoczone", + "region_continent_SHA": "Europa Zachodnia", + "region_continent_all": "Wszystkie lokalizacje" +} diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json new file mode 100644 index 000000000000..365abdb1cdac --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json @@ -0,0 +1,67 @@ +{ + "region_SBG1": "Estrasburgo (SBG1)", + "region_BHS1": "Beauharnois (BHS1)", + "region_GRA1": "Gravelines (GRA1)", + "region_SBG": "Estrasburgo", + "region_SBG_micro": "Estrasburgo ({{ micro }})", + "region_BHS": "Beauharnois", + "region_BHS_micro": "Beauharnois ({{ micro }})", + "region_ERI": "Londres", + "region_ERI_micro": "Londres ({{ micro }})", + "region_GRA": "Gravelines", + "region_GRA_micro": "Gravelines ({{ micro }})", + "region_LIM": "Limburgo", + "region_LIM_micro": "Limburgo ({{ micro }})", + "region_RBX": "Roubaix", + "region_RBX_micro": "Roubaix ({{ micro }})", + "region_WAW": "Varsóvia", + "region_WAW_micro": "Varsóvia ({{ micro }})", + "region_DE": "Frankfurt ", + "region_DE_micro": "Frankfurt ({{ micro }})", + "region_UK": "Londres", + "region_UK_micro": "Londres ({{ micro }})", + "region_SGP": "Singapura", + "region_SGP_micro": "Singapura ({{ micro }})", + "region_SYD": "Sydney", + "region_SYD_micro": "Sydney ({{ micro }})", + "region_US": "Estados Unidos", + "region_US_micro": "Estados Unidos ({{ micro }})", + "region_GS": "GS", + "region_MAD": "Madrid", + "region_BRU": "Bruxelas", + "region_SHA_micro": "Gravelines (SHADOW-EU-1)", + "region_GS_micro": "Gridscale ({{ micro }})", + "region_MAD_micro": "Madrid ({{ micro }})", + "region_BRU_micro": "Bruxelas ({{ micro }})", + "region_localize": "Localizar", + "region_location_SBG": "Europa Central (França)", + "region_location_WAW": "Europa Central (Polónia)", + "region_location_BHS": "América do Norte (Canadá)", + "region_location_ERI": "Europa Ocidental (Reino Unido)", + "region_location_GRA": "Europa Ocidental (França)", + "region_location_GS": "Western Europe", + "region_location_MAD": "Western Europe", + "region_location_BRU": "Western Europe", + "region_location_LIM": "Europa Central (Alemanha)", + "region_location_RBX": "Europa Ocidental (França)", + "region_location_DE": "Europa Central (Alemanha)", + "region_location_UK": "Europa Ocidental (Reino Unido)", + "region_location_SGP": "Sudeste Asiático (Singapura)", + "region_location_SYD": "Oceânia (Austrália)", + "region_location_US": "Estados Unidos", + "region_continent_SBG": "Europa Central", + "region_continent_WAW": "Europa Central", + "region_continent_BHS": "América do Norte", + "region_continent_GRA": "Europa Ocidental", + "region_continent_RBX": "Europa Ocidental", + "region_continent_GS": "Western Europe", + "region_continent_MAD": "Western Europe", + "region_continent_BRU": "Western Europe", + "region_continent_DE": "Europa Central", + "region_continent_UK": "Europa Ocidental", + "region_continent_SGP": "Sudeste Asiático", + "region_continent_SYD": "Oceânia", + "region_continent_US": "Estados Unidos", + "region_continent_SHA": "Europa Ocidental", + "region_continent_all": "Todas as localizações" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/App.tsx b/packages/manager/apps/pci-ai-notebooks/src/App.tsx new file mode 100644 index 000000000000..0656cb0bf69e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/App.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { odsSetup } from '@ovhcloud/ods-common-core'; +import { useShell } from '@ovh-ux/manager-react-shell-client'; + +import '@ovhcloud/ods-theme-blue-jeans'; +import './global.css'; + +import queryClient from './query.client'; +import Router from '@/routes/Router'; +import Loading from './components/loading/Loading.component'; +import { useLoadingIndicatorContext } from './contexts/LoadingIndicator.context'; +import ProgressLoader from './components/loading/ProgressLoader.component'; + +odsSetup(); + +function App() { + const { loading } = useLoadingIndicatorContext(); + const shell = useShell(); + useEffect(() => { + shell.ux.hidePreloader(); + }, []); + + return ( + + {loading && } + }> + + + + ); +} + +export default App; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts new file mode 100644 index 000000000000..a8c1253ec235 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts @@ -0,0 +1,10 @@ +import { AIError } from '@/data/api'; + +export const apiErrorMock = new AIError( + 'test', + 'test error', + new XMLHttpRequest(), + { message: 'api error message' }, + 500, + 'statusText', +); diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts new file mode 100644 index 000000000000..11b8fb4795be --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts @@ -0,0 +1,5 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedAuthorization: ai.AuthorizationStatus = { + authorized: true, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts new file mode 100644 index 000000000000..dccaac7ac55c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts @@ -0,0 +1,192 @@ +import { nichandle, order } from '@/types/catalog'; + +export const mockedCatalogPlan: order.publicOrder.Plan = { + addonFamilies: [ + { + name: 'name', + }, + ], + configurations: [ + { + isCustom: true, + isMandatory: true, + name: 'name', + }, + ], + invoiceName: 'invoiceName', + planCode: 'databases.mongodb-plan-flavor.hour.consumption', + pricingType: order.cart.GenericProductPricingTypeEnum.consumption, + pricings: [ + { + capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption], + commitment: 1, + description: 'description', + interval: 2, + intervalUnit: order.cart.DurationUnitEnum.day, + mode: 'mode', + mustBeCompleted: true, + phase: 14, + price: 1, + quantity: { + min: 1, + }, + repeat: { + min: 1, + }, + strategy: order.cart.GenericProductPricingStrategyEnum.stairstep, + tax: 1, + type: order.cart.GenericProductPricingTypeEnum.consumption, + }, + ], + product: 'product', +}; + +export const mockedCatalogPlanMonth: order.publicOrder.Plan = { + addonFamilies: [ + { + name: 'name', + }, + ], + configurations: [ + { + isCustom: true, + isMandatory: true, + name: 'name1', + }, + ], + invoiceName: 'invoiceName1', + planCode: 'databases.mongodb-plan-flavor.month.consumption', + pricingType: order.cart.GenericProductPricingTypeEnum.consumption, + pricings: [ + { + capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption], + commitment: 1, + description: 'description', + interval: 2, + intervalUnit: order.cart.DurationUnitEnum.day, + mode: 'mode', + mustBeCompleted: true, + phase: 14, + price: 1, + quantity: { + min: 1, + }, + repeat: { + min: 1, + }, + strategy: order.cart.GenericProductPricingStrategyEnum.stairstep, + tax: 1, + type: order.cart.GenericProductPricingTypeEnum.consumption, + }, + ], + product: 'product', +}; + +export const mockedCatalogStorageMonth: order.publicOrder.Plan = { + addonFamilies: [ + { + name: 'name', + }, + ], + configurations: [ + { + isCustom: true, + isMandatory: true, + name: 'name2', + }, + ], + invoiceName: 'invoiceName2', + planCode: 'databases.mongodb-plan-additionnal-storage-gb.month.consumption', + pricingType: order.cart.GenericProductPricingTypeEnum.consumption, + pricings: [ + { + capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption], + commitment: 1, + description: 'description', + interval: 2, + intervalUnit: order.cart.DurationUnitEnum.day, + mode: 'mode', + mustBeCompleted: true, + phase: 14, + price: 1, + quantity: { + min: 1, + }, + repeat: { + min: 1, + }, + strategy: order.cart.GenericProductPricingStrategyEnum.stairstep, + tax: 1, + type: order.cart.GenericProductPricingTypeEnum.consumption, + }, + ], + product: 'product', +}; + +export const mockedCatalogStorageHour: order.publicOrder.Plan = { + addonFamilies: [ + { + name: 'name', + }, + ], + configurations: [ + { + isCustom: true, + isMandatory: true, + name: 'name3', + }, + ], + invoiceName: 'invoiceName3', + planCode: 'databases.mongodb-plan-additionnal-storage-gb.hour.consumption', + pricingType: order.cart.GenericProductPricingTypeEnum.consumption, + pricings: [ + { + capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption], + commitment: 1, + description: 'description', + interval: 2, + intervalUnit: order.cart.DurationUnitEnum.day, + mode: 'mode', + mustBeCompleted: true, + phase: 14, + price: 1, + quantity: { + min: 1, + }, + repeat: { + min: 1, + }, + strategy: order.cart.GenericProductPricingStrategyEnum.stairstep, + tax: 1, + type: order.cart.GenericProductPricingTypeEnum.consumption, + }, + ], + product: 'product', +}; + +export const mockedCatalog: order.publicOrder.Catalog = { + addons: [ + mockedCatalogPlan, + mockedCatalogPlanMonth, + mockedCatalogStorageMonth, + mockedCatalogStorageHour, + ], + catalogId: 1, + locale: { + currencyCode: order.CurrencyCodeEnum.EUR, + subsidiary: nichandle.OvhSubsidiaryEnum.EU, + taxRate: 40, + }, + planFamilies: [ + { + name: 'planFamiliesName', + }, + ], + plans: [mockedCatalogPlan], + products: [ + { + description: 'description', + name: 'name', + }, + ], +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts new file mode 100644 index 000000000000..fa244519142a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts @@ -0,0 +1,7 @@ +import { Containers } from '@/types/orderFunnel'; + +export const mockedContainer: Containers = { + status: 'ok', + message: 'message', + containers: ['container1', 'container2'], +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts new file mode 100644 index 000000000000..17e9b9de61f0 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts @@ -0,0 +1,59 @@ +import { DataStoresWithContainers } from '@/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook'; +import { DataStoresWithRegion } from '@/hooks/api/ai/datastore/useGetDatastoresWithRegions.hook'; +import * as ai from '@/types/cloud/project/ai'; + +export const mockedDatastoreInput: ai.DataStoreInput = { + alias: 'datastoreAlias', + credentials: { + git: { + basicAuth: { + password: 'password', + username: 'username', + }, + }, + }, + endpoint: 'datastoreEndpoints', + owner: ai.DataStoreOwnerEnum.customer, + type: ai.DataStoreTypeEnum.git, +}; + +export const mockedDatastore: ai.DataStore = { + alias: 'alias', + endpoint: 'endpoint', + owner: ai.DataStoreOwnerEnum.customer, + type: ai.DataStoreTypeEnum.s3, +}; + +export const mockedDatastoreAuth: ai.DataStoreAuth = { + accessKey: 'accessKey', + region: 'region', + s3Url: 's3url', + secretKey: 'secretKey', + token: 'token', + url: 'url', +}; + +export const mockedDatastoreWithRegion: DataStoresWithRegion = { + alias: 'alias', + endpoint: 'endpoint', + owner: ai.DataStoreOwnerEnum.customer, + type: ai.DataStoreTypeEnum.s3, + region: 'GRA', +}; + +export const mockedGitWithRegion: DataStoresWithRegion = { + alias: 'alias', + endpoint: 'endpoint', + owner: ai.DataStoreOwnerEnum.customer, + type: ai.DataStoreTypeEnum.git, + region: 'GRA', +}; + +export const mockedDatastoreWithContainer: DataStoresWithContainers = { + alias: 'alias', + endpoint: 'endpoint', + owner: ai.DataStoreOwnerEnum.customer, + type: ai.DataStoreTypeEnum.s3, + id: 'id', + container: 'container2', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts new file mode 100644 index 000000000000..94a9f84e327c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts @@ -0,0 +1,21 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedCapabilitiesFlavor: ai.capabilities.Flavor = { + default: false, + description: 'descriptionFlav', + gpuInformation: { + gpuBrand: 'gpuBrand', + gpuMemory: 15, + gpuModel: 'gpuModel', + }, + id: 'flavorId', + max: 1, + resourcesPerUnit: { + cpu: 11, + ephemeralStorage: 15, + memory: 5, + privateNetwork: 5, + publicNetwork: 10, + }, + type: ai.capabilities.FlavorTypeEnum.cpu, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts new file mode 100644 index 000000000000..4c520fc03bda --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts @@ -0,0 +1,19 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedGuides: ai.Guide = { + section: 'section', + lang: 'lang', + slug: 'slug', + title: 'title', + excerpt: 'excerpt', + url: 'https://monguide.ovhai.com', +}; + +export const mockedGuideOnboarding: ai.Guide = { + section: 'onbaording', + lang: 'fr_FR', + slug: 'slug', + title: 'how to start a notebook', + excerpt: 'excerpt', + url: 'https://monguide-onboarding.ovhai.com', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts new file mode 100644 index 000000000000..eb786859b1b1 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts @@ -0,0 +1,68 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedJobSpec: ai.job.JobSpec = { + image: 'image', + name: 'name', + region: 'region', + resources: { + cpu: 1, + ephemeralStorage: 1, + flavor: 'flavor', + gpu: 1, + memory: 1, + privateNetwork: 1, + publicNetwork: 1, + }, +}; +export const mockedJobStatus: ai.job.JobStatus = { + dataSync: [ + { + createdAt: 'createdAt', + id: 'datasyncId', + status: { + info: { + code: ai.InfoCodeEnum.APP_RUNNING, + message: 'message', + }, + progress: [ + { + completed: 1, + createdAt: 'createdAt', + deleted: 0, + direction: ai.volume.DataSyncEnum.push, + failed: 0, + id: 'progressId', + info: 'info', + processed: 3, + skipped: 1, + state: ai.volume.DataSyncProgressStateEnum.DONE, + total: 2, + transferredBytes: 30, + updatedAt: 'updatedAt', + }, + ], + queuedAt: 'queuedAt', + state: ai.volume.DataSyncStateEnum.DONE, + }, + updatedAt: 'updatedAt', + }, + ], + history: [ + { + date: 'date', + state: ai.job.JobStateEnum.DONE, + }, + ], + info: { + code: ai.InfoCodeEnum.JOB_DONE, + message: 'message', + }, +}; +export const mockedJob: ai.job.Job = { + createdAt: 'createdAt', + id: 'jobId', + spec: mockedJobSpec, + status: mockedJobStatus, + updatedAt: 'updatedAt', + user: 'user', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts new file mode 100644 index 000000000000..d3c1c78f9f02 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts @@ -0,0 +1,78 @@ +import * as ai from '@/types/cloud/project/ai'; +import { mockedJobStatus } from './job'; + +export const mockedNotebookSpec: ai.notebook.NotebookSpec = { + env: { + editorId: 'editor', + frameworkId: 'frameworkId', + frameworkVersion: 'frameworkVersion', + }, + envVars: [ + { + name: 'envVarsName', + value: 'envVarsValue', + }, + ], + name: 'name', + region: 'region', + resources: { + cpu: 1, + ephemeralStorage: 1, + flavor: 'flavor', + gpu: 1, + memory: 1, + privateNetwork: 1, + publicNetwork: 1, + }, +}; + +export const mockedNotebookStatus: ai.notebook.NotebookStatus = { + dataSync: [ + { + createdAt: 'createdAt', + id: 'datasyncId', + status: { + info: { + code: ai.InfoCodeEnum.APP_RUNNING, + message: 'message', + }, + progress: [ + { + completed: 1, + createdAt: 'createdAt', + deleted: 0, + direction: ai.volume.DataSyncEnum.push, + failed: 0, + id: 'progressId', + info: 'info', + processed: 3, + skipped: 1, + state: ai.volume.DataSyncProgressStateEnum.DONE, + total: 2, + transferredBytes: 30, + updatedAt: 'updatedAt', + }, + ], + queuedAt: 'queuedAt', + state: ai.volume.DataSyncStateEnum.DONE, + }, + updatedAt: 'updatedAt', + }, + ], + + info: { + code: ai.InfoCodeEnum.JOB_DONE, + message: 'message', + }, + + lastJobStatus: mockedJobStatus, +}; + +export const mockedNotebook: ai.notebook.Notebook = { + createdAt: 'createdAt', + id: 'notebookId', + spec: mockedNotebookSpec, + status: mockedNotebookStatus, + updatedAt: 'updatedAt', + user: 'user', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts new file mode 100644 index 000000000000..e839836720ed --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts @@ -0,0 +1,10 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedEditor: ai.notebook.Editor = { + description: 'description', + docUrl: 'docURl', + id: 'editorId', + logoUrl: 'logo', + name: 'EditorName', + version: 'version', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts new file mode 100644 index 000000000000..cf546a9996eb --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts @@ -0,0 +1,10 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedFramework: ai.notebook.Framework = { + description: 'description', + docUrl: 'docURl', + id: 'editorId', + logoUrl: 'logo', + name: 'EditorName', + versions: ['version'], +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts new file mode 100644 index 000000000000..0430f04061ef --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts @@ -0,0 +1,27 @@ +import { AccessTypeEnum } from '@/types/cloud/AccessTypeEnum'; +import { Project } from '@/types/cloud/Project'; +import { ProjectStatusEnum } from '@/types/cloud/project/ProjectStatusEnum'; + +export const mockedPciProject: Project = { + access: AccessTypeEnum.full, + creationDate: 'creationDate', + manualQuota: false, + description: 'description', + projectName: 'projectName', + project_id: 'projectId', + status: ProjectStatusEnum.ok, + unleash: true, + planCode: 'project.2018', +}; + +export const mockedPciDiscoveryProject: Project = { + access: AccessTypeEnum.full, + creationDate: 'creationDate', + manualQuota: false, + description: 'description', + projectName: 'projectName', + project_id: 'projectId', + status: ProjectStatusEnum.ok, + unleash: true, + planCode: 'project.discovery', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts new file mode 100644 index 000000000000..37d20e3303e4 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts @@ -0,0 +1,9 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedCapabilitiesRegion: ai.capabilities.Region = { + cliInstallUrl: 'cliInstallUrl', + documentationUrl: 'documentationUrl', + id: 'GRA', + registryUrl: 'registryUrl', + version: 'version', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts new file mode 100644 index 000000000000..df17aeefe6c9 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts @@ -0,0 +1,25 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedRegistryEdit: ai.registry.RegistryUpdateInput = { + password: 'password', + url: 'url', + username: 'username', +}; + +export const mockedRegistryInput = { + password: 'password', + url: 'url', + username: 'username', + region: 'GRA', +}; + +export const mockedRegistry: ai.registry.Registry = { + createdAt: '1989/04/08', + id: 'id', + region: 'GRA', + updatedAt: '1989/04/08', + user: 'user', + password: '', + url: 'registryUrl', + username: 'username', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts new file mode 100644 index 000000000000..bf64920e5d25 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts @@ -0,0 +1,8 @@ +import * as sshkey from '@/types/cloud/sshkey'; + +export const mockedSshKey: sshkey.SshKey = { + id: 'idSSHKEY', + name: 'nameSSHKEY', + publicKey: 'publicKey', + regions: ['GRA'], +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts new file mode 100644 index 000000000000..203b13392910 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts @@ -0,0 +1,11 @@ +import * as storage from '@/types/cloud/storage'; + +export const mockedStorage: storage.Container = { + archive: false, + containerType: storage.TypeEnum.private, + id: 'storageId', + name: 'storageName', + region: 'region', + storedBytes: 0, + storedObjects: 0, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts new file mode 100644 index 000000000000..f71f897bd437 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts @@ -0,0 +1,30 @@ +import { Suggestions } from '@/types/orderFunnel'; + +export const mockedSuggestion: Suggestions[] = [ + { + region: 'GRA', + ressources: { + nb: 1, + flavor: 'l4-1-gpu', + }, + framework: { + id: 'one-for-all', + version: 'v98-ovh.beta.1', + }, + editorId: 'jupyterlab', + unsecureHttp: false, + }, + { + region: 'BHS', + ressources: { + nb: 1, + flavor: 'ai1-le-1-gpu', + }, + framework: { + id: 'one-for-all', + version: 'v98-ovh.beta.1', + }, + editorId: 'jupyterlab', + unsecureHttp: false, + }, +]; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts new file mode 100644 index 000000000000..e8f4edb2bcfc --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts @@ -0,0 +1,24 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedTokenCreation: ai.token.TokenSpec = { + labelSelector: 'labelSelector', + name: 'tokenName', + region: 'region', + role: ai.TokenRoleEnum.ai_training_operator, +}; + +export const mockedToken: ai.token.Token = { + createdAt: '1989/04/08', + id: 'id', + status: { + value: 'value', + version: 4, + }, + spec: { + labelSelector: 'labelSelector', + name: 'tokenName', + region: 'GRA', + role: ai.TokenRoleEnum.ai_training_operator, + }, + updatedAt: '1989/04/08', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts new file mode 100644 index 000000000000..1880297782fd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts @@ -0,0 +1,42 @@ +import * as user from '@/types/cloud/user'; +import * as ai from '@/types/cloud/project/ai'; + +export const mockedUserDetails: user.UserDetail = { + creationDate: '1989/04/08', + description: 'description', + id: 25, + openstackId: 'openStackId', + roles: [ + { + description: 'description', + id: 'idRole', + name: 'roleName', + permissions: ['RO'], + }, + ], + status: user.UserStatusEnum.ok, + username: 'username', + password: 'password', +}; + +export const mockedUser: user.User = { + creationDate: '1989/04/08', + description: 'description', + id: 25, + openstackId: 'openStackId', + roles: [ + { + description: 'description', + id: 'idRole', + name: 'roleName', + permissions: ['RO'], + }, + ], + status: user.UserStatusEnum.ok, + username: 'username', +}; + +export const mockedUserCreation = { + description: 'description', + role: ai.TokenRoleEnum.ai_training_operator, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts new file mode 100644 index 000000000000..4efc2f5724dd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts @@ -0,0 +1,52 @@ +import { User } from '@ovh-ux/manager-config'; + +export const mockedUser: User = { + nichandle: 'test123', + address: 'address', + area: 'area', + auth: { + description: 'description', + method: 'method', + roles: [], + user: 'user', + account: '', + allowedRoutes: [], + identities: [], + }, + birthCity: 'birthCity', + birthDay: 'birthDay', + certificates: [], + city: 'city', + companyNationalIdentificationNumber: 0, + corporationType: '', + country: '', + currency: { + code: '', + format: '', + symbol: '', + }, + customerCode: '', + email: '', + enterprise: false, + fax: '', + firstname: '', + isTrusted: false, + italianSDI: '', + language: '', + legalform: '', + name: '', + nationalIdentificationNumber: 0, + organisation: '', + ovhCompany: '', + ovhSubsidiary: 'FR', + phone: '', + phoneCountry: '', + sex: '', + spareEmail: '', + state: '', + supportLevel: { + level: '', + }, + vat: '', + zip: '', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts new file mode 100644 index 000000000000..9dbaca05689b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts @@ -0,0 +1,57 @@ +import { User } from '@ovh-ux/manager-config'; + +export const mockedUser: User = { + nichandle: 'test123', + address: 'address', + area: 'area', + auth: { + account: 'account', + description: 'description', + allowedRoutes: [ + { + method: 'GET', + path: 'path', + }, + ], + method: 'method', + identities: ['identities'], + roles: [], + user: 'user', + }, + birthCity: 'birthCity', + birthDay: 'birthDay', + certificates: [], + city: 'city', + companyNationalIdentificationNumber: 0, + corporationType: '', + country: '', + currency: { + code: '', + format: '', + symbol: '', + }, + customerCode: '', + email: '', + enterprise: false, + fax: '', + firstname: '', + isTrusted: false, + italianSDI: '', + language: '', + legalform: '', + name: '', + nationalIdentificationNumber: 0, + organisation: '', + ovhCompany: '', + ovhSubsidiary: '', + phone: '', + phoneCountry: '', + sex: '', + spareEmail: '', + state: '', + supportLevel: { + level: '', + }, + vat: '', + zip: '', +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts new file mode 100644 index 000000000000..a27898956a04 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts @@ -0,0 +1,19 @@ +interface PointerEventProps extends EventInit { + button: number; + ctrlKey: boolean; +} +export class PointerEvent extends Event { + button: number; + + ctrlKey: boolean; + + constructor(type: string, props: PointerEventProps) { + super(type, props); + if (props.button != null) { + this.button = props.button; + } + if (props.ctrlKey != null) { + this.ctrlKey = props.ctrlKey; + } + } +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts new file mode 100644 index 000000000000..9941ec5ee168 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts @@ -0,0 +1,39 @@ +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; + +/** + * Open a Select component and select the correct option + * @param triggerId testId of the trigger select + * @param options labels of the options that should be in the select + * @param optionToSelect the option to select + */ +export async function handleSelectOption( + triggerId: string, + options: string[], + optionToSelect: string, +) { + // Open select + act(() => { + const trigger = screen.getByTestId(triggerId); + fireEvent.focus(trigger); + fireEvent.keyDown(trigger, { key: 'Enter', code: 13 }); + }); + + // Check if select has the options + await waitFor(() => { + expect(screen.getByTestId(triggerId)).not.toHaveAttribute( + 'data-state', + 'closed', + ); + options.forEach((optionLabel) => { + expect(screen.getByText(optionLabel)).toBeInTheDocument(); + }); + }); + // Select the option + act(() => { + const optionsElements = screen.getAllByRole('option'); + const elem = optionsElements.find((e) => + e.innerHTML.includes(optionToSelect), + ); + fireEvent.keyDown(elem, { key: 'Enter', code: 13 }); + }); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx new file mode 100644 index 000000000000..1b9e387c9f5a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx @@ -0,0 +1,10 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); +export const QueryClientWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => ( + {children} +); diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx new file mode 100644 index 000000000000..39e1752e38fd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + RouterProvider, + createHashRouter, + MemoryRouter, + useLocation, +} from 'react-router-dom'; +import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context'; + +/** + * Displays the current location in order to test the syncing between the state of the hook and the url + */ +export const LocationDisplay = () => { + const location = useLocation(); + return ( +
{`${location.pathname}${location.search}`}
+ ); +}; + +export const RouterWithLocationWrapper = ({ + children, + initialEntries = ['/test'], +}: { + children: React.ReactNode; + initialEntries: string[]; +}) => { + return ( + + {children} + + + ); +}; + +export const HashRouterWithLocationWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const element = ( + <> + {children} + + + ); + const router = createHashRouter([ + { + path: '/', + element, + }, + ]); + return ( + + + + ); +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx new file mode 100644 index 000000000000..5728c38997bc --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx @@ -0,0 +1,167 @@ +import { vi } from 'vitest'; +import { ShellClientApi } from '@ovh-ux/shell'; +import { Environment } from '@ovh-ux/manager-config'; +import { ShellProvider } from '@ovh-ux/manager-react-shell-client'; +import { HashRouterWithLocationWrapper } from './RouterWithLocationWrapper'; +import { QueryClientWrapper } from './QueryClientWrapper'; +import { USER_INACTIVITY_TIMEOUT } from '@/configuration/polling.constants'; +import { mockedUser } from '../mocks/user'; +import { UserActivityProvider } from '@/contexts/UserActivityContext'; + +export const RouterWithQueryClientWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const client = { + environment: { + region: 'EU', + userLocale: 'en-GB', + version: '1', + user: mockedUser, + applicationName: 'pci-ai-notebooks', + universe: 'pci', + applicationURLs: {}, + message: { + nl: { + description: '', + }, + fr: { + description: '', + }, + en: { + description: '', + }, + de: { + description: '', + }, + es: { + description: '', + }, + it: { + description: '', + }, + pl: { + description: '', + }, + pt: { + description: '', + }, + }, + applications: {}, + setRegion: vi.fn(), + getRegion: vi.fn(), + setUser: vi.fn(), + getUser: vi.fn(), + setUserLocale: vi.fn(), + getUserLocale: vi.fn(), + getUserLanguage: vi.fn(), + setVersion: vi.fn(), + getVersion: vi.fn(), + setApplicationName: vi.fn(), + getApplicationName: vi.fn(), + getApplication: vi.fn(), + setUniverse: vi.fn(), + setUniverseFromApplicationId: vi.fn(), + getUniverse: vi.fn(), + setApplicationURLs: vi.fn(), + getApplicationURLs: vi.fn(), + getApplicationURL: vi.fn(), + setMessage: vi.fn(), + getMessage: vi.fn(), + getApplications: vi.fn(), + setApplications: vi.fn(), + }, + shell: { + environment: { + getEnvironment: vi.fn(), + setUniverse: vi.fn(), + setApplication: vi.fn(), + }, + i18n: { + getLocale: vi.fn(), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + getAvailableLocales: vi.fn(), + }, + routing: { + listenForHashChange: vi.fn(), + stopListenForHashChange: vi.fn(), + onHashChange: vi.fn(), + }, + auth: { + login: vi.fn(), + logout: vi.fn(), + }, + ux: { + showAccountSidebar: vi.fn(), + disableAccountSidebarToggle: vi.fn(), + enableAccountSidebarToggle: vi.fn(), + isAccountSidebarVisible: vi.fn(), + setForceAccountSiderBarDisplayOnLargeScreen: vi.fn(), + resetAccountSidebar: vi.fn(), + isMenuSidebarVisible: vi.fn(), + showMenuSidebar: vi.fn(), + updateMenuSidebarItemLabel: vi.fn(), + onRequestClientSidebarOpen: vi.fn(), + getSSOAuthModalMode: vi.fn(), + getUserIdCookie: vi.fn(), + onOpenChatbot: vi.fn(), + onCloseChatbot: vi.fn(), + onReduceChatbot: vi.fn(), + onChatbotOpen: vi.fn(), + onChatbotClose: vi.fn(), + openLiveChat: vi.fn(), + openChatbot: vi.fn(), + closeChatbot: vi.fn(), + isChatbotReduced: vi.fn(), + isChatbotVisible: vi.fn(), + startProgress: vi.fn(), + stopProgress: vi.fn(), + hidePreloader: vi.fn(), + showPreloader: vi.fn(), + }, + navigation: { + getURL: vi.fn(), + navigateTo: vi.fn(), + reload: vi.fn(), + }, + tracking: { + setPciProjectMode: vi.fn(), + init: vi.fn(), + onConsentModalDisplay: vi.fn(), + onUserConsentFromModal: vi.fn(), + setConfig: vi.fn(), + setDefaults: vi.fn(), + setRegion: vi.fn(), + trackClick: vi.fn(), + trackClickImpression: vi.fn(), + trackEvent: vi.fn(), + trackImpression: vi.fn(), + trackMVTest: vi.fn(), + trackPage: vi.fn(), + }, + logger: { + log: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + }, + } as { + shell: ShellClientApi; + environment: Environment; + }; + return ( + + + + + {children} + + + + + ); +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts new file mode 100644 index 000000000000..8cd2e90548c9 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { PointerEvent } from './helpers/pointerEvent'; + +// use a custom pointerEvent as jest does not implement it. +// it is requiered for DropdownMenus +// source: https://github.com/radix-ui/primitives/issues/856#issuecomment-928704064 +window.PointerEvent = PointerEvent as any; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx new file mode 100644 index 000000000000..c1ec45111e9e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { Params, useParams, useLocation, useMatches } from 'react-router-dom'; +import { useNavigation } from '@ovh-ux/manager-react-shell-client'; +import usePciProject from '@/hooks/api/project/usePciProject.hook'; +import { Skeleton } from '../ui/skeleton'; +import Link from '@/components/links/Link.component'; +import A from '@/components/links/A.component'; + +export type BreadcrumbHandleParams = { + data: unknown; + params: Params; +}; + +export interface MatchWithBreadcrumb { + id: string; + pathname: string; + params: Params; + data: unknown; + handle: { + breadcrumb?: (breadcrumbParams: { + params: Params; + data: unknown; + }) => React.ReactElement | null; + }; +} + +function Breadcrumb(): JSX.Element { + const { projectId } = useParams(); + const { data: project } = usePciProject(); + + const location = useLocation(); + + const navigation = useNavigation(); + const [baseUrl, setBaseUrl] = useState(''); + const matches = useMatches() as MatchWithBreadcrumb[]; + const [breadcrumbData, setBreadcrumbData] = React.useState([]); + + React.useEffect(() => { + const breadcrumbArray = matches + .filter((match) => Boolean(match.handle?.breadcrumb)) + .map((match) => { + return { + path: match.pathname, + label: match.handle.breadcrumb(match), + }; + }); + setBreadcrumbData(breadcrumbArray); + }, [location.pathname]); + + React.useEffect(() => { + const updateNav = async () => { + const url = await navigation.getURL('public-cloud', ``, {}); + setBaseUrl(url as string); + }; + updateNav(); + }, [navigation]); + + return ( + <> + + {project?.description ?? ( + + )} + + {breadcrumbData.map((bc, index) => ( + + | + {index < breadcrumbData.length - 1 ? ( + + {bc.label} + + ) : ( + + {bc.label} + + )} + + ))} + + ); +} + +export default Breadcrumb; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx new file mode 100644 index 000000000000..9eba67a82652 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx @@ -0,0 +1,66 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import Breadcrumb from '@/components/breadcrumb/Breadcrumb.component'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; + +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: '123456', + }), + useMatches: () => [ + { + id: '1', + pathname: 'users', + data: {}, + handle: { + breadcrumb: () => users, + }, + }, + { + id: '2', + pathname: 'userName', + data: {}, + handle: { + breadcrumb: () => userName, + }, + }, + ], + }; +}); + +vi.mock('@/data/api/project/project.api', () => { + return { + getProject: vi.fn(() => ({ + project_id: '123456', + projectName: 'projectName', + description: 'description', + })), + }; +}); +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useNavigation: () => ({ + getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`), + }), + }; +}); + +describe('Breadcrumb component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('should display the breadcrumb component', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByText('description')).toBeInTheDocument(); + expect(screen.getByText('users')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx new file mode 100644 index 000000000000..9d7164f1b24d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx @@ -0,0 +1,12 @@ +import { useTranslation } from 'react-i18next'; + +interface BreadcrumbItemProps { + translationKey: string; + namespace: string; +} +const BreadcrumbItem = ({ translationKey, namespace }: BreadcrumbItemProps) => { + const { t } = useTranslation(namespace); + return t(translationKey); +}; + +export default BreadcrumbItem; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx new file mode 100644 index 000000000000..26c0cb50a235 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx @@ -0,0 +1,24 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('BreadcrumbItem component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('should display the breadcrumb item component', async () => { + const translationKey = 'testKey'; + render( + , + ); + await waitFor(() => { + expect(screen.getByText(translationKey)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx new file mode 100644 index 000000000000..a38aceed6c35 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx @@ -0,0 +1,49 @@ +import { Copy } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useToast } from '../ui/use-toast'; +import { Button } from '../ui/button'; +import { ScrollArea } from '../ui/scroll-area'; + +interface CliCodeBlockProps { + title: string; + code: string; + toastMessage?: string; +} + +const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => { + const { t } = useTranslation('common'); + const toast = useToast(); + const handleCopyPass = (valueToCopy: string) => { + navigator.clipboard.writeText(valueToCopy); + toast.toast({ + title: toastMessage || t('copied'), + }); + }; + + return ( +
+
+

{title}

+ +
+ +
+          {code}
+        
+
+
+ ); +}; + +export default CliCodeBlock; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx new file mode 100644 index 000000000000..c51a6da30cf8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx @@ -0,0 +1,74 @@ +import { useNavigation } from '@ovh-ux/manager-react-shell-client'; +import { useRouteError } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import errorImgSrc from '@/../public/assets/oops.png'; +import { Alert, AlertDescription } from '../ui/alert'; +import { Button } from '../ui/button'; + +const ErrorBoundary = () => { + const error = useRouteError(); + const { t } = useTranslation('error'); + const nav = useNavigation(); + + const navigateToHomepage = () => { + nav.navigateTo('public-cloud', '', {}); + }; + const reloadPage = () => { + nav.reload(); + }; + + if (error instanceof Error) { + return ( +
+
+
+ +
+

{t('manager_error_page_title')}

+ + +

{t('manager_error_page_default')}

+

{error.message}

+ {'xOvhQueryId' in error && ( +

+ {t('manager_error_page_detail_code', { + code: error.xOvhQueryId, + })} +

+ )} +
+
+ +
+ + +
+
+
+ ); + } + return <>; +}; + +export default ErrorBoundary; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx new file mode 100644 index 000000000000..6c60627a0fe6 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx @@ -0,0 +1,114 @@ +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import * as RouterDom from 'react-router-dom'; +import { useNavigation } from '@ovh-ux/manager-react-shell-client'; +import ErrorBoundary from '@/components/error-boundary/ErrorBoundary.component'; +import { RouterWithQueryClientWrapper } from '../../__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; + +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useRouteError: vi.fn(() => new Error('Test error')), + }; +}); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, replaceObject?: Record) => { + if (!replaceObject) { + return key; + } + const replacements = Object.keys(replaceObject) + .map((replaceKey) => `_${replaceKey}:${replaceObject[replaceKey]}`) + .join(''); + return `${key}${replacements}`; + }, + }), +})); + +vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + const navigateTo = vi.fn(); + const reload = vi.fn(); + return { + ...mod, + useNavigation: () => ({ + navigateTo, + reload, + }), + }; +}); + +describe('ErrorBoundary component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('should display the error boundary component', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByText('Test error')).toBeInTheDocument(); + }); + }); + it('should navigate to home page when button is clicked', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('errorBoundaryGoToHomepage')); + }); + await waitFor(async () => { + expect(screen.getByText('Test error')).toBeInTheDocument(); + expect(useNavigation().navigateTo).toHaveBeenCalled(); + expect(useNavigation().reload).not.toHaveBeenCalled(); + }); + }); + it('should reload page when button is clicked', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('errorBoundaryReload')); + }); + await waitFor(async () => { + expect(screen.getByText('Test error')).toBeInTheDocument(); + expect(useNavigation().navigateTo).not.toHaveBeenCalled(); + expect(useNavigation().reload).toHaveBeenCalled(); + }); + }); + it('should display the ovhquery if it is provided in the error', async () => { + class ErrorWithQueryId extends Error { + xOvhQueryId: string; + + constructor(message: string, xOvhQueryId: string) { + super(message); + this.xOvhQueryId = xOvhQueryId; + } + } + const errorMessage = 'Error with id'; + const queryId = 'mockedQueryId'; + vi.mocked(RouterDom.useRouteError).mockImplementation( + () => new ErrorWithQueryId(errorMessage, queryId), + ); + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(async () => { + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + expect( + screen.getByText((content) => content.includes(queryId)), + ).toBeInTheDocument(); + }); + }); + it('should not render anything if error is not of type Error', async () => { + vi.mocked(RouterDom.useRouteError).mockImplementation(() => null); + const { container } = render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(async () => { + expect(container.childNodes).toHaveLength(1); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx new file mode 100644 index 000000000000..703bd2fe396c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; +import { useLocale } from '@/hooks/useLocale'; + +interface TableDateCellProps { + date: Date; + options?: Intl.DateTimeFormatOptions; +} + +const FormattedDate = ({ date, options }: TableDateCellProps) => { + const locale = useLocale(); + const formater = useMemo( + () => new Intl.DateTimeFormat(locale.replace('_', '-'), options), + [locale], + ); + return formater.format(date); +}; +export default FormattedDate; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx new file mode 100644 index 000000000000..4bc90cde7770 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx @@ -0,0 +1,76 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { useShell } from '@ovh-ux/manager-react-shell-client'; +import FormattedDate from '@/components/formatted-date/FormattedDate.component'; +import { Locale } from '@/hooks/useLocale'; + +vi.mock('@ovh-ux/manager-react-shell-client', () => { + type CallbackType = (localePros: { locale: string }) => void; + let localeChangeCallback: CallbackType | null = null; + const onLocaleChange = (callback: CallbackType) => { + localeChangeCallback = callback; + }; + const getLocale = vi.fn(); + return { + useShell: vi.fn(() => ({ + i18n: { + getLocale, + onLocaleChange, + setLocale: vi.fn((newLocale: string) => { + if (localeChangeCallback) { + localeChangeCallback({ locale: newLocale }); + } + }), + }, + })), + }; +}); + +describe('FormattedDate component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('should render the date with french locale', async () => { + const date = new Date(2024, 5, 8, 12, 42, 50, 151); + vi.mocked(useShell().i18n.getLocale).mockResolvedValue(Locale.fr_FR); + render(); + await waitFor(() => { + expect(screen.getByText('08/06/2024')).toBeInTheDocument(); + }); + }); + it('should render the date with PT locale', async () => { + vi.mocked(useShell().i18n.getLocale).mockResolvedValue(() => Locale.pt_PT); + const date = new Date(2024, 5, 8, 12, 42, 50, 151); + render( + , + ); + await waitFor(() => { + expect( + screen.getByText('8 de junho de 2024 às 12:42:50'), + ).toBeInTheDocument(); + }); + }); + + it('should render the date with EN locale', async () => { + vi.mocked(useShell().i18n.getLocale).mockResolvedValue(() => Locale.en_GB); + const date = new Date(2024, 5, 8, 12, 42, 50, 151); + render( + , + ); + await waitFor(() => { + expect(screen.getByText('8 June 2024 at 12:42:50')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx new file mode 100644 index 000000000000..6cba69f1983f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx @@ -0,0 +1,103 @@ +import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import { BookOpen } from 'lucide-react'; + +import { useParams } from 'react-router-dom'; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '../ui/command'; + +import { Button } from '../ui/button'; +import { Skeleton } from '../ui/skeleton'; +import * as ai from '@/types/cloud/project/ai'; +import { useGetGuides } from '@/hooks/api/ai/guide/useGetGuides.hook'; +import { useLocale } from '@/hooks/useLocale'; + +interface GuidesProps { + section?: string; + onGuideClick?: (guide: ai.Guide) => void; +} +const Guides = ({ section, onGuideClick }: GuidesProps) => { + const { projectId } = useParams(); + const { t } = useTranslation('guides'); + const locale = useLocale(); + const [open, setOpen] = useState(false); + const guidesQuery = useGetGuides( + projectId, + section, + locale.toLocaleLowerCase().replace('_', '-'), + ); + // open the menu on cmd + j + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((prevValue) => !prevValue); + } + }; + + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); + // open a guide in a new tab + const openGuide = (guide: ai.Guide) => { + if (onGuideClick) { + onGuideClick(guide); + } + const { url } = guide; + if (url && /^(http|https):\/\//i.test(url)) { + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); + if (newWindow) newWindow.opener = null; + } + }; + if (guidesQuery.isFetching) + return ; + if (guidesQuery.data?.length === 0) return <>; + return ( + <> + + + + + {t('noResult')} + + {guidesQuery.data + ?.sort((a, b) => a.title.localeCompare(b.title)) + .map((guide) => ( + openGuide(guide)} + className="cursor-pointer flex-col items-start" + > +

{guide.title}

+

{guide.excerpt}

+
+ ))} +
+
+
+ + ); +}; + +export default Guides; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx new file mode 100644 index 000000000000..dabe80fd36ad --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; + +import Guides from './Guides.component'; + +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; + +import { Locale } from '@/hooks/useLocale'; +import { + mockedGuideOnboarding, + mockedGuides, +} from '@/__tests__/helpers/mocks/guides'; + +describe('Guides component', () => { + beforeEach(() => { + vi.restoreAllMocks(); + + // Mock necessary hooks and dependencies + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + + vi.mock('@/data/api/ai/guide.api', () => ({ + getGuides: vi.fn(() => [mockedGuides, mockedGuideOnboarding]), + })); + + const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders the skeleton component while loading', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('guide-skeleton')).toBeInTheDocument(); + }); + }); + it('renders the guide button, open with ctrl+k and close', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('guide-open-button')).toBeInTheDocument(); + }); + act(() => { + fireEvent.keyDown(window, { key: 'k', ctrlKey: true }); + }); + const trigger = screen.getByTestId('guide-open-button'); + fireEvent.keyDown(trigger, { + key: 'k', + ctrlKey: true, + }); + await waitFor(() => { + expect(screen.getByTestId('guide-header')).toBeInTheDocument(); + }); + act(() => { + fireEvent.keyDown(screen.getByTestId('guide-header'), { + key: 'Escape', + code: 'Escape', + }); + }); + await waitFor(() => { + expect(screen.queryByTestId('guide-header')).not.toBeInTheDocument(); + }); + }); + it('open the guide component on button click and click on a guide', async () => { + Object.assign(window, { + open: vi.fn().mockImplementation(() => Promise.resolve()), + }); + const mockedOnGuideClick = vi.fn(); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.getByTestId('guide-open-button')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId('guide-open-button')); + }); + await waitFor(() => { + expect(screen.getByTestId('guide-header')).toBeInTheDocument(); + expect(screen.getByTestId(mockedGuides.url)).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId(mockedGuides.url)); + }); + await waitFor(() => { + expect(window.open).toHaveBeenCalledWith( + mockedGuides.url, + '_blank', + 'noopener,noreferrer', + ); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx new file mode 100644 index 000000000000..fadd9f43be6d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx @@ -0,0 +1,38 @@ +import { cn } from '@/lib/utils'; + +function A({ + className, + children, + disabled, + ...props +}: React.AnchorHTMLAttributes & { disabled?: boolean }) { + const baseClassName = + 'text-primary-500 font-semibold outiline-none cursor-pointer no-underline hover:text-primary-700 hover:underline'; + const disabledClass = 'opacity-50 cursor-not-allowed hover:text-primary-500'; + const combinedClassName = cn( + baseClassName, + className, + disabled && disabledClass, + ); + if (disabled) { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const { href, onClick, tabIndex, ...otherProps } = props; + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} +export default A; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx new file mode 100644 index 000000000000..95a22c88888c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx @@ -0,0 +1,34 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; +import A from '@/components/links/A.component'; + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + useNavigation: () => ({ + getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`), + }), +})); + +describe('A', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders anchor element correctly', () => { + const { container } = render(Link); + const anchor = container.querySelector('a'); + expect(anchor).toBeInTheDocument(); + expect(anchor).toHaveTextContent('Link'); + expect(anchor).toHaveAttribute('href', '#'); + }); + it('renders disabled anchor element correctly', () => { + const { container } = render( + + Link + , + ); + const anchor = container.querySelector('a'); + expect(anchor).toBeInTheDocument(); + expect(anchor).toHaveTextContent('Link'); + expect(anchor).not.toHaveAttribute('href'); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx new file mode 100644 index 000000000000..21ac9352e5b0 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx @@ -0,0 +1,39 @@ +import { LinkProps, Link as RouterLink } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context'; + +function Link({ + className, + disabled, + children, + to, + ...props +}: LinkProps & { disabled?: boolean }) { + const { setLoading } = useLoadingIndicatorContext(); + const baseClassName = + 'text-primary-500 font-semibold outiline-none cursor-pointer no-underline hover:text-primary-700 hover:underline'; + const disabledClass = 'opacity-50 cursor-not-allowed hover:text-primary-500'; + const combinedClassName = cn( + baseClassName, + className, + disabled && disabledClass, + ); + return ( + { + if (disabled) { + e.preventDefault(); + } else { + setLoading(true); + } + }} + > + {children} + + ); +} + +export default Link; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx new file mode 100644 index 000000000000..bb32dd135da8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx @@ -0,0 +1,85 @@ +import { act, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; +import { BrowserRouter as Router } from 'react-router-dom'; +import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context'; +import Link from './Link.component'; + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + useNavigation: () => ({ + getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`), + }), +})); + +describe('Link', () => { + const useLoadingIndicatorSpy = vi.spyOn( + useLoadingIndicator, + 'useLoadingIndicatorContext', + ); + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders RouterLink element correctly', () => { + const { container } = render( + + + Link + + , + ); + const routerLink = container.querySelector('a'); + expect(routerLink).toBeInTheDocument(); + expect(routerLink).toHaveTextContent('Link'); + expect(routerLink).toHaveAttribute('href', '/route'); + }); + + it('should set loading indicator on click', async () => { + const mockLoading = vi.fn(); + useLoadingIndicatorSpy.mockImplementation(() => ({ + loading: false, + setLoading: mockLoading, + })); + const { container } = render( + + + Link + + , + ); + + const routerLink = container.querySelector('a'); + expect(mockLoading).not.toHaveBeenCalled(); + act(() => { + if (routerLink) { + fireEvent.click(routerLink); + } + }); + expect(mockLoading).toHaveBeenCalled(); + }); + + it('should not set loading indicator when clicked on disabled link', async () => { + const mockLoading = vi.fn(); + useLoadingIndicatorSpy.mockImplementation(() => ({ + loading: false, + setLoading: mockLoading, + })); + const { container } = render( + + + + Link + + + , + ); + + const routerLink = container.querySelector('a'); + expect(mockLoading).not.toHaveBeenCalled(); + act(() => { + if (routerLink) { + fireEvent.click(routerLink); + } + }); + expect(mockLoading).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx new file mode 100644 index 000000000000..d094d9708f88 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx @@ -0,0 +1,45 @@ +import { NavLink as RouterNavLink, NavLinkProps } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context'; + +function NavLink({ + className, + disabled, + children, + to, + end, + ...props +}: NavLinkProps & { disabled?: boolean }) { + const { setLoading } = useLoadingIndicatorContext(); + const baseClassName = + 'whitespace-nowrap w-fit text-primary-500 text-base font-semibold m-0 py-2 hover:text-primary-700'; + const activeClass = 'border-b-2 border-primary-500'; + const disabledClass = 'cursor-not-allowed opacity-50 hover:text-primary-500'; + + return ( + + cn( + baseClassName, + isActive && activeClass, + className, + disabled && disabledClass, + ) + } + onClick={(e) => { + if (disabled) { + e.preventDefault(); + } else { + setLoading(true); + } + }} + {...props} + > + {children} + + ); +} + +export default NavLink; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx new file mode 100644 index 000000000000..a800713bd98f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx @@ -0,0 +1,84 @@ +import { act, fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; +import { BrowserRouter as Router } from 'react-router-dom'; +import NavLink from '@/components/links/NavLink.component'; +import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context'; + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + useNavigation: () => ({ + getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`), + }), +})); + +describe('NavLink', () => { + const useLoadingIndicatorSpy = vi.spyOn( + useLoadingIndicator, + 'useLoadingIndicatorContext', + ); + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders NavLink element correctly', () => { + const { container } = render( + + + Link + + , + ); + const routerLink = container.querySelector('a'); + expect(routerLink).toBeInTheDocument(); + expect(routerLink).toHaveTextContent('Link'); + expect(routerLink).toHaveAttribute('href', '/route'); + }); + it('should set loading indicator on click', async () => { + const mockLoading = vi.fn(); + useLoadingIndicatorSpy.mockImplementation(() => ({ + loading: false, + setLoading: mockLoading, + })); + const { container } = render( + + + Link + + , + ); + + const routerLink = container.querySelector('a'); + expect(mockLoading).not.toHaveBeenCalled(); + act(() => { + if (routerLink) { + fireEvent.click(routerLink); + } + }); + expect(mockLoading).toHaveBeenCalled(); + }); + + it('should not set loading indicator when clicked on disabled link', async () => { + const mockLoading = vi.fn(); + useLoadingIndicatorSpy.mockImplementation(() => ({ + loading: false, + setLoading: mockLoading, + })); + const { container } = render( + + + + Link + + + , + ); + + const routerLink = container.querySelector('a'); + expect(mockLoading).not.toHaveBeenCalled(); + act(() => { + if (routerLink) { + fireEvent.click(routerLink); + } + }); + expect(mockLoading).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx new file mode 100644 index 000000000000..541426114437 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { useNavigation } from '@ovh-ux/manager-react-shell-client'; +import A from './A.component'; + +interface OvhLinkProps { + application: string; + path: string; + params?: Record; +} +function OvhLink({ + application, + path, + params = {}, + children, + ...props +}: React.AnchorHTMLAttributes & + OvhLinkProps & { disabled?: boolean }) { + const navigation = useNavigation(); + const [url, setUrl] = React.useState(''); + React.useEffect(() => { + const fetchUrl = async (urlParams: OvhLinkProps) => { + const goTo = (await navigation.getURL( + urlParams.application, + urlParams.path, + urlParams.params, + )) as string; + setUrl(goTo); + }; + fetchUrl({ application, path, params }); + }, [application, path, params, navigation]); + return ( + + {children} + + ); +} + +export default OvhLink; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx new file mode 100644 index 000000000000..3fc02db92261 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx @@ -0,0 +1,27 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; +import OvhLink from './OvhLink.component'; + +vi.mock('@ovh-ux/manager-react-shell-client', () => ({ + useNavigation: () => ({ + getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`), + }), +})); + +describe('OvhLink component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + it('should render anchor element with href fetched from navigation', async () => { + render( + + Link + , + ); + const anchorElement = screen.getByText('Link'); + await waitFor(() => { + expect(anchorElement).toHaveAttribute('href', '#mockedurl-app/some-path'); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx new file mode 100644 index 000000000000..d534bff440af --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx @@ -0,0 +1,11 @@ +import { OsdsSpinner } from '@ovhcloud/ods-components/react'; + +export default function Loading() { + return ( +
+
+ +
+
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx new file mode 100644 index 000000000000..a6a91d99c966 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx @@ -0,0 +1,13 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import Loading from '@/components/loading/Loading.component'; + +describe('Loading component', () => { + it('should display the loading component', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('loading-container')).toBeInTheDocument(); + expect(screen.getByTestId('osds-spinner')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx new file mode 100644 index 000000000000..7e0cca660f09 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; + +export default function ProgressLoader() { + const [progress, setProgress] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setProgress((prevProgress) => { + // Increment progress in a non-linear fashion for a more dynamic feel + const increment = Math.random() * 20; + const nextProgress = prevProgress + increment; + if (nextProgress < 95) { + return nextProgress; + } + clearInterval(interval); // Stop incrementing near 100% to mimic NProgress behavior + return prevProgress; + }); + }, 500); // Increase progress more frequently + + return () => clearInterval(interval); // Cleanup on component unmount + }, []); + + return ( +
+
+
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx new file mode 100644 index 000000000000..d68bbef047da --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx @@ -0,0 +1,76 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import ProgressLoader from '@/components/loading/ProgressLoader.component'; + +describe('ProgressLoader component', () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + }); + + afterEach(() => { + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + }); + it('should display the loader component', async () => { + render(); + act(() => { + vi.clearAllTimers(); + }); + await waitFor(() => { + expect(screen.getByTestId('progressLoaderContainer')).toBeInTheDocument(); + expect(screen.getByTestId('progressLoaderBar')).toBeInTheDocument(); + }); + }); + it('should increment the progress over time', async () => { + render(); + const initialWidth = screen.getByTestId('progressLoaderBar').style.width; + act(() => { + vi.advanceTimersByTime(500); // Advance time by 500ms + vi.clearAllTimers(); + }); + + await waitFor(() => { + const newWidth = screen.getByTestId('progressLoaderBar').style.width; + expect(newWidth).not.toBe(initialWidth); + }); + }); + + it('stops incrementing progress near 100% and does not exceed it', async () => { + render(); + act(() => { + vi.advanceTimersByTime(20000); + vi.clearAllTimers(); + }); + const progressBar = screen.getByTestId('progressLoaderBar'); + + await waitFor(() => { + const progressWidth = parseFloat(progressBar.style.width); + expect(progressWidth).toBeGreaterThan(90); + expect(progressWidth).toBeLessThan(100); + }); + }); + + it('should reset progress to 0 after reaching 100%', async () => { + render(); + act(() => { + vi.advanceTimersByTime(100); + vi.clearAllTimers(); + }); + + await waitFor(() => { + const { width } = screen.getByTestId('progressLoaderBar').style; + expect(width).toBe('0%'); + }); + }); + + it('cleans up the interval when the component is unmounted', () => { + const { unmount } = render(); + vi.clearAllTimers(); + unmount(); + + // After unmounting the component, advancing timers should not cause any errors or warnings + expect(() => { + vi.advanceTimersByTime(500); + }).not.toThrow(); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx new file mode 100644 index 000000000000..91d874f5352d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { PlusCircle, Trash2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import * as ai from '@/types/cloud/project/ai'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { CONFIGURATION_CONFIG } from './configuration.constants'; + +interface LabelsFormProps { + labelValue: ai.Label[]; + onChange: (newLabels: ai.Label[]) => void; + disabled?: boolean; +} + +const LabelsForm = React.forwardRef( + ({ labelValue, onChange, disabled }, ref) => { + const { t } = useTranslation('pci-ai-notebooks/components/configuration'); + const labelSchema = z.object({ + name: z + .string() + .min(1) + .max(15) + .refine( + (newKey) => + !labelValue.some( + (existingLabel) => + existingLabel.name.toLowerCase() === newKey.toLowerCase(), + ), + { + message: t('existingKeyError'), + }, + ), + value: z + .string() + .min(1) + .max(15), + }); + + const form = useForm({ + resolver: zodResolver(labelSchema), + }); + + const onSubmit: SubmitHandler = (data: ai.Label) => { + const newLabels = [...labelValue, data]; + onChange(newLabels); + form.reset(); + }; + + const removeLabel = (indexToRemove: number) => { + const newLabels = labelValue.filter( + (_, index) => index !== indexToRemove, + ); + onChange(newLabels); + }; + + return ( +
+
+
+ ( + + + {t('keyFieldLabel')} + + + + + + + )} + /> + ( + + {t('valueFieldLabel')} + + + + + + )} + /> +
+ +
+
    + {labelValue.map((label, index) => ( +
  • +
    + {label.name} + {label.value && ( + <> + - + + {label.value} + + + )} +
    + +
  • + ))} +
+

+ {t('numberOfConfiguredLabels', { + count: labelValue.length, + max: CONFIGURATION_CONFIG.maxLabelNumber, + context: `${labelValue.length}`, + })} +

+
+ ); + }, +); + +LabelsForm.displayName = 'LabelsForm'; + +export default LabelsForm; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx new file mode 100644 index 000000000000..1448eec54239 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx @@ -0,0 +1,161 @@ +import React, { useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { PlusCircle, Trash2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { CONFIGURATION_CONFIG } from './configuration.constants'; +import { OrderSshKey } from '@/types/orderFunnel'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import * as sshkey from '@/types/cloud/sshkey'; + +interface SshKeyFormProps { + configuredSshKeys: sshkey.SshKey[]; + sshKeyList: OrderSshKey[]; + onChange: (newSshKeyList: OrderSshKey[]) => void; + disabled?: boolean; +} + +const SshKeyForm = React.forwardRef( + ({ configuredSshKeys, sshKeyList, onChange, disabled }, ref) => { + const { t } = useTranslation('pci-ai-notebooks/components/configuration'); + const [selectedSSH, setSelectedSSH] = useState(); + const sshKeySchema = z.object({ + name: z + .string() + .min(1) + .max(15) + .refine( + (newKeyName) => + !sshKeyList.some( + (existingSSHKey) => existingSSHKey.name === newKeyName, + ), + { + message: t('duplicateKeyError'), + }, + ), + sshKey: z.string().min(1), + }); + + const form = useForm({ + resolver: zodResolver(sshKeySchema), + }); + + const onSubmit: SubmitHandler = (data: OrderSshKey) => { + const newSSHKeys = [...sshKeyList, data]; + onChange(newSSHKeys); + form.reset(); + }; + + const removeSSHKey = (indexToRemove: number) => { + const newSSHKeys = sshKeyList.filter( + (_, index) => index !== indexToRemove, + ); + onChange(newSSHKeys); + }; + + return ( +
+ {configuredSshKeys.length > 0 && ( +
+
+ ( + + + {t('configuredKeyFieldLabel')} + + + + + + )} + /> +
+ +
+ )} +
    + {sshKeyList.map((sshKey, index) => ( +
  • + {sshKey.name} + +
  • + ))} +
+

+ {t('numberOfConfiguredKeys', { + count: sshKeyList.length, + max: CONFIGURATION_CONFIG.maxSshKeyNumber, + context: `${sshKeyList.length}`, + })} +

+
+ ); + }, +); + +SshKeyForm.displayName = 'SshKeysForm'; + +export default SshKeyForm; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts new file mode 100644 index 000000000000..780110b7928c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts @@ -0,0 +1,4 @@ +export const CONFIGURATION_CONFIG = { + maxLabelNumber: 10, + maxSshKeyNumber: 10, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx new file mode 100644 index 000000000000..f6582ea98bf0 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { UserRound, UsersRound } from 'lucide-react'; +import * as ai from '@/types/cloud/project/ai'; +import { cn } from '@/lib/utils'; +import RadioTile from '@/components/radio-tile/RadioTile.component'; + +interface EditorsSelectProps { + editors: ai.capabilities.notebook.Editor[]; + value: string; + onChange: (newEditor: string) => void; + className?: string; +} + +const EditorsSelect = React.forwardRef( + ({ editors, value, onChange, className }, ref) => { + return ( +
+ {editors.map((editor) => ( + onChange(editor.id)} + value={editor.id} + checked={editor.id === value} + > +
+
+
+ {editor.name} +
+ {editor.id === 'jupyterlabcollaborative' ? ( + + ) : ( + + )} +
+ {editor.logoUrl && ( + {editor.name} + )} +
+ +

+ {editor.description} +

+
+ ))} +
+ ); + }, +); +EditorsSelect.displayName = 'EditorsSelect'; +export default EditorsSelect; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx new file mode 100644 index 000000000000..a047b8bb597b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx @@ -0,0 +1,24 @@ +import { FieldError, FieldErrors } from 'react-hook-form'; + +const ErrorList = ({ error }: { error: FieldErrors }) => { + const renderErrorMessages = (errors: FieldErrors): React.ReactNode[] => { + return Object.entries(errors).flatMap(([key, value]) => { + if (typeof value === 'object' && value !== null) { + if ('message' in value) { + // Direct error message + return
  • {(value as FieldError).message}
  • ; + } + // Nested errors or array errors + const nestedErrors = renderErrorMessages(value as FieldErrors); + return nestedErrors.length > 0 ? nestedErrors : []; + } + return []; // For TypeScript compliance, return empty array for non-error cases + }); + }; + + return ( +
      {renderErrorMessages(error)}
    + ); +}; + +export default ErrorList; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx new file mode 100644 index 000000000000..d60c332b43dc --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx @@ -0,0 +1,52 @@ +import { FieldErrors } from 'react-hook-form'; +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import ErrorList from '@/components/order/error-list/ErrorList.component'; + +describe('ErrorList component', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + const requiredMessage = 'This field is required'; + const minLenghtdMessage = 'Minimum 5 digits for this field'; + const nestedError = 'This is a nestedError'; + const mockFieldErrors: FieldErrors = { + // Erreurs au niveau des champs + field1: { + type: 'required', + message: requiredMessage, + }, + field2: { + type: 'minLength', + message: minLenghtdMessage, + }, + }; + + it('should display Error', async () => { + render(); + await waitFor(() => { + expect(screen.getByText(requiredMessage)).toBeInTheDocument(); + expect(screen.getByText(minLenghtdMessage)).toBeInTheDocument(); + }); + }); + + it('Empty Error List should not display Error', async () => { + const mockedEmptyFieldErrors: FieldErrors = {}; + render(); + await waitFor(() => { + expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); + }); + }); + + it('Nested Error list should not display Error', async () => { + const mockedNestedFieldErrors: FieldErrors = { + field1: { + type: nestedError, + }, + }; + render(); + await waitFor(() => { + expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx new file mode 100644 index 000000000000..09f93282fa32 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Cpu, Zap } from 'lucide-react'; +import Price from '@/components/price/Price.component'; +import * as ai from '@/types/cloud/project/ai'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { cn } from '@/lib/utils'; +import { bytesConverter } from '@/lib/bytesHelper'; +import { Flavor } from '@/types/orderFunnel'; + +interface FlavorsSelectProps { + flavors: Flavor[]; + value: string; + resourcesQuantity: number; + onChange: (newFlavor: string) => void; + className?: string; +} + +const FlavorsSelect = React.forwardRef( + ({ flavors, value, resourcesQuantity, onChange, className }, ref) => { + const { t } = useTranslation('pci-ai-notebooks/components/flavor'); + + flavors.sort((a, b) => a.pricing[0].price - b.pricing[0].price); + + const clickInput = (flavorName: string) => { + const inputElement = document.getElementById( + `flavor-${flavorName}`, + ) as HTMLInputElement | null; + if (inputElement) { + inputElement.click(); + } + }; + const handleKeyDown = ( + e: React.KeyboardEvent, + flavorName: string, + ) => { + if (e.key === 'Enter') { + clickInput(flavorName); + } + }; + return ( + + + + + {t('tableHeadType')} + + + {t('tableHeadDescription')} + + + {t('tableHeadVcores')} + + + {t('tableHeadMemory')} + + + {t('tableHeadStorage')} + + + {t('tableHeadPrice')} + + + + + {flavors.map((flavor) => ( + clickInput(flavor.id)} + onKeyDown={(e) => handleKeyDown(e, flavor.id)} + key={flavor.id} + className={`border border-primary-100 hover:bg-primary-50 cursor-pointer text-[#4d5592] ${ + value === flavor.id ? 'bg-[#DEF8FF] font-bold' : '' + }`} + > + + +
    + {flavor.type === ai.capabilities.FlavorTypeEnum.cpu ? ( + + ) : ( + + )} + {flavor.id} +
    +
    + + {flavor.description} + + + {resourcesQuantity * flavor.resourcesPerUnit.cpu} + + + {bytesConverter( + resourcesQuantity * flavor.resourcesPerUnit.memory, + false, + 0, + )} + + + {bytesConverter( + resourcesQuantity * flavor.resourcesPerUnit.ephemeralStorage, + false, + 0, + )} + + + + + + ))} + +
    + onChange(e.target.value)} + className="hidden" + id={`flavor-${flavor.id}`} + value={flavor.id} + checked={value === flavor.id} + /> +
    + ); + }, +); + +FlavorsSelect.displayName = 'FlavorsSelect'; + +export default FlavorsSelect; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx new file mode 100644 index 000000000000..17cd20ecbf57 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import * as ai from '@/types/cloud/project/ai'; +import FrameworkTile from './FrameworkTile.component'; +import { FrameworkWithVersion } from '@/types/orderFunnel'; + +interface FrameworkSelectProps { + frameworks: ai.capabilities.notebook.Framework[]; + value: FrameworkWithVersion; + onChange: (newFrameworkWithVersion: FrameworkWithVersion) => void; +} +const FrameworksSelect = React.forwardRef< + HTMLInputElement, + FrameworkSelectProps +>(({ frameworks, value, onChange }, ref) => { + const [selectedFrameworkTypeIndex, setSelectedFrameworkTypeIndex] = useState( + 0, + ); + const mappedFramework = frameworks.map((fmk) => ({ + ...fmk, + type: fmk.docUrl ? 'classical' : 'quantique', + })); + const fmkTypes = [...new Set([...mappedFramework.map((mr) => mr.type)])]; + return ( + <> +
    + setSelectedFrameworkTypeIndex(+v)} + > + + {fmkTypes.map((fmkType, index) => ( + + {fmkType} + + ))} + +
    + {mappedFramework + .filter((r) => r.type === fmkTypes[selectedFrameworkTypeIndex]) + .map((fmk) => ( + v === value.version) + : fmk.versions[0] + } + selected={fmk.id === value.framework} + onChange={( + newFramework: ai.capabilities.notebook.Framework, + newVersion: string, + ) => { + onChange({ + framework: newFramework.id, + version: newVersion, + }); + }} + /> + ))} +
    +
    +
    + + ); +}); + +FrameworksSelect.displayName = 'FrameworksSelect'; +export default FrameworksSelect; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx new file mode 100644 index 000000000000..8025133c9d9d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react'; +import RadioTile from '@/components/radio-tile/RadioTile.component'; +import * as ai from '@/types/cloud/project/ai'; +import VersionSelector from './FrameworkTileVersion.component'; + +export const FrameworkTile = ({ + framework, + version, + selected, + onChange, +}: { + framework: ai.capabilities.notebook.Framework; + version: string; + selected: boolean; + onChange: ( + framework: ai.capabilities.notebook.Framework, + version: string, + ) => void; +}) => { + const [selectedVersion, setSelectedVersion] = useState(version); + const handleFrameworkClick = () => { + onChange(framework, selectedVersion); + }; + useEffect(() => { + onChange(framework, selectedVersion); + }, [selectedVersion]); + return ( + +
    +
    +
    + {framework.name} +
    +
    + {framework.logoUrl && ( + {framework.name} + )} +
    + + { + setSelectedVersion(framework.versions.find((v) => v === versionName)); + }} + /> + +

    + {framework.description} +

    +
    + ); +}; + +export default FrameworkTile; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx new file mode 100644 index 000000000000..28a2aca1fd30 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface VersionSelectorProps { + versions: string[]; + selectedVersion: string; + isFrameworkSelected: boolean; + onChange: (version: string) => void; +} +const VersionSelector = ({ + versions, + selectedVersion, + isFrameworkSelected, + onChange, +}: VersionSelectorProps) => { + const [open, setOpen] = useState(false); + const { t } = useTranslation('pci-ai-notebooks/components/framework'); + return ( + +
    + {versions.map((fmkVersion) => ( + + ))} +
    + + + + + + + + {t('noVersionFound')} + + {versions.map((fmkVersion) => ( + { + onChange(value); + setOpen(false); + }} + > + + + {fmkVersion} + + + ))} + + + + +
    + ); +}; + +export default VersionSelector; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx new file mode 100644 index 000000000000..c070788996e8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx @@ -0,0 +1,30 @@ +import { useTranslation } from 'react-i18next'; +import Price from '@/components/price/Price.component'; +import { order } from '@/types/catalog'; + +interface OrderPriceProps { + minuteConverter: number; + price: order.publicOrder.Pricing; + quantity: number; +} +const OrderPrice = ({ minuteConverter, price, quantity }: OrderPriceProps) => { + const { t } = useTranslation('pricing'); + return ( +
    + {t('pricingLabel')} +
    + +
    +
    + ); +}; + +export default OrderPrice; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx new file mode 100644 index 000000000000..442a211a8c1a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import * as ai from '@/types/cloud/project/ai'; +import { cn } from '@/lib/utils'; +import RegionTile from './RegionTile.component'; + +interface RegionsSelectProps { + regions: ai.capabilities.Region[]; + value: string; + onChange: (newRegion: string) => void; + className?: string; +} + +const RegionsSelect = React.forwardRef( + ({ regions, value, onChange, className }, ref) => { + return ( +
    + {regions.map((region) => ( + onChange(newValue)} + /> + ))} +
    + ); + }, +); +RegionsSelect.displayName = 'RegionsSelect'; +export default RegionsSelect; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx new file mode 100644 index 000000000000..d1f96841a811 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import RadioTile from '@/components/radio-tile/RadioTile.component'; +import * as ai from '@/types/cloud/project/ai'; + +export const RegionTile = ({ + region, + selected, + onChange, +}: { + region: ai.capabilities.Region; + selected: boolean; + onChange: (newRegion: string) => void; +}) => { + const { t: tRegions } = useTranslation('regions'); + return ( + onChange(region.id)} + value={region.id} + checked={selected} + > +
    +
    +
    + {tRegions(`region_${region.id}`)} + {` (${region.id})`} +
    +
    +
    +
    + ); +}; + +export default RegionTile; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx new file mode 100644 index 000000000000..b1cdaa1162b2 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx @@ -0,0 +1,345 @@ +import React, { useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { + GitBranchIcon, + HelpCircle, + Package, + PlusCircle, + Trash2, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import * as ai from '@/types/cloud/project/ai'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { DataStoresWithContainers } from '@/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook'; +import { FormVolumes, OrderVolumes } from '@/types/orderFunnel'; +import { VOLUMES_CONFIG } from './volume.const'; +import { Switch } from '@/components/ui/switch'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; + +interface VolumesFormProps { + configuredVolumesList: DataStoresWithContainers[]; + selectedVolumesList: OrderVolumes[]; + onChange: (newVolumesList: OrderVolumes[]) => void; + disabled?: boolean; +} + +const VolumeForm = React.forwardRef( + ({ configuredVolumesList, selectedVolumesList, onChange, disabled }, ref) => { + const { t } = useTranslation('pci-ai-notebooks/components/volumes'); + const [selectedVolume, setSelectedVolume] = useState< + DataStoresWithContainers + >(); + const volumesSchema = z.object({ + gitBranch: z.string().optional(), + mountDirectory: z + .string() + .min(VOLUMES_CONFIG.mountDirectory.min) + .max(VOLUMES_CONFIG.mountDirectory.max) + .regex(VOLUMES_CONFIG.mountDirectory.pattern, { + message: t('mountPathErrorFormat'), + }) + .refine( + (newMountDirectory) => + !selectedVolumesList.some( + (vol) => vol.mountPath === newMountDirectory, + ), + { + message: t('duplicateMountPathError'), + }, + ) + .refine( + (data) => { + if (data === '/workspace') { + return false; + } + return true; + }, + { message: t('mountPathError') }, + ), + permission: z.nativeEnum(ai.VolumePermissionEnum), + cache: z.boolean(), + }); + + const form = useForm({ + resolver: zodResolver(volumesSchema), + }); + + const onSubmit: SubmitHandler = (data: FormVolumes) => { + const newVolumes: OrderVolumes = { + dataStore: { + alias: selectedVolume.alias, + container: + selectedVolume.type === ai.DataStoreTypeEnum.git + ? data.gitBranch + : selectedVolume.container, + type: selectedVolume.type, + }, + cache: data.cache, + mountPath: data.mountDirectory, + permission: data.permission, + }; + + const newVolumesList = [...selectedVolumesList, newVolumes]; + form.reset(); + onChange(newVolumesList); + }; + + const removeVolume = (indexToRemove: number) => { + const newVolumesList = selectedVolumesList.filter( + (_, index) => index !== indexToRemove, + ); + onChange(newVolumesList); + }; + + return ( +
    +
    +
    + + {t('containerFieldLabel')} + + + + + + + {selectedVolume?.type === ai.DataStoreTypeEnum.git && ( + ( + + + {t('gitBranchFieldLabel')} + + + + + + + )} + /> + )} + + ( + + + {t('mountDirectoryFieldLabel')} + + + + + + + )} + /> + ( + +
    + {t('permissionsFieldLabel')} + + + + + +

    {t('permissionDescription')}

    +
    +
    +
    + + + + +
    + )} + /> +
    +
    + ( + +
    + + {t('cachingFieldLabel')} + + + + + + +

    {t('cachingDescription')}

    +
    +
    +
    + +
    + +
    +
    + +
    + )} + /> + + +
    +
    +
      + {selectedVolumesList.map((volume, index) => ( +
    • +
      + {volume.dataStore.type === ai.DataStoreTypeEnum.git ? ( + + ) : ( + + )} +
      + {volume.dataStore.alias} + {volume.dataStore.container && ( + <> + - + + {volume.dataStore.container} + + + )} + - + {volume.permission} + - + {volume.mountPath} + - + {volume.cache ? 'cache' : 'no cache'} +
      + +
      +
    • + ))} +
    +

    + {t('numberOfConfiguredVolumes', { + count: selectedVolumesList.length, + max: VOLUMES_CONFIG.maxVolumes, + context: `${selectedVolumesList.length}`, + })} +

    +
    + ); + }, +); + +VolumeForm.displayName = 'VolumeForm'; + +export default VolumeForm; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts new file mode 100644 index 000000000000..c567d8b54228 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts @@ -0,0 +1,8 @@ +export const VOLUMES_CONFIG = { + mountDirectory: { + min: 1, + max: 50, + pattern: /^\/(\S)*$/, + }, + maxVolumes: 10, +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx new file mode 100644 index 000000000000..df32d00961fc --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const PageLayout: React.FC = ({ children }) => { + return ( +
    + {children} +
    + ); +}; + +export default PageLayout; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx new file mode 100644 index 000000000000..2f33ab59aeae --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx @@ -0,0 +1,12 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it } from 'vitest'; +import PageLayout from '@/components/page-layout/PageLayout.component'; + +describe('PageLayout component', () => { + it('should display the page layout', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('pageLayout')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx new file mode 100644 index 000000000000..cbba04e59424 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from 'react-i18next'; +import { useLocale } from '@/hooks/useLocale'; +import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; + +interface PriceProps { + priceInUcents: number; + taxInUcents: number; + decimals: number; + displayInHour: boolean; +} +const Price = ({ + priceInUcents, + taxInUcents, + decimals = 2, + displayInHour = true, +}: PriceProps) => { + const { t } = useTranslation('pricing'); + const catalog = useGetCatalog(); + const locale = useLocale(); + + if (!catalog.isSuccess) { + return ( + + ); + } + + const ucentToEur = 100_000_000; + const price = priceInUcents / ucentToEur; + const priceWithTax = (priceInUcents + taxInUcents) / ucentToEur; + const formatPrice = (value: number) => { + const formatter = new Intl.NumberFormat(locale.replace('_', '-'), { + style: 'currency', + currency: catalog.data.locale.currencyCode, + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); + return formatter.format(value); + }; + + return ( + <> + + {t('pricingHt', { price: formatPrice(price) })} + {' '} + {displayInHour && {t('pricingInHour')}}{' '} + + ({t('pricingTtc', { price: formatPrice(priceWithTax) })}) + + + ); +}; + +export default Price; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx new file mode 100644 index 000000000000..b9e64b3b074a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx @@ -0,0 +1,115 @@ +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; +import Price from '@/components/price/Price.component'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options: Record): string => { + if (key === 'pricingInHour') return ''; + return `${key} ${options.price}`; + }, + }), + Trans: ({ children }: { children: React.ReactNode }) => children, +})); +vi.mock('@/hooks/api/catalog/useGetCatalog.hook', () => { + return { + useGetCatalog: vi.fn(() => ({ + isSuccess: true, + data: { + locale: { + currencyCode: 'EUR', + }, + }, + })), + }; +}); +vi.mock('@/hooks/useLocale', () => { + return { + useLocale: vi.fn(() => 'fr_FR'), + }; +}); + +describe('Price component renders', () => { + it('should display the 2 span', () => { + render( + , + ); + expect(screen.getByTestId('pricing-ht')).toBeInTheDocument(); + expect(screen.getByTestId('pricing-ttc')).toBeInTheDocument(); + }); +}); + +describe('Price component value', () => { + it('should display price with tax', () => { + render( + , + ); + expect(screen.getByTestId('pricing-ht')).toHaveTextContent( + 'pricingHt 10,00 €', + ); + expect(screen.getByTestId('pricing-ttc')).toHaveTextContent( + '(pricingTtc 12,00 €)', + ); + }); + + it('should display price without tax', () => { + render( + , + ); + expect(screen.getByTestId('pricing-ht')).toHaveTextContent( + 'pricingHt 10,00 €', + ); + expect(screen.getByTestId('pricing-ttc')).toHaveTextContent( + '(pricingTtc 10,00 €)', + ); + }); + + it('should display price with 3 decimals', () => { + render( + , + ); + expect(screen.getByTestId('pricing-ht')).toHaveTextContent( + 'pricingHt 10,000 €', + ); + expect(screen.getByTestId('pricing-ttc')).toHaveTextContent( + '(pricingTtc 12,000 €)', + ); + }); + + it('should display 0,00 when given 0', () => { + render( + , + ); + expect(screen.getByTestId('pricing-ht')).toHaveTextContent( + 'pricingHt 0,00 €', + ); + expect(screen.getByTestId('pricing-ttc')).toHaveTextContent( + '(pricingTtc 0,00 €)', + ); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx new file mode 100644 index 000000000000..73c47fa1516b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx @@ -0,0 +1,62 @@ +import { useId } from 'react'; +import { cn } from '@/lib/utils'; + +interface RadioTileProps extends React.InputHTMLAttributes { + children: React.ReactNode | React.ReactNode[]; + className?: string; +} +const RadioTile = ({ children, className, ...props }: RadioTileProps) => { + const id = useId(); + const handleLabelKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + const inputElement = document.getElementById( + id, + ) as HTMLInputElement | null; + if (inputElement) { + inputElement.click(); + } + } + }; + return ( +
    + (props.onChange ? props.onChange(e) : null)} + className="hidden" + type="radio" + id={id} + checked={props.checked} + {...props} + /> + +
    + ); +}; + +RadioTile.Separator = function RadioTileSeparator() { + return ( +
    + ); +}; + +export default RadioTile; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx new file mode 100644 index 000000000000..8d0cbe0a0e5f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx @@ -0,0 +1,88 @@ +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import RadioTile from '@/components/radio-tile/RadioTile.component'; + +describe('RadioTile component', () => { + it('should display the radio tile', async () => { + render( + +

    title

    + +

    content

    +
    , + ); + await waitFor(() => { + expect(screen.getByText('title')).toBeInTheDocument(); + expect(screen.getByText('content')).toBeInTheDocument(); + }); + }); + + it('should trigger callback when selected', async () => { + const onChange = vi.fn(); + render( + +

    title

    + +

    content

    +
    , + ); + act(() => { + fireEvent.click(screen.getByTestId('radio-tile-input')); + }); + await waitFor(() => { + expect(onChange).toHaveBeenCalled(); + }); + }); + + it('should select on key down', async () => { + const onChange = vi.fn(); + render( + +

    title

    + +

    content

    +
    , + ); + act(() => { + fireEvent.keyDown(screen.getByTestId('radio-tile-container'), { + key: 'Enter', + code: 'Enter', + keyCode: 13, + charCode: 13, + }); + }); + await waitFor(() => { + expect(onChange).toHaveBeenCalled(); + }); + }); + + it('should have a different style when selected', async () => { + const onChange = vi.fn(); + const { rerender } = render( + + foo + , + ); + await waitFor(() => { + expect(screen.getByTestId('radio-tile-label')).not.toHaveClass( + 'selected', + ); + }); + act(() => { + rerender( + + foo + , + ); + }); + await waitFor(() => { + expect(screen.getByTestId('radio-tile-label')).toHaveClass('selected'); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx new file mode 100644 index 000000000000..41277d3eb7ab --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx @@ -0,0 +1,85 @@ +import { useRef } from 'react'; +import { Skeleton } from '../ui/skeleton'; +import { ScrollArea, ScrollBar } from '../ui/scroll-area'; +import NavLink from '@/components/links/NavLink.component'; +import { Badge } from '../ui/badge'; + +export interface Tab { + href: string; + label: string; + count?: number; + end?: boolean; + disabled?: boolean; +} +interface TabsMenuProps { + tabs: Tab[]; +} +const TabsMenu = ({ tabs }: TabsMenuProps) => { + const s = useRef(null); + return ( + { + s.current.children[1].scrollLeft += e.deltaY; + }} + > +
    + {tabs.map((tab, index) => ( + + {({ isActive }) => ( + { + if (node && isActive) + node?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + }} + > + {tab.label} + {tab.count > 0 && ( + + {tab.count} + + )} + + )} + + ))} +
    + +
    + ); +}; + +TabsMenu.Skeleton = function TabsMenuSkeleton() { + return ( +
    +
    + + + + + + + + + + + + +
    +
    + ); +}; + +export default TabsMenu; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx new file mode 100644 index 000000000000..a2e043de8e1a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import TabsMenu, { Tab } from '@/components/tabs-menu/TabsMenu.component'; +import { RouterWithQueryClientWrapper } from '../../__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; + +function setup(tabs: Tab[] = []) { + return render(, { + wrapper: RouterWithQueryClientWrapper, + }); +} + +describe('TabsMenu component', () => { + beforeEach(() => { + const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders skeletons correctly', async () => { + render(); + expect(screen.getByTestId('skeleton-container')).toBeInTheDocument(); + }); + + it('renders correctly with no tabs', () => { + setup(); + const scrollArea = screen.getByTestId('tab-container'); + expect(scrollArea).toBeEmptyDOMElement(); + }); + + it('renders tabs with labels and optional count badges', () => { + const tabs = [ + { href: '/tab1', label: 'Tab 1', count: 10 }, + { href: '/tab2', label: 'Tab 2' }, + { href: '/tab3', label: 'Tab 3', count: 5 }, + ]; + setup(tabs); + + tabs.forEach((tab) => { + const link = screen.getByText(tab.label); + expect(link).toBeInTheDocument(); + if (tab.count) { + const badge = screen.getAllByText(tab.count.toString())[0]; + expect(badge).toBeInTheDocument(); + } + }); + }); + + it('handles scroll on wheel event', async () => { + const tabs = [ + { href: '/tab1', label: 'Tab 1', count: 10 }, + { href: '/tab2', label: 'Tab 2' }, + { href: '/tab3', label: 'Tab 3', count: 5 }, + { href: '/tab4', label: 'Tab 4', count: 20 }, + ]; + setup(tabs); + const scrollArea = screen.getByTestId('scrollbar'); + const scrollLeftInitial = scrollArea.children[1].scrollLeft; + + fireEvent.wheel(scrollArea, { deltaY: 50 }); + await waitFor(() => { + expect(scrollArea.children[1].scrollLeft).not.toBe(scrollLeftInitial); + }); + }); + + it('active tab gets scrolled into view on click', async () => { + const scrollMock = vi.fn(); + window.HTMLElement.prototype.scrollIntoView = scrollMock; + const tabs = [ + { href: '/', label: 'Tab 1', count: 10 }, + { href: '/tab2', label: 'Tab 2' }, + { href: '/tab3', label: 'Tab 3', count: 5 }, + ]; + setup(tabs); + const tab1 = screen.getByText('Tab 1').closest('span'); + + await userEvent.click(tab1); + + await waitFor(() => { + expect(scrollMock).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'center', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx new file mode 100644 index 000000000000..83d58e2e18ff --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx @@ -0,0 +1,162 @@ +import React, { useRef } from 'react'; +import { z } from 'zod'; +import { X, PlusCircle } from 'lucide-react'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '../ui/input'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '../ui/form'; +import { Button } from '../ui/button'; + +interface TagsInputProps { + value: string[]; + onChange: (newTags: string[]) => void; + min?: number; + max?: number; + pattern?: RegExp; + placeholder?: string; + disabled?: boolean; + schema?: z.ZodString; + children?: React.ReactNode; +} + +const TagsInput = React.forwardRef( + ( + { + value, + onChange, + min, + max, + pattern, + placeholder, + disabled = false, + schema: inputSchema, + children, + }, + ref, + ) => { + const addTagBtnRef = useRef(null); + let inputRules = z.string(); + + if (inputSchema) { + inputRules = inputSchema; + } else { + if (typeof min === 'number') { + inputRules = inputRules.min(min); + } + if (typeof max === 'number') { + inputRules = inputRules.max(max); + } + if (pattern instanceof RegExp) { + inputRules = inputRules.regex(pattern, { + message: 'Invalid pattern', + }); + } + } + const schema = z + .object({ + tag: inputRules, + }) + .refine((newTag) => !value.includes(newTag.tag), { + message: 'No duplicate value', + path: ['tag'], + }); + type ValidationSchema = z.infer; + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + tag: '', + }, + }); + + const handleAddTag: SubmitHandler = (formValues) => { + const newTags = [...value, formValues.tag]; + onChange(newTags); + form.reset(); + }; + + const handleRemoveTag = (index: number) => { + const updatedTags = [...value]; + updatedTags.splice(index, 1); + onChange(updatedTags); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + addTagBtnRef.current?.click(); + event.preventDefault(); + } + }; + + return ( +
    +
    + ( + <> +
    + + + + + + +
    + + + )} + /> + {children || ( +
    + {value.map((tag, index) => ( +
    + {tag} + +
    + ))} +
    + )} +
    +
    + ); + }, +); + +TagsInput.displayName = 'TagsInput'; + +export default TagsInput; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx new file mode 100644 index 000000000000..9fa4482246da --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx @@ -0,0 +1,287 @@ +import { + render, + screen, + fireEvent, + waitFor, + act, +} from '@testing-library/react'; +import { vi } from 'vitest'; +import TagsInput from '@/components/tags-input/TagsInput.component'; + +describe('TagsInput component', () => { + it('should render correctly with initial tags', () => { + const handleChange = vi.fn(); + render( + , + ); + + const inputElement = screen.getByPlaceholderText('Enter tag'); + expect(inputElement).toBeInTheDocument(); + + const tag1Element = screen.getByText('tag1'); + expect(tag1Element).toBeInTheDocument(); + + const tag2Element = screen.getByText('tag2'); + expect(tag2Element).toBeInTheDocument(); + }); + + it('should remove tag when remove button is clicked', () => { + const handleChange = vi.fn(); + render( + , + ); + const removeButton = screen.getByTestId('remove_tag_button_0'); + fireEvent.click(removeButton); + expect(handleChange).toHaveBeenCalledWith(['tag2']); + }); + + it('should add new tag when user presses button', async () => { + // initial state + let tags: string[] = ['tag1', 'tag2']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn((newTags: string[]) => { + tags = [...newTags]; + }); + // initialize renderer + const component = render( + dom((newTags) => { + mockOnChange(newTags); + component.rerender(dom(() => {})); + }), + ); + + // act + const inputElement = screen.getByTestId('input_tag'); + const addTagButton = screen.getByTestId('add_tag_button'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tag3', + }, + }); + fireEvent.click(addTagButton); + }); + // assert + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith(['tag1', 'tag2', 'tag3']); + expect(screen.getByText('tag3')).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + }); + + it('should add new tag when user presses enter', async () => { + // initial state + let tags: string[] = ['tag1', 'tag2']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn((newTags: string[]) => { + tags = [...newTags]; + }); + // initialize renderer + const component = render( + dom((newTags) => { + mockOnChange(newTags); + component.rerender(dom(() => {})); + }), + ); + // act + const inputElement = screen.getByTestId('input_tag'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tag3', + }, + }); + fireEvent.keyDown(inputElement, { key: 'Enter', code: 13, charCode: 13 }); + }); + // assert + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith(['tag1', 'tag2', 'tag3']); + expect(screen.getByText('tag3')).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + }); + + it('should display an error when tag is too short', async () => { + // initial state + const tags: string[] = ['tag1']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn(); + // initialize renderer + render(dom(mockOnChange)); + // act + const inputElement = screen.getByTestId('input_tag'); + const addTagButton = screen.getByTestId('add_tag_button'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tag2', + }, + }); + fireEvent.click(addTagButton); + }); + // assert + await waitFor(() => { + expect(mockOnChange).not.toHaveBeenCalled(); + expect( + screen.getByText('String must contain at least 5 character(s)'), + ).toBeInTheDocument(); + expect(inputElement).toHaveValue('tag2'); + }); + }); + + it('should display an error when tag is too long', async () => { + // initial state + const tags: string[] = ['tag1']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn(); + // initialize renderer + render(dom(mockOnChange)); + // act + const inputElement = screen.getByTestId('input_tag'); + const addTagButton = screen.getByTestId('add_tag_button'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tagwithaverylongvaluethatexceedsmaximum', + }, + }); + fireEvent.click(addTagButton); + }); + // assert + await waitFor(() => { + expect(mockOnChange).not.toHaveBeenCalled(); + expect( + screen.getByText('String must contain at most 10 character(s)'), + ).toBeInTheDocument(); + expect(inputElement).toHaveValue( + 'tagwithaverylongvaluethatexceedsmaximum', + ); + }); + }); + + it('should display an error when tag does not match pattern', async () => { + // initial state + const tags: string[] = ['tag1']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn(); + // initialize renderer + render(dom(mockOnChange)); + // act + const inputElement = screen.getByTestId('input_tag'); + const addTagButton = screen.getByTestId('add_tag_button'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tag@forbidden', + }, + }); + fireEvent.click(addTagButton); + }); + // assert + await waitFor(() => { + expect(mockOnChange).not.toHaveBeenCalled(); + expect(screen.getByText('Invalid pattern')).toBeInTheDocument(); + expect(inputElement).toHaveValue('tag@forbidden'); + }); + }); + + it('should display an error when adding a duplicate tag', async () => { + // initial state + const tags: string[] = ['tag1']; + // dom + const dom = (onChange: (newTags: string[]) => void) => ( + + ); + // spy, rerender component on call + const mockOnChange = vi.fn(); + // initialize renderer + render(dom(mockOnChange)); + // act + const inputElement = screen.getByTestId('input_tag'); + const addTagButton = screen.getByTestId('add_tag_button'); + act(() => { + fireEvent.input(inputElement, { + target: { + value: 'tag1', + }, + }); + fireEvent.click(addTagButton); + }); + // assert + await waitFor(() => { + expect(mockOnChange).not.toHaveBeenCalled(); + expect(screen.getByText('No duplicate value')).toBeInTheDocument(); + expect(inputElement).toHaveValue('tag1'); + }); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx new file mode 100644 index 000000000000..e6a723d06574 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx new file mode 100644 index 000000000000..8722561cf6bd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx new file mode 100644 index 000000000000..924b130c73e3 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx @@ -0,0 +1,69 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + info: + "border-transparent bg-blue-100 text-blue-700 [&>svg]:text-blue-700", + success: + "border-transparent bg-green-100 text-green-700 [&>svg]:text-green-700", + warning: + "border-transparent bg-yellow-200 text-yellow-800 [&>svg]:text-green-800", + error: + "border-transparent bg-red-100 text-red-700 [&>svg]:text-red-700", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
    +)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( +
    + {children} +
    +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx new file mode 100644 index 000000000000..c2ed15071965 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx @@ -0,0 +1,44 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + success: + "border-transparent bg-green-100 text-green-700 hover:bg-green-100/80", + info: + "border-transparent bg-blue-100 text-blue-700 hover:bg-blue-100/80", + warning: + "border-transparent bg-yellow-300 text-yellow-800 hover:bg-yellow-100/80", + error: + "border-transparent bg-red-100 text-red-700 hover:bg-red-100/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ) +} + +export { Badge, badgeVariants } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx new file mode 100644 index 000000000000..57f4a373a512 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary-600", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border text-primary border-primary border-2 bg-background font-semibold hover:bg-primary-100", + menu: + "border text-primary border-primary border-2 bg-background font-semibold hover:bg-primary-100 rounded-full", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + table: "hover:bg-primary-100 hover:text-primary-700 hover:font-semibold", + link: "text-primary underline-offset-4 hover:underline", + input: "border border-input bg-background", + }, + size: { + default: "h-10 px-4 py-2 text-base", + sm: "h-9 rounded-md px-3 text-sm", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + link: "text-base", + table: "h-4 w-4 my-auto", + menu: 'size-8 p-0', + input: "h-10 w-full rounded-md px-3 py-2 text-sm" + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx new file mode 100644 index 000000000000..b065f8e0cd2b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx new file mode 100644 index 000000000000..60b8cbe52fa1 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( +

    {children}

    +)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx new file mode 100644 index 000000000000..a23e7a281287 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx new file mode 100644 index 000000000000..d4a5338a3d59 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx @@ -0,0 +1,154 @@ +/* eslint-disable react/no-unknown-property */ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
    + + +
    +)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx new file mode 100644 index 000000000000..a71a1c699516 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx @@ -0,0 +1,262 @@ +import { useState } from 'react'; +import { ChevronDown, ChevronUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronFirst, ChevronLast } from 'lucide-react'; +import { + ColumnDef, + SortingColumn, + SortingState, + flexRender, + Table as TanStackTable, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from './skeleton'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; + +interface DataTablePaginationProps { + table: TanStackTable +} +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( +
    +
    +
    + +
    +
    + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
    +
    + + + + +
    +
    +
    + ); +} + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + pageSize?: number; +} + +export function DataTable({ + columns, + data, + pageSize, +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + state: { + sorting, + }, + initialState: { + pagination: { pageSize: pageSize ?? 5 }, + }, + }); + + return ( + <> +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
    +
    + + + ); +} + +interface SortableHeaderProps { + column: SortingColumn; + children: React.ReactNode; +} +export function SortableHeader({ + column, + children, +}: SortableHeaderProps) { + const sort = column.getIsSorted(); + let icon = ; + if (sort === 'asc') { + icon = ; + } else if (sort === 'desc') { + icon = ; + } + + const buttonClass = `px-0 font-bold hover:bg-primary-100 ${ + sort + ? 'text-primary-500 hover:text-primary-500' + : 'text-primary-700 hover:text-primary-500' + }`; + return ( + + ); +} + +interface DataTableSkeletonProps { + columns?: number; + rows?: number; + height?: number; + width?: number; +} +DataTable.Skeleton = function DataTableSkeleton({ + columns = 5, + rows = 5, + height = 16, + width = 80, +}: DataTableSkeletonProps) { + return ( + + + + {Array.from({ length: columns }).map((colHead, iColHead) => ( + + + + ))} + + + + {Array.from({ length: rows }).map((row, iRow) => ( + + {Array.from({ length: columns }).map((col, iCol) => ( + + + + ))} + + ))} + +
    + ); +}; \ No newline at end of file diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/dialog.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/dialog.tsx new file mode 100644 index 000000000000..c23630eb8415 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/dropdown-menu.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000000..f3b4e7e4aad4 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,218 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" +import { VariantProps, cva } from "class-variance-authority" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const dropdownMenuItemVariant = cva( + "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + { + variants: { + variant: { + default: "focus:bg-accent focus:text-accent-foreground", + primary: "text-primary-500 focus:text-primary-500 focus:bg-primary-100 font-semibold", + destructive: + "text-red-500 focus:text-red-500 focus:bg-red-100 font-semibold", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) +export interface DropdownMenuItemProps + extends React.ComponentPropsWithoutRef, + VariantProps { + inset?: boolean; + } + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + DropdownMenuItemProps +>(({ className, variant, inset, ...props }, ref) => ( +
    + +
    +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, + dropdownMenuItemVariant, +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/form.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/form.tsx new file mode 100644 index 000000000000..4603f8b3d5ee --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
    + + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +