diff --git a/.eslintignore b/.eslintignore
index f5c69691a2a8..af55cf13f700 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -16,3 +16,4 @@ vue/src
packages/manager/core/generator
packages/components/ovh-at-internet/src/ovh-at-internet.ts
packages/manager/apps/container
+packages/manager/apps/pci-ai-notebooks/src/components/ui
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
index c26c9f17c5b7..aedc62fde2e4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,3 +5,5 @@ custom-elements
loader
react
vue
+packages/manager/apps/pci-ai-notebooks/src/components/ui
+packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts
\ No newline at end of file
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 e8637959e018..01e23d3d9cf1 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
@@ -44,7 +44,8 @@ export const features = [
'public-cloud:project-settings',
'ai-dashboard',
'data-platform',
- 'ai-endpoints'
+ 'ai-endpoints',
+ 'pci-ai-notebooks'
];
export function getPciProjectMenu(
@@ -297,8 +298,8 @@ export function getPciProjectMenu(
{
id: 'notebooks',
title: 'AI Notebooks',
- feature: 'notebooks',
- href: getURL('public-cloud', `#/pci/projects/${projectId}/notebooks`),
+ feature: 'pci-ai-notebooks',
+ href: getURL('public-cloud', `#/pci/projects/${projectId}/ai/notebooks`),
},
{
id: 'training',
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/package.json b/packages/manager/apps/pci-ai-notebooks/package.json
new file mode 100644
index 000000000000..9564db61118b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/package.json
@@ -0,0 +1,127 @@
+{
+ "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": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.0",
+ "@hookform/resolvers": "^3.3.4",
+ "@ovh-ux/manager-config": "*",
+ "@ovh-ux/manager-core-api": "*",
+ "@ovh-ux/manager-react-core-application": "*",
+ "@ovh-ux/manager-react-shell-client": "*",
+ "@ovh-ux/shell": "*",
+ "@ovhcloud/manager-components": "^1.3.0",
+ "@ovhcloud/ods-common-core": "^17.1.0",
+ "@ovhcloud/ods-common-theming": "^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.8.3",
+ "@tanstack/react-table": "^8.12.0",
+ "@testing-library/dom": "^9.3.4",
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/react": "^14.2.2",
+ "@testing-library/user-event": "^14.5.2",
+ "@types/node": "^18.0.0",
+ "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.4.0",
+ "duration-fns": "^3.0.2",
+ "i18next": "^23.10.0",
+ "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.1",
+ "tailwindcss-animate": "^1.0.7",
+ "turbo": "^1.4.3",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+ "@jest/globals": "^29.7.0",
+ "@ovh-ux/manager-vite-config": "*",
+ "@tanstack/react-query-devtools": "^5.8.3",
+ "@testing-library/dom": "^9.3.3",
+ "@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",
+ "@typescript-eslint/eslint-plugin": "^6.16.0",
+ "@typescript-eslint/parser": "^6.16.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "@vitest/coverage-v8": "^1.2.0",
+ "@vitest/ui": "^1.4.0",
+ "autoprefixer": "^10.4.16",
+ "babel-plugin-transform-import-meta": "^2.2.1",
+ "element-internals-polyfill": "^1.3.10",
+ "eslint": "^8.56.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "knip": "^3.10.0",
+ "postcss": "^8.4.32",
+ "rollup": "^4.4.0",
+ "tailwindcss": "^3.4.0",
+ "ts-jest": "^29.1.1",
+ "ts-node": "^10.9.2",
+ "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/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..63af51e824fb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json
@@ -0,0 +1,3 @@
+{
+ "title": "AI Notebooks"
+}
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..8b9a4cd48b07
--- /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';
+import { useLoadingIndicatorContext } from './contexts/loadingIndicatorContext';
+import ProgressLoader from './components/Loading/ProgressLoader';
+
+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/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..0d310dca93fb
--- /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/loadingIndicatorContext';
+
+/**
+ * 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__/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.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 000000000000..c790aff8ff7c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/Breadcrumb/Breadcrumb.tsx
@@ -0,0 +1,82 @@
+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 '@/data/hooks/pciProjects.api.hooks';
+import { Skeleton } from '../ui/skeleton';
+import { Link, A } from '../links';
+
+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/BreadcrumbItem.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/Breadcrumb/BreadcrumbItem.tsx
new file mode 100644
index 000000000000..9d7164f1b24d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/Breadcrumb/BreadcrumbItem.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/Loading/Loading.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/Loading/Loading.tsx
new file mode 100644
index 000000000000..d534bff440af
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/Loading/Loading.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/ProgressLoader.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/Loading/ProgressLoader.tsx
new file mode 100644
index 000000000000..7e0cca660f09
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/Loading/ProgressLoader.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/PageLayout/PageLayout.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/PageLayout/PageLayout.tsx
new file mode 100644
index 000000000000..df32d00961fc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/PageLayout/PageLayout.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/errorBoundary.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/errorBoundary.tsx
new file mode 100644
index 000000000000..66832b01741f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/errorBoundary.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/links.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links.tsx
new file mode 100644
index 000000000000..edca85019d07
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links.tsx
@@ -0,0 +1,154 @@
+import * as React from 'react';
+import { useNavigation } from '@ovh-ux/manager-react-shell-client';
+import {
+ LinkProps,
+ NavLink as RouterNavLink,
+ Link as RouterLink,
+ NavLinkProps,
+} from 'react-router-dom';
+import { cn } from '@/lib/utils';
+import { useLoadingIndicatorContext } from '@/contexts/loadingIndicatorContext';
+
+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
+ const { href, onClick, tabIndex, ...otherProps } = props;
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+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}
+
+ );
+}
+
+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}
+
+ );
+}
+
+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 { A, Link, NavLink, OvhLink };
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/table-date.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/table-date.tsx
new file mode 100644
index 000000000000..703bd2fe396c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/table-date.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/tabs-menu.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu.tsx
new file mode 100644
index 000000000000..90ecea26b442
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu.tsx
@@ -0,0 +1,85 @@
+import { useRef } from 'react';
+import { Skeleton } from './ui/skeleton';
+import { ScrollArea, ScrollBar } from './ui/scroll-area';
+import { NavLink } from './links';
+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/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 (
+
+ )
+}
+
+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 (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/input.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/input.tsx
new file mode 100644
index 000000000000..677d05fd6c1c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/label.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/label.tsx
new file mode 100644
index 000000000000..d0f4cac95225
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-[#4D5693] text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/pagination.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/pagination.tsx
new file mode 100644
index 000000000000..ea40d196dc72
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/pagination.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { ButtonProps, buttonVariants } from "@/components/ui/button"
+
+const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
+
+)
+Pagination.displayName = "Pagination"
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationContent.displayName = "PaginationContent"
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationItem.displayName = "PaginationItem"
+
+type PaginationLinkProps = {
+ isActive?: boolean
+} & Pick &
+ React.ComponentProps<"a">
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = "icon",
+ ...props
+}: PaginationLinkProps) => (
+
+)
+PaginationLink.displayName = "PaginationLink"
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+)
+PaginationPrevious.displayName = "PaginationPrevious"
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+)
+PaginationNext.displayName = "PaginationNext"
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More pages
+
+)
+PaginationEllipsis.displayName = "PaginationEllipsis"
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/popover.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/popover.tsx
new file mode 100644
index 000000000000..bbba7e0ebf26
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/popover.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/progress.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/progress.tsx
new file mode 100644
index 000000000000..d9fd8b29d55e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/progress.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/radio-group.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/radio-group.tsx
new file mode 100644
index 000000000000..163693a77629
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/radio-group.tsx
@@ -0,0 +1,42 @@
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/scroll-area.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/scroll-area.tsx
new file mode 100644
index 000000000000..18963722e425
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/scroll-area.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & { viewportClassName?: string}
+>(({ className, viewportClassName, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/select.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/select.tsx
new file mode 100644
index 000000000000..fe56d4d3ad53
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/select.tsx
@@ -0,0 +1,158 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/skeleton.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/skeleton.tsx
new file mode 100644
index 000000000000..01b8b6d4f716
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/slider.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/slider.tsx
new file mode 100644
index 000000000000..aa8840ee9700
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/slider.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "@/lib/utils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/sonner.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/sonner.tsx
new file mode 100644
index 000000000000..1128edfceec0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/sonner.tsx
@@ -0,0 +1,29 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/switch.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/switch.tsx
new file mode 100644
index 000000000000..aa58baa29c67
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/switch.tsx
@@ -0,0 +1,27 @@
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/table.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/table.tsx
new file mode 100644
index 000000000000..7ebace421e8a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/tabs.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/tabs.tsx
new file mode 100644
index 000000000000..f57fffdb5a07
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-input.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-input.tsx
new file mode 100644
index 000000000000..656dcfcd4311
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-input.tsx
@@ -0,0 +1,117 @@
+"use client"
+
+import { Input } from "@/components/ui/input";
+
+import { cn } from "@/lib/utils";
+import React from "react";
+import {
+ TimePickerType,
+ getArrowByType,
+ getDateByType,
+ setDateByType,
+} from "./time-picker-utils";
+
+export interface TimePickerInputProps
+ extends React.InputHTMLAttributes {
+ picker: TimePickerType;
+ date: Date | undefined;
+ setDate: (date: Date | undefined) => void;
+ onRightFocus?: () => void;
+ onLeftFocus?: () => void;
+}
+
+const TimePickerInput = React.forwardRef<
+ HTMLInputElement,
+ TimePickerInputProps
+>(
+ (
+ {
+ className,
+ type = "tel",
+ value,
+ id,
+ name,
+ date = new Date(new Date().setHours(0,0,0,0)),
+ setDate,
+ onChange,
+ onKeyDown,
+ picker,
+ onLeftFocus,
+ onRightFocus,
+ ...props
+ },
+ ref
+ ) => {
+ const [flag, setFlag] = React.useState(false);
+
+ /**
+ * allow the user to enter the second digit within 2 seconds
+ * otherwise start again with entering first digit
+ */
+ React.useEffect(() => {
+ if (flag) {
+ const timer = setTimeout(() => {
+ setFlag(false);
+ }, 2000);
+
+ return () => clearTimeout(timer);
+ }
+ }, [flag]);
+
+ const calculatedValue = React.useMemo(
+ () => getDateByType(date, picker),
+ [date, picker]
+ );
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Tab") return;
+ e.preventDefault();
+ if (e.key === "ArrowRight") onRightFocus?.();
+ if (e.key === "ArrowLeft") onLeftFocus?.();
+ if (["ArrowUp", "ArrowDown"].includes(e.key)) {
+ const step = e.key === "ArrowUp" ? 1 : -1;
+ const newValue = getArrowByType(calculatedValue, step, picker);
+ if (flag) setFlag(false);
+ const tempDate = new Date(date);
+ setDate(setDateByType(tempDate, newValue, picker));
+ }
+ if (e.key >= "0" && e.key <= "9") {
+ const newValue = !flag
+ ? "0" + e.key
+ : calculatedValue.slice(1, 2) + e.key;
+ if (flag) onRightFocus?.();
+ setFlag((prev) => !prev);
+ const tempDate = new Date(date);
+ setDate(setDateByType(tempDate, newValue, picker));
+ }
+ };
+
+ return (
+ {
+ e.preventDefault();
+ onChange?.(e);
+ }}
+ type={type}
+ inputMode="decimal"
+ onKeyDown={(e) => {
+ onKeyDown?.(e);
+ handleKeyDown(e);
+ }}
+ {...props}
+ />
+ );
+ }
+);
+
+TimePickerInput.displayName = "TimePickerInput";
+
+export { TimePickerInput };
\ No newline at end of file
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-utils.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-utils.tsx
new file mode 100644
index 000000000000..126bd7625f64
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker-utils.tsx
@@ -0,0 +1,146 @@
+/**
+ * regular expression to check for valid hour format (01-23)
+ */
+export function isValidHour(value: string) {
+ return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
+ }
+
+ /**
+ * regular expression to check for valid 12 hour format (01-12)
+ */
+ export function isValid12Hour(value: string) {
+ return /^(0[1-9]|1[0-2])$/.test(value);
+ }
+
+ /**
+ * regular expression to check for valid minute format (00-59)
+ */
+ export function isValidMinuteOrSecond(value: string) {
+ return /^[0-5][0-9]$/.test(value);
+ }
+
+ type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
+
+ export function getValidNumber(
+ value: string,
+ { max, min = 0, loop = false }: GetValidNumberConfig
+ ) {
+ let numericValue = parseInt(value, 10);
+
+ if (!isNaN(numericValue)) {
+ if (!loop) {
+ if (numericValue > max) numericValue = max;
+ if (numericValue < min) numericValue = min;
+ } else {
+ if (numericValue > max) numericValue = min;
+ if (numericValue < min) numericValue = max;
+ }
+ return numericValue.toString().padStart(2, "0");
+ }
+
+ return "00";
+ }
+
+ export function getValidHour(value: string) {
+ if (isValidHour(value)) return value;
+ return getValidNumber(value, { max: 23 });
+ }
+
+ export function getValid12Hour(value: string) {
+ if (isValid12Hour(value)) return value;
+ return getValidNumber(value, { max: 12 });
+ }
+
+ export function getValidMinuteOrSecond(value: string) {
+ if (isValidMinuteOrSecond(value)) return value;
+ return getValidNumber(value, { max: 59 });
+ }
+
+ type GetValidArrowNumberConfig = {
+ min: number;
+ max: number;
+ step: number;
+ };
+
+ export function getValidArrowNumber(
+ value: string,
+ { min, max, step }: GetValidArrowNumberConfig
+ ) {
+ let numericValue = parseInt(value, 10);
+ if (!isNaN(numericValue)) {
+ numericValue += step;
+ return getValidNumber(String(numericValue), { min, max, loop: true });
+ }
+ return "00";
+ }
+
+ export function getValidArrowHour(value: string, step: number) {
+ return getValidArrowNumber(value, { min: 0, max: 23, step });
+ }
+
+ export function getValidArrowMinuteOrSecond(value: string, step: number) {
+ return getValidArrowNumber(value, { min: 0, max: 59, step });
+ }
+
+ export function setMinutes(date: Date, value: string) {
+ const minutes = getValidMinuteOrSecond(value);
+ date.setMinutes(parseInt(minutes, 10));
+ return date;
+ }
+
+ export function setSeconds(date: Date, value: string) {
+ const seconds = getValidMinuteOrSecond(value);
+ date.setSeconds(parseInt(seconds, 10));
+ return date;
+ }
+
+ export function setHours(date: Date, value: string) {
+ const hours = getValidHour(value);
+ date.setHours(parseInt(hours, 10));
+ return date;
+ }
+
+ export type TimePickerType = "minutes" | "seconds" | "hours"; // | "12hours";
+
+ export function setDateByType(date: Date, value: string, type: TimePickerType) {
+ switch (type) {
+ case "minutes":
+ return setMinutes(date, value);
+ case "seconds":
+ return setSeconds(date, value);
+ case "hours":
+ return setHours(date, value);
+ default:
+ return date;
+ }
+ }
+
+ export function getDateByType(date: Date, type: TimePickerType) {
+ switch (type) {
+ case "minutes":
+ return getValidMinuteOrSecond(String(date.getMinutes()));
+ case "seconds":
+ return getValidMinuteOrSecond(String(date.getSeconds()));
+ case "hours":
+ return getValidHour(String(date.getHours()));
+ default:
+ return "00";
+ }
+ }
+
+ export function getArrowByType(
+ value: string,
+ step: number,
+ type: TimePickerType
+ ) {
+ switch (type) {
+ case "minutes":
+ return getValidArrowMinuteOrSecond(value, step);
+ case "seconds":
+ return getValidArrowMinuteOrSecond(value, step);
+ case "hours":
+ return getValidArrowHour(value, step);
+ default:
+ return "00";
+ }
+ }
\ No newline at end of file
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker.tsx
new file mode 100644
index 000000000000..aa52ea16ef16
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/time-picker.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import * as React from "react";
+import { Clock } from "lucide-react";
+import { Label } from "@/components/ui/label";
+import { TimePickerInput } from "./time-picker-input";
+
+interface TimePickerProps {
+ date: Date | undefined;
+ setDate: (date: Date | undefined) => void;
+}
+
+export function TimePicker({ date, setDate }: TimePickerProps) {
+ const minuteRef = React.useRef(null);
+ const hourRef = React.useRef(null);
+ const secondRef = React.useRef(null);
+
+ return (
+
+
+
+ minuteRef.current?.focus()}
+ />
+
+
+
+ hourRef.current?.focus()}
+ onRightFocus={() => secondRef.current?.focus()}
+ />
+
+
+
+ minuteRef.current?.focus()}
+ />
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/toast.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/toast.tsx
new file mode 100644
index 000000000000..a82247753419
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/toast.tsx
@@ -0,0 +1,127 @@
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/toaster.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/toaster.tsx
new file mode 100644
index 000000000000..a2209ba5866d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/toaster.tsx
@@ -0,0 +1,33 @@
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "@/components/ui/toast"
+import { useToast } from "@/components/ui/use-toast"
+
+export function Toaster() {
+ const { toasts } = useToast()
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/tooltip.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/tooltip.tsx
new file mode 100644
index 000000000000..e121f0aea0b3
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/tooltip.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/use-toast.ts b/packages/manager/apps/pci-ai-notebooks/src/components/ui/use-toast.ts
new file mode 100644
index 000000000000..c44a9ad4b715
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/use-toast.ts
@@ -0,0 +1,192 @@
+// Inspired by react-hot-toast library
+import * as React from "react"
+
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 10
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/contexts/loadingIndicatorContext.tsx b/packages/manager/apps/pci-ai-notebooks/src/contexts/loadingIndicatorContext.tsx
new file mode 100644
index 000000000000..baec62764931
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/contexts/loadingIndicatorContext.tsx
@@ -0,0 +1,36 @@
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+interface LoadingIndicatorContextType {
+ loading: boolean;
+ setLoading: React.Dispatch>;
+}
+
+const LoadingIndicatorContext = createContext<
+ LoadingIndicatorContextType | undefined
+>(undefined);
+
+interface LoadingIndicatorProviderProps {
+ children: ReactNode;
+}
+
+export const LoadingIndicatorProvider: React.FC = ({
+ children,
+}) => {
+ const [loading, setLoading] = useState(false);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useLoadingIndicatorContext = (): LoadingIndicatorContextType => {
+ const context = useContext(LoadingIndicatorContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useLoadingIndicatorContext must be used within a LoadingIndicatorProvider',
+ );
+ }
+ return context;
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/api.type.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/api.type.ts
new file mode 100644
index 000000000000..a59dcaa8f544
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/api.type.ts
@@ -0,0 +1,20 @@
+export type ResponseData = {
+ headers: Record;
+ status: number;
+ data?: T;
+ code: string;
+ response?: {
+ status: number;
+ data: { message: string };
+ };
+ detail?: {
+ status: number;
+ };
+};
+
+export type ErrorResponse = {
+ response: {
+ status: number;
+ data: { message: string };
+ };
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/index.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/index.tsx
new file mode 100644
index 000000000000..c5c8ce3ac875
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/index.tsx
@@ -0,0 +1,43 @@
+export interface PCIAI {
+ projectId: string;
+}
+export interface DashboardData extends PCIAI {
+ serviceId: string;
+}
+
+export class AIError extends Error {
+ code: string;
+
+ request: XMLHttpRequest;
+
+ response: {
+ data: {
+ class: string;
+ message: string;
+ details: {
+ message: string;
+ };
+ };
+ status: number;
+ statusText: string;
+ };
+
+ constructor(
+ code: string,
+ message: string,
+ request: XMLHttpRequest,
+ responseData: any,
+ status: number,
+ statusText: string,
+ ) {
+ super(message);
+ this.name = 'AIError';
+ this.code = code;
+ this.request = request;
+ this.response = {
+ data: responseData,
+ status,
+ statusText,
+ };
+ }
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/projects.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/projects.ts
new file mode 100644
index 000000000000..2e4892e07e8c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/projects.ts
@@ -0,0 +1,7 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { PciProject } from '@/types/project';
+
+export const getProject = async (projectId: string): Promise => {
+ const response = await apiClient.v6.get(`/cloud/project/${projectId}`);
+ return response.data as PciProject;
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/hooks/pciProjects.api.hooks.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/hooks/pciProjects.api.hooks.tsx
new file mode 100644
index 000000000000..c1e8067effae
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/hooks/pciProjects.api.hooks.tsx
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+import { useParams } from 'react-router-dom';
+
+import { getProject } from '@/data/api/projects';
+
+const usePciProject = () => {
+ const { projectId } = useParams();
+
+ return useQuery({
+ queryKey: ['projectId', projectId],
+ queryFn: () => getProject(projectId),
+ });
+};
+
+export default usePciProject;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/global.css b/packages/manager/apps/pci-ai-notebooks/src/global.css
new file mode 100644
index 000000000000..75327810a81d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/global.css
@@ -0,0 +1,116 @@
+/* stylelint-disable */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ }
+}
+
+
+/* width */
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 5px;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-family: "Source Sans Pro",sans-serif;
+ font-size: 16px;
+ }
+ h1 {
+ @apply text-ods-heading-800 font-ods-heading-800 leading-ods-heading-800;
+ }
+ h2 {
+ @apply text-ods-heading-700 font-ods-heading-700 leading-ods-heading-700;
+ }
+ h3 {
+ @apply text-ods-heading-500 font-ods-heading-500 leading-ods-heading-500;
+ }
+ h4 {
+ @apply text-ods-heading-400 font-ods-heading-400 leading-ods-heading-400;
+ }
+ h5 {
+ @apply text-ods-heading-200 font-ods-heading-200 leading-ods-heading-200;
+ }
+}
\ No newline at end of file
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useDateFnsLocale.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/useDateFnsLocale.hook.tsx
new file mode 100644
index 000000000000..644be4c3a915
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useDateFnsLocale.hook.tsx
@@ -0,0 +1,26 @@
+import { fr, de, enGB, es, frCA, it, pl, pt } from 'date-fns/locale';
+import { Locale, useLocale } from '@/hooks/useLocale';
+
+export function useDateFnsLocale() {
+ const locale = useLocale();
+ switch (locale) {
+ case Locale.fr_FR:
+ return fr;
+ case Locale.de_DE:
+ return de;
+ case Locale.en_GB:
+ return enGB;
+ case Locale.es_ES:
+ return es;
+ case Locale.fr_CA:
+ return frCA;
+ case Locale.it_IT:
+ return it;
+ case Locale.pl_PL:
+ return pl;
+ case Locale.pt_PT:
+ return pt;
+ default:
+ return enGB;
+ }
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useLocale.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/useLocale.tsx
new file mode 100644
index 000000000000..142d7c3b2d1c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useLocale.tsx
@@ -0,0 +1,35 @@
+import { useShell } from '@ovh-ux/manager-react-shell-client';
+import { useEffect, useState } from 'react';
+
+export enum Locale {
+ 'fr_FR' = 'fr_FR',
+ 'de_DE' = 'de_DE',
+ 'en_GB' = 'en_GB',
+ 'es_ES' = 'es_ES',
+ 'fr_CA' = 'fr_CA',
+ 'it_IT' = 'it_IT',
+ 'pl_PL' = 'pl_PL',
+ 'pt_PT' = 'pt_PT',
+}
+
+/**
+ * Fetches and keep synced the current locale
+ * @returns The current locale string
+ */
+export function useLocale() {
+ const shellCtx = useShell();
+ const [locale, setLocale] = useState(Locale.fr_FR);
+ // fetch initial locale
+ useEffect(() => {
+ const fetchLocale = async () => {
+ const l = (await shellCtx.i18n.getLocale()) as Locale;
+ setLocale(l);
+ };
+ fetchLocale();
+ }, [shellCtx.i18n]);
+ // update on locale change
+ shellCtx.i18n.onLocaleChange(({ locale: newLocale }: { locale: string }) => {
+ setLocale(newLocale as Locale);
+ });
+ return locale;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useModale.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/useModale.tsx
new file mode 100644
index 000000000000..5e99ac8ca43c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useModale.tsx
@@ -0,0 +1,29 @@
+import { useQueryState } from './useQueryState';
+
+export const DEFAULT_OPEN_VALUE = 'true';
+
+export interface ModalController {
+ open: boolean;
+ onOpenChange: (newValue: boolean) => void;
+}
+export function useModale(queryParamKey: string) {
+ const [modalState, setModalState] = useQueryState(queryParamKey);
+
+ const setOpen = (value = DEFAULT_OPEN_VALUE) => setModalState(value);
+ const setClose = () => setModalState(undefined);
+ const toggle = () => (modalState ? setClose() : setOpen());
+ const isOpen = !!modalState;
+ const controller: ModalController = {
+ open: isOpen,
+ onOpenChange: (newStatus: boolean) =>
+ newStatus === false ? setClose() : null,
+ };
+ return {
+ isOpen,
+ value: modalState,
+ open: setOpen,
+ close: setClose,
+ toggle,
+ controller,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useQueryState.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/useQueryState.tsx
new file mode 100644
index 000000000000..f45ae8291ca9
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useQueryState.tsx
@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import qs from 'qs';
+
+export const useQueryState = (
+ query: string,
+): [T | undefined, (value: T | undefined) => void] => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const setQuery = useCallback(
+ (value: T | undefined) => {
+ const existingQueries = qs.parse(location.search, {
+ ignoreQueryPrefix: true,
+ });
+
+ // If value is undefined, delete the query from existingQueries
+ if (value === undefined) {
+ delete existingQueries[query];
+ } else {
+ existingQueries[query] = value;
+ }
+
+ const newQueries =
+ value !== undefined
+ ? { ...existingQueries, [query]: value }
+ : { ...existingQueries };
+
+ const queryString = qs.stringify(newQueries, {
+ skipNulls: true,
+ addQueryPrefix: true,
+ });
+
+ navigate(`${location.pathname}${queryString}`);
+ },
+ [navigate, location, query],
+ );
+
+ return [
+ (qs.parse(location.search, { ignoreQueryPrefix: true })[query] as T) ||
+ undefined,
+ setQuery,
+ ];
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useUser.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/useUser.tsx
new file mode 100644
index 000000000000..8f4f4f3ab7ac
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useUser.tsx
@@ -0,0 +1,21 @@
+import { User } from '@ovh-ux/manager-config';
+import { useShell } from '@ovh-ux/manager-react-shell-client';
+import { useEffect, useState } from 'react';
+
+/**
+ * Fetches the current user
+ * @returns The current user
+ */
+export function useUser() {
+ const shellCtx = useShell();
+ const [user, setUser] = useState();
+ // fetch initial user
+ useEffect(() => {
+ const fetchUser = async () => {
+ const l = (await shellCtx.environment.getEnvironment()).getUser();
+ setUser(l);
+ };
+ fetchUser();
+ }, [shellCtx.environment]);
+ return user;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/i18n.ts b/packages/manager/apps/pci-ai-notebooks/src/i18n.ts
new file mode 100644
index 000000000000..2158568afa61
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/i18n.ts
@@ -0,0 +1,44 @@
+import i18n from 'i18next';
+import I18NextHttpBackend from 'i18next-http-backend';
+import { initReactI18next } from 'react-i18next';
+
+export default function initI18n(
+ locale = 'fr_FR',
+ availablesLocales = ['fr_FR'],
+) {
+ i18n
+ .use(initReactI18next)
+ .use(I18NextHttpBackend)
+ .use({
+ type: 'postProcessor',
+ name: 'normalize',
+ process: (value: string) =>
+ value ? value.replace(/&/g, '&') : value,
+ })
+ .init({
+ lng: locale,
+ debug: false,
+ fallbackLng: 'fr_FR',
+ supportedLngs: availablesLocales,
+ ns: ['pci-ai-notebooks', 'bytes'], // namespaces to load by default
+ backend: {
+ loadPath: (lngs: string[], namespaces: string[]) =>
+ `${import.meta.env.BASE_URL}translations/${namespaces[0]}/Messages_${
+ lngs[0]
+ }.json`,
+ },
+ postProcess: 'normalize',
+ })
+ .then(() => {
+ // plurals are set for locales like this: en-GB, so our locale en_GB is not found.
+ // We copy the rules for each available locale
+ availablesLocales.forEach((l) => {
+ i18n.services.pluralResolver.addRule(
+ l,
+ i18n.services.pluralResolver.getRule(l.replace('_', '-')),
+ );
+ });
+ });
+
+ return i18n;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/index.html b/packages/manager/apps/pci-ai-notebooks/src/index.html
new file mode 100644
index 000000000000..0b4eba6bb850
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ OVHcloud
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/manager/apps/pci-ai-notebooks/src/index.scss b/packages/manager/apps/pci-ai-notebooks/src/index.scss
new file mode 100644
index 000000000000..ab8b62543fad
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/index.scss
@@ -0,0 +1,11 @@
+.border-ods-primary-100 {
+ border-color: var(--ods-color-primary-100);
+}
+
+.background-ods-primary-100 {
+ background-color: var(--ods-color-primary-100);
+}
+
+.background-ods-primary-000 {
+ background-color: var(--ods-color-primary-000);
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/index.tsx b/packages/manager/apps/pci-ai-notebooks/src/index.tsx
new file mode 100644
index 000000000000..d44a4095701c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/index.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ ShellProvider,
+ initShellContext,
+} from '@ovh-ux/manager-react-shell-client';
+import initI18n from './i18n';
+import App from './App';
+
+import '@/vite-hmr.ts';
+import { LoadingIndicatorProvider } from './contexts/loadingIndicatorContext';
+
+const init = async (
+ appName: string,
+ { reloadOnLocaleChange } = { reloadOnLocaleChange: false },
+) => {
+ const context = await initShellContext(appName);
+
+ const region = context.environment.getRegion();
+ try {
+ await import(`./config-${region}.js`);
+ } catch (error) {
+ // nothing to do
+ }
+
+ const locales = await context.shell.i18n.getAvailableLocales();
+
+ const i18n = initI18n(
+ context.environment.getUserLocale(),
+ locales.map(({ key }: { key: string }) => key),
+ );
+
+ context.shell.i18n.onLocaleChange(({ locale }: { locale: string }) => {
+ if (reloadOnLocaleChange) {
+ window.top?.location.reload();
+ } else {
+ i18n.changeLanguage(locale);
+ }
+ });
+
+ ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+
+ ,
+ );
+};
+
+init('pci-ai-notebooks');
diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/bytesHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/bytesHelper.ts
new file mode 100644
index 000000000000..37927a5c1b80
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/lib/bytesHelper.ts
@@ -0,0 +1,96 @@
+import i18next from 'i18next';
+
+export interface Storage {
+ unit: string;
+ value: number;
+}
+const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+
+export function convertToBytes(storage: Storage): number {
+ const unitIndex = units.indexOf(storage.unit);
+ return storage.value * 1000 ** unitIndex;
+}
+
+export function compareStorage(a: Storage, b: Storage): number {
+ // Convert both storage values to bytes
+ const aValueInBytes = convertToBytes(a);
+ const bValueInBytes = convertToBytes(b);
+
+ // Compare the converted values
+ if (aValueInBytes < bValueInBytes) {
+ return -1;
+ }
+
+ if (aValueInBytes > bValueInBytes) {
+ return 1;
+ }
+ return 0;
+}
+
+export function addStorage(a: Storage, b: Storage): Storage {
+ // Convert both storages to bytes for accurate calculation
+ const aValueInBytes = convertToBytes(a);
+ const bValueInBytes = convertToBytes(b);
+
+ // Perform addition in bytes
+ let sumValueInBytes = aValueInBytes + bValueInBytes;
+
+ // Determine the most appropriate unit for the result
+ let resultUnitIndex = 0; // Start from the smallest unit ('B')
+ while (sumValueInBytes >= 1000 && resultUnitIndex < units.length - 1) {
+ sumValueInBytes /= 1000;
+ resultUnitIndex += 1;
+ }
+
+ // Adjust the sum to a sensible precision to avoid floating point issues
+ sumValueInBytes = parseFloat(sumValueInBytes.toFixed(3));
+
+ // Construct the result with the adjusted unit
+ return {
+ unit: units[resultUnitIndex],
+ value: sumValueInBytes,
+ };
+}
+
+export function subtractStorage(a: Storage, b: Storage): Storage {
+ // Convert both storages to bytes for accurate calculation
+ const aValueInBytes = convertToBytes(a);
+ const bValueInBytes = convertToBytes(b);
+
+ // Perform subtraction in bytes
+ let diffValueInBytes = aValueInBytes - bValueInBytes;
+
+ // Determine the most appropriate unit for the result
+ let resultUnitIndex = 0; // Start from the smallest unit ('B')
+ while (diffValueInBytes >= 1000 && resultUnitIndex < units.length - 1) {
+ diffValueInBytes /= 1000;
+ resultUnitIndex += 1;
+ }
+
+ // Construct the result with the adjusted unit
+ return {
+ unit: units[resultUnitIndex],
+ value: diffValueInBytes,
+ };
+}
+
+export function formatStorage(
+ storage: Storage,
+ decimals = 2,
+ useShortUnit = true,
+) {
+ let size = storage.value;
+ let unitIndex = units.indexOf(storage.unit);
+
+ while (size >= 1000 && unitIndex < units.length - 1) {
+ size /= 1000;
+ unitIndex += 1;
+ }
+
+ const unit = i18next.t(
+ `unit${useShortUnit ? 'Short' : 'Long'}-${units[unitIndex]}`,
+ { ns: 'bytes', count: size },
+ );
+
+ return `${Number(Number(size).toFixed(decimals))} ${unit}`;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/durationHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/durationHelper.ts
new file mode 100644
index 000000000000..5ca1571b8a5e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/lib/durationHelper.ts
@@ -0,0 +1,65 @@
+import { Duration, Locale, formatDuration } from 'date-fns';
+import * as duration from 'duration-fns';
+
+export function durationStringToHuman(durationString: string, locale: Locale) {
+ return formatDuration(duration.parse(durationString), {
+ locale,
+ delimiter: ', ',
+ });
+}
+
+export function durationStringToDuration(durationString: string) {
+ // Regular expression to match the duration parts
+ const pattern = /(\d+Y)?(\d+M)?(\d+D)?(\d+H)?(\d+m)?(\d+S)?/;
+ const matches = pattern.exec(durationString);
+
+ // Convert matched values to numbers, defaulting to 0 if not present
+ const years = matches[1] ? parseInt(matches[1], 10) : 0;
+ const months = matches[2] ? parseInt(matches[2], 10) : 0;
+ const days = matches[3] ? parseInt(matches[3], 10) : 0;
+ const hours = matches[4] ? parseInt(matches[4], 10) : 0;
+ const minutes = matches[5] ? parseInt(matches[5], 10) : 0;
+ const seconds = matches[6] ? parseInt(matches[6], 10) : 0;
+ // Construct and return the duration object
+ return { years, months, days, hours, minutes, seconds };
+}
+
+export function durationISOStringToShortTime(durationISOString: string) {
+ let durationString = '';
+ const durationISO = duration.parse(durationISOString);
+ if (durationISO.years > 0) durationString += `${durationISO.years}Y`;
+ if (durationISO.months > 0) durationString += `${durationISO.months}M`;
+ if (durationISO.days > 0) durationString += `${durationISO.days}D`;
+ if (durationISO.hours > 0) durationString += `${durationISO.hours}H`;
+ if (durationISO.minutes > 0) durationString += `${durationISO.minutes}m`;
+ if (durationISO.seconds > 0) durationString += `${durationISO.seconds}S`;
+ return durationString;
+}
+
+export function durationToISODurationString(durationTime: Duration) {
+ const {
+ years = 0,
+ months = 0,
+ days = 0,
+ hours = 0,
+ minutes = 0,
+ seconds = 0,
+ } = durationTime;
+
+ // Construct the duration string part by part
+ const yearsPart = years > 0 ? `${years}Y` : '';
+ const monthsPart = months > 0 ? `${months}M` : '';
+ const daysPart = days > 0 ? `${days}D` : '';
+ const timePart = hours > 0 || minutes > 0 || seconds > 0 ? 'T' : '';
+ const hoursPart = hours > 0 ? `${hours}H` : '';
+ const minutesPart = minutes > 0 ? `${minutes}M` : '';
+ const secondsPart = seconds > 0 ? `${seconds}S` : '';
+
+ // Combine all parts to form the full ISO 8601 duration string
+ const isoDuration = `P${yearsPart}${monthsPart}${daysPart}${timePart}${hoursPart}${minutesPart}${secondsPart}`;
+ return isoDuration;
+}
+
+export function convertDurationStringToISODuration(durationTime: string) {
+ return durationToISODurationString(durationStringToDuration(durationTime));
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts
new file mode 100644
index 000000000000..d084ccade0d8
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/404.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/404.tsx
new file mode 100644
index 000000000000..00c7063ace3e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/pages/404.tsx
@@ -0,0 +1,3 @@
+export default function NotFound() {
+ return 404 - route not found notebooks
;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/_app.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/_app.tsx
new file mode 100644
index 000000000000..f26ee200fb1e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/pages/_app.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+export default function App({ children }: { children: React.ReactNode }) {
+ useTranslation('pci-ai-notebooks');
+
+ return <>{children}>;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/home/index.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/home/index.tsx
new file mode 100644
index 000000000000..0b196b132ca5
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/pages/home/index.tsx
@@ -0,0 +1,7 @@
+export default function Home() {
+ return (
+ <>
+ AI Notebooks
+ >
+ );
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/layout.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/layout.tsx
new file mode 100644
index 000000000000..e6d8253f01ba
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/pages/layout.tsx
@@ -0,0 +1,63 @@
+import { Outlet, redirect, useLocation, useParams } from 'react-router-dom';
+import { useRouting, useShell } from '@ovh-ux/manager-react-shell-client';
+
+import { useEffect } from 'react';
+import queryClient from '@/query.client';
+import { getProject } from '@/data/api/projects';
+import { useLoadingIndicatorContext } from '@/contexts/loadingIndicatorContext';
+
+interface DashboardLayoutProps {
+ params: {
+ projectId: string;
+ };
+ request: Request;
+}
+// try to fetch the service data, redirect to service page if it fails
+export const Loader = async ({ params }: DashboardLayoutProps) => {
+ console.log('inLoader');
+ const { projectId } = params;
+ return queryClient
+ .fetchQuery({
+ queryKey: ['projectId', projectId],
+ queryFn: () => getProject(projectId),
+ })
+ .then(
+ () => null,
+ () => redirect(`/pci/projects`),
+ );
+};
+
+function RoutingSynchronisation() {
+ const { setLoading } = useLoadingIndicatorContext();
+ const location = useLocation();
+ const routing = useRouting();
+ const shell = useShell();
+ useEffect(() => {
+ routing.stopListenForHashChange();
+ }, []);
+ useEffect(() => {
+ shell.ux.hidePreloader();
+ setLoading(false);
+ routing.onHashChange();
+ }, [location]);
+ return <>>;
+}
+
+export function breadcrumb() {
+ return 'ai-dashboard';
+}
+
+export function useNotebooksData() {
+ const { projectId } = useParams();
+ return { projectId };
+}
+
+export default function NotebooksLayout() {
+ console.log('layout');
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/query.client.ts b/packages/manager/apps/pci-ai-notebooks/src/query.client.ts
new file mode 100644
index 000000000000..e836e5b346d5
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/query.client.ts
@@ -0,0 +1,11 @@
+import { QueryClient } from '@tanstack/react-query';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60_000,
+ },
+ },
+});
+
+export default queryClient;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/react-app-env.d.ts b/packages/manager/apps/pci-ai-notebooks/src/react-app-env.d.ts
new file mode 100644
index 000000000000..a666151b9b48
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/react-app-env.d.ts
@@ -0,0 +1,4 @@
+declare module '*.png';
+declare module '*.svg';
+declare module '*.jpeg';
+declare module '*.jpg';
diff --git a/packages/manager/apps/pci-ai-notebooks/src/routes/Router.tsx b/packages/manager/apps/pci-ai-notebooks/src/routes/Router.tsx
new file mode 100644
index 000000000000..81afdd081376
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/routes/Router.tsx
@@ -0,0 +1,8 @@
+import { RouterProvider, createHashRouter } from 'react-router-dom';
+import appRoutes from '@/routes/routes';
+
+const router = createHashRouter(appRoutes);
+
+const Router = () => ;
+
+export default Router;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx b/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx
new file mode 100644
index 000000000000..f0163575db76
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import NotFound from '@/pages/404';
+import ErrorBoundary from '@/components/errorBoundary';
+
+const lazyRouteConfig = (importFn: CallableFunction) => {
+ return {
+ lazy: async () => {
+ const { default: moduleDefault, ...moduleExports } = await importFn();
+ return {
+ Component: moduleDefault,
+ loader: moduleExports?.Loader,
+ ErrorBoundary,
+ handle: {
+ breadcrumb: moduleExports.breadcrumb,
+ },
+ ...moduleExports,
+ };
+ },
+ };
+};
+
+export const COMMON_PATH = '/pci/projects';
+
+export default [
+ {
+ path: '/pci/projects/:projectId/ai/notebooks',
+ ...lazyRouteConfig(() => import('@/pages/layout')),
+ children: [
+ {
+ path: '',
+ ...lazyRouteConfig(() => import('@/pages/home')),
+ },
+ ],
+ },
+ {
+ path: '*',
+ element: ,
+ },
+];
diff --git a/packages/manager/apps/pci-ai-notebooks/src/types/catalog.ts b/packages/manager/apps/pci-ai-notebooks/src/types/catalog.ts
new file mode 100644
index 000000000000..48657b51753b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/types/catalog.ts
@@ -0,0 +1,693 @@
+/* eslint-disable @typescript-eslint/no-namespace */
+export namespace order {
+ /** Currency code */
+ export enum CurrencyCodeEnum {
+ 'AUD' = 'AUD',
+ 'CAD' = 'CAD',
+ 'CZK' = 'CZK',
+ 'EUR' = 'EUR',
+ 'GBP' = 'GBP',
+ 'INR' = 'INR',
+ 'LTL' = 'LTL',
+ 'MAD' = 'MAD',
+ 'N/A' = 'N/A',
+ 'PLN' = 'PLN',
+ 'SGD' = 'SGD',
+ 'TND' = 'TND',
+ 'USD' = 'USD',
+ 'XOF' = 'XOF',
+ 'points' = 'points',
+ }
+ export namespace cart {
+ /** Type of a pricing */
+ export enum GenericProductPricingTypeEnum {
+ 'consumption' = 'consumption',
+ 'purchase' = 'purchase',
+ 'rental' = 'rental',
+ }
+ /** Capacity of a pricing (type) */
+ export enum GenericProductPricingCapacitiesEnum {
+ 'consumption' = 'consumption',
+ 'detach' = 'detach',
+ 'downgrade' = 'downgrade',
+ 'dynamic' = 'dynamic',
+ 'installation' = 'installation',
+ 'renew' = 'renew',
+ 'upgrade' = 'upgrade',
+ }
+ /** Unit corresponding to a duration range */
+ export enum DurationUnitEnum {
+ 'day' = 'day',
+ 'hour' = 'hour',
+ 'month' = 'month',
+ 'none' = 'none',
+ }
+ /** Strategy of a Pricing */
+ export enum GenericProductPricingStrategyEnum {
+ 'stairstep' = 'stairstep',
+ 'tiered' = 'tiered',
+ 'volume' = 'volume',
+ }
+ }
+ export namespace publicOrder {
+ export namespace EngagementConfiguration {
+ /** Strategy applicable at the end of the Engagement */
+ export enum EndStrategyEnum {
+ 'CANCEL_SERVICE' = 'CANCEL_SERVICE',
+ 'REACTIVATE_ENGAGEMENT' = 'REACTIVATE_ENGAGEMENT',
+ 'STOP_ENGAGEMENT_FALLBACK_DEFAULT_PRICE' = 'STOP_ENGAGEMENT_FALLBACK_DEFAULT_PRICE',
+ 'STOP_ENGAGEMENT_KEEP_PRICE' = 'STOP_ENGAGEMENT_KEEP_PRICE',
+ }
+ /** Engagement's type, either fully pre-paid (upfront) or periodically paid up to engagement duration (periodic) */
+ export enum TypeEnum {
+ 'periodic' = 'periodic',
+ 'upfront' = 'upfront',
+ }
+ }
+ /** Describes a Catalog inside a Subsidiary */
+ export interface Catalog {
+ /** List of addons of the catalog */
+ addons: order.publicOrder.Plan[];
+ /** Identifier of the catalog */
+ catalogId: number;
+ /** Subsidiary specific information */
+ locale: order.publicOrder.Locale;
+ /** List of plan families of the catalog */
+ planFamilies: order.publicOrder.PlanFamily[];
+ /** List of main plans of the catalog */
+ plans: order.publicOrder.Plan[];
+ /** List of products of the catalog */
+ products: order.publicOrder.Product[];
+ }
+
+ /** Describes a PlanFamily for a Catalog */
+ export interface PlanFamily {
+ /** Family name */
+ name: string;
+ }
+ /** Describes specifics for a given Subsidiary */
+ export interface Locale {
+ /** Currency used by the Subsidiary */
+ currencyCode: order.CurrencyCodeEnum;
+ /** Current Subsidiary */
+ subsidiary: nichandle.OvhSubsidiaryEnum;
+ /** Default VAT rate used by the Subsidiary */
+ taxRate: number;
+ }
+ /** Describes a Product attached to a Commercial offer */
+ export interface Product {
+ /** Product blobs */
+ blobs?: order.publicOrder.ProductBlob;
+ /** List of possible Configurations for this Commercial offer */
+ configurations?: order.publicOrder.Configuration[];
+ /** Description of the Product */
+ description: string;
+ /** Identifier of the Product */
+ name: string;
+ }
+ /** Describes a Commercial offer inside a Catalog */
+ export interface Plan {
+ /** Addon families for this offer */
+ addonFamilies: order.publicOrder.AddonFamily[];
+ /** Blobs */
+ blobs?: order.publicOrder.ProductBlob;
+ /** List of possible Configurations for this Commercial offer */
+ configurations: order.publicOrder.Configuration[];
+ /** Configuration when pricing type is consumption */
+ consumptionConfiguration?: order.publicOrder.ConsumptionConfiguration;
+ /** Name of the family this Commercial offer belongs to */
+ family?: string;
+ /** Commercial offer description */
+ invoiceName: string;
+ /** Commercial offer identifier */
+ planCode: string;
+ /** Type of Pricing used by this Commercial offer */
+ pricingType: order.cart.GenericProductPricingTypeEnum;
+ /** List of possible Pricings for this Commercial offer */
+ pricings: order.publicOrder.Pricing[];
+ /** Identifier of the Product linked to this Commercial offer */
+ product: string;
+ }
+ /** Describes an Addon family for a Commercial offer */
+ export interface AddonFamily {
+ /** List of Commercial offers that can be ordered as an Addon of the current Commerical offer for the current Family */
+ addons?: string[];
+ /** Default Commercial offer that can be ordered as an Addon of the current Commercial offer for the current Family */
+ default?: string;
+ /** Whether this Addon family is exclusive and can be ordered only once for the main Commercial offer */
+ exclusive?: boolean;
+ /** Whether this Addon family is mandatory */
+ mandatory?: boolean;
+ /** Family name */
+ name: string;
+ }
+ /** Describes a Blob */
+ export interface ProductBlob {
+ /** Commercial information for Dedicated Server Product */
+ commercial?: order.publicOrder.ProductBlobCommercial;
+ /** Marketing information for VPS Product */
+ marketing?: order.publicOrder.ProductBlobMarketing;
+ /** Meta blobs for VPS Product */
+ meta?: order.publicOrder.ProductBlobMeta;
+ /** Tags */
+ tags?: string[];
+ /** Technical information for Dedicated Server Product */
+ technical?: order.publicOrder.ProductBlobTechnical;
+ /** Value for meta blobs */
+ value?: string;
+ }
+ /** Describes a Commercial blob */
+ export interface ProductBlobCommercial {
+ /** Brick */
+ brick?: string;
+ /** Brick subtype */
+ brickSubtype?: string;
+ /** Connection */
+ connection?: order.publicOrder.ProductBlobConnection;
+ /** Features */
+ features?: order.publicOrder.ProductBlobCommercialFeatures[];
+ /** Line */
+ line?: string;
+ /** Name */
+ name?: string;
+ /** Price */
+ price?: order.publicOrder.ProductBlobCommercialPrice;
+ /** Range */
+ range?: string;
+ }
+ /** Describes Features for a commercial blob */
+ export interface ProductBlobCommercialFeatures {
+ /** Name */
+ name?: string;
+ /** Value */
+ value?: string;
+ }
+ /** Describes a Price for a commercial blob */
+ export interface ProductBlobCommercialPrice {
+ /** Display */
+ display?: order.publicOrder.ProductBlobCommercialPriceDisplay;
+ /** Interval */
+ interval?: string;
+ /** Precision */
+ precision?: number;
+ /** Unit */
+ unit?: string;
+ }
+ /** Describes a Display a price */
+ export interface ProductBlobCommercialPriceDisplay {
+ /** Value */
+ value: string;
+ }
+ /** Describes a Connection for a blob for a Dedicated Server */
+ export interface ProductBlobConnection {
+ /** Clients */
+ clients: order.publicOrder.ProductBlobConnectionClients;
+ /** Total */
+ total: number;
+ }
+ /** Describes Clients for a Connection for a blob for a Dedicated Server */
+ export interface ProductBlobConnectionClients {
+ /** Concurrency */
+ concurrency: number;
+ /** Number */
+ number: number;
+ }
+ /** Describes a Marketing blob */
+ export interface ProductBlobMarketing {
+ /** Marketing content information for VPS Product */
+ content: order.publicOrder.ProductBlobMarketingContent[];
+ }
+ /** Describes a Content for a Marketing blob */
+ export interface ProductBlobMarketingContent {
+ /** Key */
+ key: string;
+ /** Value */
+ value: string;
+ }
+ /** Describes a Meta blob */
+ export interface ProductBlobMeta {
+ /** Configurations */
+ configurations: order.publicOrder.ProductBlobMetaConfigurations[];
+ }
+ /** Describes a Configuration for a meta blob */
+ export interface ProductBlobMetaConfigurations {
+ /** Name */
+ name: string;
+ /** Values */
+ values: order.publicOrder.ProductBlobMetaConfigurationsValues[];
+ }
+ /** Describes a Values configuration for a meta blob */
+ export interface ProductBlobMetaConfigurationsValues {
+ /** Blobs */
+ blobs: order.publicOrder.ProductBlob;
+ /** Value */
+ value: string;
+ }
+ /** Describes a Technical Blob */
+ export interface ProductBlobTechnical {
+ /** Network informations */
+ bandwidth?: order.publicOrder.ProductBlobTechnicalNetwork;
+ /** Connection */
+ connection?: order.publicOrder.ProductBlobConnection;
+ /** Connection per seconds */
+ connectionPerSeconds?: order.publicOrder.ProductBlobTechnicalPerSeconds;
+ /** CPU informations */
+ cpu?: order.publicOrder.ProductBlobTechnicalCPU;
+ /** Datacenter */
+ datacenter?: order.publicOrder.ProductBlobTechnicalDatacenter;
+ /** Ephemeral local storage */
+ ephemeralLocalStorage?: order.publicOrder.ProductBlobTechnicalEphemeralStorage;
+ /** GPU informations */
+ gpu?: order.publicOrder.ProductBlobTechnicalGPU;
+ /** License informations */
+ license?: order.publicOrder.ProductBlobTechnicalLicense;
+ /** Memory informations */
+ memory?: order.publicOrder.ProductBlobTechnicalMemory;
+ /** Name */
+ name?: string;
+ /** Nodes */
+ nodes?: order.publicOrder.ProductBlobTechnicalNodes;
+ /** NVME */
+ nvme?: order.publicOrder.ProductBlobTechnicalNvme;
+ /** OS */
+ os?: order.publicOrder.ProductBlobTechnicalOS;
+ /** Connection per seconds */
+ requestPerSeconds?: order.publicOrder.ProductBlobTechnicalPerSeconds;
+ /** Hardware informations */
+ server?: order.publicOrder.ProductBlobTechnicalServer;
+ /** Disks informations */
+ storage?: order.publicOrder.ProductBlobTechnicalStorage;
+ /** Throughput */
+ throughput?: order.publicOrder.ProductBlobTechnicalThroughput;
+ /** Virtualization */
+ virtualization?: order.publicOrder.ProductBlobTechnicalVirtualization;
+ /** Volume */
+ volume?: order.publicOrder.ProductBlobTechnicalVolume;
+ /** vRack informations */
+ vrack?: order.publicOrder.ProductBlobTechnicalNetwork;
+ }
+ /** Describes a CPU for a technical blob */
+ export interface ProductBlobTechnicalCPU {
+ /** CPU Boost */
+ boost?: number;
+ /** CPU Brand */
+ brand?: string;
+ /** Number of cores */
+ cores?: number;
+ /** Customizable */
+ customizable?: boolean;
+ /** Frequency of CPU in GHz */
+ frequency?: number;
+ /** Displayable name */
+ model?: string;
+ /** Number of CPU */
+ number?: number;
+ /** CPU score */
+ score?: number;
+ /** Number of threads */
+ threads?: number;
+ }
+ /** Describes a Datacenter for a technical Blob */
+ export interface ProductBlobTechnicalDatacenter {
+ /** City */
+ city?: string;
+ /** Country */
+ country?: string;
+ /** Country code */
+ countryCode?: nichandle.OvhSubsidiaryEnum;
+ /** Name */
+ name?: string;
+ /** Region */
+ region?: string;
+ }
+ /** Describes a Disk for a technical blob */
+ export interface ProductBlobTechnicalDisk {
+ /** Disk capacity in Gb */
+ capacity: number;
+ /** Disk interface */
+ interface?: string;
+ /** Iops */
+ iops?: number;
+ /** Number of disks */
+ number?: number;
+ /** Size unit */
+ sizeUnit?: string;
+ /** Disk specs */
+ specs?: string;
+ /** Disk technology */
+ technology?: string;
+ /** Usage informations */
+ usage?: string;
+ }
+ /** Describes an Ephemeral Storage for technical blob */
+ export interface ProductBlobTechnicalEphemeralStorage {
+ /** Disk properties */
+ disks?: order.publicOrder.ProductBlobTechnicalDisk[];
+ }
+ /** Describes a Frame for a technical blob */
+ export interface ProductBlobTechnicalFrame {
+ /** Dual power supply */
+ dualPowerSupply: boolean;
+ /** Frame model */
+ model: string;
+ /** Frame size */
+ size: string;
+ }
+ /** Describes a GPU for a technical blob */
+ export interface ProductBlobTechnicalGPU {
+ /** GPU brand */
+ brand?: string;
+ /** GPU memory size */
+ memory: order.publicOrder.ProductBlobTechnicalMemory;
+ /** GPU model */
+ model?: string;
+ /** GPU number */
+ number?: number;
+ /** GPU performance */
+ performance?: number;
+ }
+ /** Describes a License for a technical Blob */
+ export interface ProductBlobTechnicalLicense {
+ /** Application */
+ application?: string;
+ /** Cores informations */
+ cores?: order.publicOrder.ProductBlobTechnicalLicenseCores;
+ /** CPU */
+ cpu?: order.publicOrder.ProductBlobTechnicalCPU;
+ /** Network informations */
+ distribution?: string;
+ /** Edition informations */
+ edition?: string;
+ /** Family */
+ family?: string;
+ /** Feature */
+ feature?: string;
+ /** Flavor informations */
+ flavor?: string;
+ /** Images informations */
+ images?: string[];
+ /** Number of account */
+ nbOfAccount?: number;
+ /** Package */
+ package?: string;
+ /** Version informations */
+ version?: string;
+ }
+ /** Describes license cores for a technical blob */
+ export interface ProductBlobTechnicalLicenseCores {
+ /** Number of cores */
+ number: number;
+ /** Total of cores */
+ total?: number;
+ }
+ /** Describes a Memory technical Blob */
+ export interface ProductBlobTechnicalMemory {
+ /** Customizable */
+ customizable?: boolean;
+ /** ECC */
+ ecc?: boolean;
+ /** RAM Frequency */
+ frequency?: number;
+ /** Interface */
+ interface?: string;
+ /** RAM Type (DDRx...) */
+ ramType?: string;
+ /** Size of the RAM in Gb */
+ size: number;
+ /** Size unit */
+ sizeUnit?: string;
+ }
+ /** Describes a Network technical Blob */
+ export interface ProductBlobTechnicalNetwork {
+ /** Network burst */
+ burst?: number;
+ /** Network capacity */
+ capacity?: number;
+ /** Guaranteed Network */
+ guaranteed?: boolean;
+ /** Network interfaces */
+ interfaces?: number;
+ /** Is max? */
+ isMax?: boolean;
+ /** Network level */
+ level?: number;
+ /** Network limit */
+ limit?: number;
+ /** Shared */
+ shared?: boolean;
+ /** Traffic */
+ traffic?: number;
+ /** Unlimited */
+ unlimited?: boolean;
+ }
+ /** Describes a Node for technical blob */
+ export interface ProductBlobTechnicalNodes {
+ /** Number of nodes */
+ number: number;
+ }
+ /** Describes a NVME for technical blob */
+ export interface ProductBlobTechnicalNvme {
+ /** Disk properties */
+ disks?: order.publicOrder.ProductBlobTechnicalDisk[];
+ }
+ /** Describes an OS for a technical blob */
+ export interface ProductBlobTechnicalOS {
+ /** Distribution */
+ distribution?: string;
+ /** Edition */
+ edition?: string;
+ /** Family */
+ family?: string;
+ /** Version */
+ version?: string;
+ }
+ /** Describes a connection or request per seconds for a technical blob */
+ export interface ProductBlobTechnicalPerSeconds {
+ /** Total */
+ total: number;
+ /** Unit */
+ unit?: string;
+ }
+ /** Describes a Raid for a technical blob */
+ export interface ProductBlobTechnicalRaid {
+ /** Card size */
+ cardModel?: string;
+ /** Card size */
+ cardSize?: string;
+ /** Type */
+ type: string;
+ }
+ /** Describes some technicals informations for a technical blob */
+ export interface ProductBlobTechnicalServer {
+ /** CPU properties */
+ cpu: order.publicOrder.ProductBlobTechnicalCPU;
+ /** Frame properties */
+ frame: order.publicOrder.ProductBlobTechnicalFrame;
+ /** Network */
+ network?: order.publicOrder.ProductBlobTechnicalNetwork;
+ /** Dedicated server series */
+ range: string;
+ /** Services properties */
+ services: order.publicOrder.ProductBlobTechnicalServices;
+ }
+ /** Describes some technicals informations */
+ export interface ProductBlobTechnicalServices {
+ /** Anti DDOS */
+ antiddos: string;
+ /** Included backup */
+ includedBackup: number;
+ /** SLA */
+ sla: number;
+ }
+ /** Describes a Storage technical Blob */
+ export interface ProductBlobTechnicalStorage {
+ /** Disk properties */
+ disks?: order.publicOrder.ProductBlobTechnicalDisk[];
+ /** Hot Swap */
+ hotSwap?: boolean;
+ /** Raid */
+ raid?: string;
+ /** Raid details */
+ raidDetails?: order.publicOrder.ProductBlobTechnicalRaid;
+ }
+ /** Describes a Throughput for a technical blob */
+ export interface ProductBlobTechnicalThroughput {
+ /** Level */
+ level: number;
+ }
+ /** Describes a Virtualization for a Technical Blob */
+ export interface ProductBlobTechnicalVirtualization {
+ /** Hypervisor */
+ hypervisor?: string;
+ }
+ /** Describes a Volume for a technichal blob */
+ export interface ProductBlobTechnicalVolume {
+ /** Capacity */
+ capacity: order.publicOrder.ProductBlobTechnicalVolumeCapacity;
+ /** CPU informations */
+ iops: order.publicOrder.ProductBlobTechnicalVolumeIops;
+ }
+ /** Describes a Capacity for a Volume for a technichal blob */
+ export interface ProductBlobTechnicalVolumeCapacity {
+ /** Max */
+ max: number;
+ }
+ /** Describes a Iops for a Volume for a technichal blob */
+ export interface ProductBlobTechnicalVolumeIops {
+ /** Guaranteed */
+ guaranteed: boolean;
+ /** Level */
+ level: number;
+ }
+ /** Describes the Configuration for a Commercial offer */
+ export interface Configuration {
+ /** Whether the value of this Configuration is custom */
+ isCustom: boolean;
+ /** Whether this Configuration is mandatory */
+ isMandatory: boolean;
+ /** Identifier of the Configuration */
+ name: string;
+ /** Possible values for this Configuration, if not custom */
+ values?: string[];
+ }
+ /** Describes consumption configuration for a Plan */
+ export interface ConsumptionConfiguration {
+ /** Consumption billing strategy */
+ billingStrategy: order.publicOrder.BillingStrategyEnum;
+ /** Consumption ping end policy used at end of usage */
+ pingEndPolicy?: order.publicOrder.PingEndPolicyEnum;
+ /** Consumption prorata unit */
+ prorataUnit: order.publicOrder.ProrataUnitEnum;
+ }
+ /** Enum values for Billing Strategy */
+ export enum BillingStrategyEnum {
+ 'custom' = 'custom',
+ 'diff' = 'diff',
+ 'max' = 'max',
+ 'max_retain' = 'max_retain',
+ 'ping' = 'ping',
+ 'sum' = 'sum',
+ }
+ /** Enum values for Ping End Policy */
+ export enum PingEndPolicyEnum {
+ 'full' = 'full',
+ 'prorata' = 'prorata',
+ }
+ /** Enum values for Prorata Unit */
+ export enum ProrataUnitEnum {
+ 'day' = 'day',
+ 'hour' = 'hour',
+ 'month' = 'month',
+ }
+ /** Describes a Pricing for a Commercial offer */
+ export interface Pricing {
+ /** Capacities of the Pricing, describes what the Pricing can be used for */
+ capacities: order.cart.GenericProductPricingCapacitiesEnum[];
+ /** Engagement period */
+ commitment: number;
+ /** Pricing description */
+ description: string;
+ /** Engagement Configuration */
+ engagementConfiguration?: order.publicOrder.EngagementConfiguration;
+ /** Length of the interval */
+ interval: number;
+ /** Unit of the interval */
+ intervalUnit: order.cart.DurationUnitEnum;
+ /** Pricing mode */
+ mode: string;
+ /** Pricing must be completed */
+ mustBeCompleted: boolean;
+ /** Phase for the Pricing */
+ phase: number;
+ /** Price, in micro-cents */
+ price: number;
+ /** Promotions */
+ promotions?: order.publicOrder.Promotion[];
+ /** Describes how many times the Commercial offer can be added to the Cart */
+ quantity: order.publicOrder.PricingMinMax;
+ /** Describes how many times the interval can be repeated */
+ repeat: order.publicOrder.PricingMinMax;
+ /** Pricing strategy */
+ strategy: order.cart.GenericProductPricingStrategyEnum;
+ /** Tax that can be applied, in micro-cents */
+ tax: number;
+ /** Pricing type */
+ type: order.cart.GenericProductPricingTypeEnum;
+ }
+ /** Describes minimal and maximal values for a Pricing */
+ export interface PricingMinMax {
+ /** Maximal value */
+ max?: number;
+ /** Minimal value */
+ min: number;
+ }
+ /** Configuration of an engagement triggered by a given pricing */
+ export interface EngagementConfiguration {
+ /** Default action executed once the engagement is fully consumed */
+ defaultEndAction: order.publicOrder.EngagementConfiguration.EndStrategyEnum;
+ /** Engagement's duration */
+ duration: string;
+ /** Engagement type, either fully pre-paid (upfront) or periodically paid up to engagement duration (periodic) */
+ type: order.publicOrder.EngagementConfiguration.TypeEnum;
+ }
+ /** Describes a Promotion inside a Catalog */
+ export interface Promotion {
+ /** Promotion description */
+ description: string;
+ /** Promotion discount */
+ discount: order.publicOrder.PromotionDiscountTotal;
+ /** Promotion duration */
+ duration?: number;
+ /** Promotion end date using rfc3339 */
+ endDate?: string;
+ /** Is the global quantity of the promotion limited? */
+ isGlobalQuantityLimited: boolean;
+ /** Promotion name */
+ name: string;
+ /** Promotion quantity */
+ quantity?: number;
+ /** Promotion start date using rfc3339 */
+ startDate: string;
+ /** Promotion total */
+ total: order.publicOrder.PromotionDiscountTotal;
+ /** Promotion type */
+ type: order.ReductionTypeEnum;
+ /** Promotion value */
+ value: number;
+ }
+ /** Describes a Promotion discount or total inside a Catalog */
+ export interface PromotionDiscountTotal {
+ /** Tax */
+ tax: number;
+ /** Value */
+ value: number;
+ }
+ }
+ /** Type of reduction */
+ export enum ReductionTypeEnum {
+ 'fixed_amount' = 'fixed_amount',
+ 'forced_amount' = 'forced_amount',
+ 'percentage' = 'percentage',
+ }
+}
+export namespace nichandle {
+ /** OVH subsidiaries */
+ export enum OvhSubsidiaryEnum {
+ 'CZ' = 'CZ',
+ 'DE' = 'DE',
+ 'ES' = 'ES',
+ 'EU' = 'EU',
+ 'FI' = 'FI',
+ 'FR' = 'FR',
+ 'GB' = 'GB',
+ 'IE' = 'IE',
+ 'IT' = 'IT',
+ 'LT' = 'LT',
+ 'MA' = 'MA',
+ 'NL' = 'NL',
+ 'PL' = 'PL',
+ 'PT' = 'PT',
+ 'SN' = 'SN',
+ 'TN' = 'TN',
+ }
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/types/project.ts b/packages/manager/apps/pci-ai-notebooks/src/types/project.ts
new file mode 100644
index 000000000000..5f0975113023
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/types/project.ts
@@ -0,0 +1,14 @@
+export enum PciProjectPlanCode {
+ DISCOVERY = 'project.discovery',
+ STANDARD = 'project.2018',
+}
+export interface PciProject {
+ access: string;
+ creationDate: string;
+ description: string;
+ projectName: string;
+ project_id: string;
+ status: string;
+ unleash: boolean;
+ planCode: PciProjectPlanCode;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/vite-hmr.ts b/packages/manager/apps/pci-ai-notebooks/src/vite-hmr.ts
new file mode 100644
index 000000000000..473d87630039
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/vite-hmr.ts
@@ -0,0 +1,5 @@
+if (import.meta.hot) {
+ import.meta.hot.on('iframe-reload', () => {
+ window.location.reload();
+ });
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/tailwind.config.ts b/packages/manager/apps/pci-ai-notebooks/tailwind.config.ts
new file mode 100644
index 000000000000..02171b216088
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/tailwind.config.ts
@@ -0,0 +1,304 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1400px',
+ },
+ },
+ extend: {
+ fontSize: {
+ 'ods-heading-800': 'var(--ods-typography-heading-800-font-size)',
+ 'ods-heading-700': 'var(--ods-typography-heading-700-font-size)',
+ 'ods-heading-600': 'var(--ods-typography-heading-600-font-size)',
+ 'ods-heading-500': 'var(--ods-typography-heading-500-font-size)',
+ 'ods-heading-400': 'var(--ods-typography-heading-400-font-size)',
+ 'ods-heading-300': 'var(--ods-typography-heading-300-font-size)',
+ 'ods-heading-200': 'var(--ods-typography-heading-200-font-size)',
+ 'ods-heading-100': 'var(--ods-typography-heading-100-font-size)',
+ 'ods-800': 'var(--ods-typography-body-800-font-size)',
+ 'ods-700': 'var(--ods-typography-body-700-font-size)',
+ 'ods-600': 'var(--ods-typography-body-600-font-size)',
+ 'ods-500': 'var(--ods-typography-body-500-font-size)',
+ 'ods-400': 'var(--ods-typography-body-400-font-size)',
+ 'ods-300': 'var(--ods-typography-body-300-font-size)',
+ 'ods-200': 'var(--ods-typography-body-200-font-size)',
+ 'ods-100': 'var(--ods-typography-body-100-font-size)',
+ },
+ fontWeight: {
+ 'ods-heading-800': 'var(--ods-typography-heading-800-font-weight)',
+ 'ods-heading-700': 'var(--ods-typography-heading-700-font-weight)',
+ 'ods-heading-600': 'var(--ods-typography-heading-600-font-weight)',
+ 'ods-heading-500': 'var(--ods-typography-heading-500-font-weight)',
+ 'ods-heading-400': 'var(--ods-typography-heading-400-font-weight)',
+ 'ods-heading-300': 'var(--ods-typography-heading-300-font-weight)',
+ 'ods-heading-200': 'var(--ods-typography-heading-200-font-weight)',
+ 'ods-heading-100': 'var(--ods-typography-heading-100-font-weight)',
+ 'ods-800': 'var(--ods-typography-body-800-font-weight)',
+ 'ods-700': 'var(--ods-typography-body-700-font-weight)',
+ 'ods-600': 'var(--ods-typography-body-600-font-weight)',
+ 'ods-500': 'var(--ods-typography-body-500-font-weight)',
+ 'ods-400': 'var(--ods-typography-body-400-font-weight)',
+ 'ods-300': 'var(--ods-typography-body-300-font-weight)',
+ 'ods-200': 'var(--ods-typography-body-200-font-weight)',
+ 'ods-100': 'var(--ods-typography-body-100-font-weight)',
+ },
+ lineHeight: {
+ 'ods-heading-800': 'var(--ods-typography-heading-800-line-height)',
+ 'ods-heading-700': 'var(--ods-typography-heading-700-line-height)',
+ 'ods-heading-600': 'var(--ods-typography-heading-600-line-height)',
+ 'ods-heading-500': 'var(--ods-typography-heading-500-line-height)',
+ 'ods-heading-400': 'var(--ods-typography-heading-400-line-height)',
+ 'ods-heading-300': 'var(--ods-typography-heading-300-line-height)',
+ 'ods-heading-200': 'var(--ods-typography-heading-200-line-height)',
+ 'ods-heading-100': 'var(--ods-typography-heading-100-line-height)',
+ 'ods-800': 'var(--ods-typography-body-800-line-height)',
+ 'ods-700': 'var(--ods-typography-body-700-line-height)',
+ 'ods-600': 'var(--ods-typography-body-600-line-height)',
+ 'ods-500': 'var(--ods-typography-body-500-line-height)',
+ 'ods-400': 'var(--ods-typography-body-400-line-height)',
+ 'ods-300': 'var(--ods-typography-body-300-line-height)',
+ 'ods-200': 'var(--ods-typography-body-200-line-height)',
+ 'ods-100': 'var(--ods-typography-body-100-line-height)',
+ },
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'var(--ods-color-text-500)',
+ primary: {
+ DEFAULT: 'var(--ods-color-primary-500)',
+ foreground: 'hsl(var(--primary-foreground))',
+ '000': 'var(--ods-color-primary-000)',
+ '000-contrasted': 'var(--ods-color-primary-000-contrasted)',
+ '50': '#f5feff',
+ '050-contrasted': 'var(--ods-color-primary-050-contrasted)',
+ '100': 'var(--ods-color-primary-100)',
+ '100-contrasted': 'var(--ods-color-primary-100-contrasted)',
+ '200': 'var(--ods-color-primary-200)',
+ '200-contrasted': 'var(--ods-color-primary-200-contrasted)',
+ '300': 'var(--ods-color-primary-300)',
+ '300-contrasted': 'var(--ods-color-primary-300-contrasted)',
+ '400': 'var(--ods-color-primary-400)',
+ '400-contrasted': 'var(--ods-color-primary-400-contrasted)',
+ '500': 'var(--ods-color-primary-500)',
+ '500-contrasted': 'var(--ods-color-primary-500-contrasted)',
+ '600': 'var(--ods-color-primary-600)',
+ '600-contrasted': 'var(--ods-color-primary-600-contrasted)',
+ '700': 'var(--ods-color-primary-700)',
+ '700-contrasted': 'var(--ods-color-primary-700-contrasted)',
+ '800': 'var(--ods-color-primary-800)',
+ '800-contrasted': 'var(--ods-color-primary-800-contrasted)',
+ '900': 'var(--ods-color-primary-900)',
+ '900-contrasted': 'var(--ods-color-primary-900-contrasted)',
+ },
+ gray: {
+ '000': 'var(--ods-color-gray-000)',
+ '000-contrasted': 'var(--ods-color-gray-000-contrasted)',
+ '050': 'var(--ods-color-gray-050)',
+ '050-contrasted': 'var(--ods-color-gray-050-contrasted)',
+ '100': 'var(--ods-color-gray-100)',
+ '100-contrasted': 'var(--ods-color-gray-100-contrasted)',
+ '200': 'var(--ods-color-gray-200)',
+ '200-contrasted': 'var(--ods-color-gray-200-contrasted)',
+ '300': 'var(--ods-color-gray-300)',
+ '300-contrasted': 'var(--ods-color-gray-300-contrasted)',
+ '400': 'var(--ods-color-gray-400)',
+ '400-contrasted': 'var(--ods-color-gray-400-contrasted)',
+ '500': 'var(--ods-color-gray-500)',
+ '500-contrasted': 'var(--ods-color-gray-500-contrasted)',
+ '600': 'var(--ods-color-gray-600)',
+ '600-contrasted': 'var(--ods-color-gray-600-contrasted)',
+ '700': 'var(--ods-color-gray-700)',
+ '700-contrasted': 'var(--ods-color-gray-700-contrasted)',
+ '800': 'var(--ods-color-gray-800)',
+ '800-contrasted': 'var(--ods-color-gray-800-contrasted)',
+ '900': 'var(--ods-color-gray-900)',
+ '900-contrasted': 'var(--ods-color-gray-900-contrasted)',
+ },
+ blue: {
+ '000': 'var(--ods-color-blue-000)',
+ '000-contrasted': 'var(--ods-color-blue-000-contrasted)',
+ '050': 'var(--ods-color-blue-050)',
+ '050-contrasted': 'var(--ods-color-blue-050-contrasted)',
+ '075': 'var(--ods-color-blue-075)',
+ '075-contrasted': 'var(--ods-color-blue-075-contrasted)',
+ '100': 'var(--ods-color-blue-100)',
+ '100-contrasted': 'var(--ods-color-blue-100-contrasted)',
+ '200': 'var(--ods-color-blue-200)',
+ '200-contrasted': 'var(--ods-color-blue-200-contrasted)',
+ '300': 'var(--ods-color-blue-300)',
+ '300-contrasted': 'var(--ods-color-blue-300-contrasted)',
+ '400': 'var(--ods-color-blue-400)',
+ '400-contrasted': 'var(--ods-color-blue-400-contrasted)',
+ '500': 'var(--ods-color-blue-500)',
+ '500-contrasted': 'var(--ods-color-blue-500-contrasted)',
+ '600': 'var(--ods-color-blue-600)',
+ '600-contrasted': 'var(--ods-color-blue-600-contrasted)',
+ '700': 'var(--ods-color-blue-700)',
+ '700-contrasted': 'var(--ods-color-blue-700-contrasted)',
+ '800': 'var(--ods-color-blue-800)',
+ '800-contrasted': 'var(--ods-color-blue-800-contrasted)',
+ '900': 'var(--ods-color-blue-900)',
+ '900-contrasted': 'var(--ods-color-blue-900-contrasted)',
+ },
+ 'gray-blue': {
+ '000': 'var(--ods-color-gray-blue-000)',
+ '000-contrasted': 'var(--ods-color-gray-blue-000-contrasted)',
+ '100': 'var(--ods-color-gray-blue-100)',
+ '100-contrasted': 'var(--ods-color-gray-blue-100-contrasted)',
+ '500': 'var(--ods-color-gray-blue-500)',
+ '500-contrasted': 'var(--ods-color-gray-blue-500-contrasted)',
+ '900': 'var(--ods-color-gray-blue-900)',
+ '900-contrasted': 'var(--ods-color-gray-blue-900-contrasted)',
+ },
+ red: {
+ '000': 'var(--ods-color-red-000)',
+ '000-contrasted': 'var(--ods-color-red-000-contrasted)',
+ '050': 'var(--ods-color-red-050)',
+ '050-contrasted': 'var(--ods-color-red-050-contrasted)',
+ '100': 'var(--ods-color-red-100)',
+ '100-contrasted': 'var(--ods-color-red-100-contrasted)',
+ '200': 'var(--ods-color-red-200)',
+ '200-contrasted': 'var(--ods-color-red-200-contrasted)',
+ '300': 'var(--ods-color-red-300)',
+ '300-contrasted': 'var(--ods-color-red-300-contrasted)',
+ '400': 'var(--ods-color-red-400)',
+ '400-contrasted': 'var(--ods-color-red-400-contrasted)',
+ '500': 'var(--ods-color-red-500)',
+ '500-contrasted': 'var(--ods-color-red-500-contrasted)',
+ '600': 'var(--ods-color-red-600)',
+ '600-contrasted': 'var(--ods-color-red-600-contrasted)',
+ '700': 'var(--ods-color-red-700)',
+ '700-contrasted': 'var(--ods-color-red-700-contrasted)',
+ '800': 'var(--ods-color-red-800)',
+ '800-contrasted': 'var(--ods-color-red-800-contrasted)',
+ '900': 'var(--ods-color-red-900)',
+ '900-contrasted': 'var(--ods-color-red-900-contrasted)',
+ },
+ orange: {
+ '000': 'var(--ods-color-orange-000)',
+ '000-contrasted': 'var(--ods-color-orange-000-contrasted)',
+ '050': 'var(--ods-color-orange-050)',
+ '050-contrasted': 'var(--ods-color-orange-050-contrasted)',
+ '100': 'var(--ods-color-orange-100)',
+ '100-contrasted': 'var(--ods-color-orange-100-contrasted)',
+ '200': 'var(--ods-color-orange-200)',
+ '200-contrasted': 'var(--ods-color-orange-200-contrasted)',
+ '300': 'var(--ods-color-orange-300)',
+ '300-contrasted': 'var(--ods-color-orange-300-contrasted)',
+ '400': 'var(--ods-color-orange-400)',
+ '400-contrasted': 'var(--ods-color-orange-400-contrasted)',
+ '500': 'var(--ods-color-orange-500)',
+ '500-contrasted': 'var(--ods-color-orange-500-contrasted)',
+ '600': 'var(--ods-color-orange-600)',
+ '600-contrasted': 'var(--ods-color-orange-600-contrasted)',
+ '700': 'var(--ods-color-orange-700)',
+ '700-contrasted': 'var(--ods-color-orange-700-contrasted)',
+ '800': 'var(--ods-color-orange-800)',
+ '800-contrasted': 'var(--ods-color-orange-800-contrasted)',
+ '900': 'var(--ods-color-orange-900)',
+ '900-contrasted': 'var(--ods-color-orange-900-contrasted)',
+ },
+ green: {
+ '000': 'var(--ods-color-green-000)',
+ '000-contrasted': 'var(--ods-color-green-000-contrasted)',
+ '050': 'var(--ods-color-green-050)',
+ '050-contrasted': 'var(--ods-color-green-050-contrasted)',
+ '100': 'var(--ods-color-green-100)',
+ '100-contrasted': 'var(--ods-color-green-100-contrasted)',
+ '200': 'var(--ods-color-green-200)',
+ '200-contrasted': 'var(--ods-color-green-200-contrasted)',
+ '300': 'var(--ods-color-green-300)',
+ '300-contrasted': 'var(--ods-color-green-300-contrasted)',
+ '400': 'var(--ods-color-green-400)',
+ '400-contrasted': 'var(--ods-color-green-400-contrasted)',
+ '500': 'var(--ods-color-green-500)',
+ '500-contrasted': 'var(--ods-color-green-500-contrasted)',
+ '600': 'var(--ods-color-green-600)',
+ '600-contrasted': 'var(--ods-color-green-600-contrasted)',
+ '700': 'var(--ods-color-green-700)',
+ '700-contrasted': 'var(--ods-color-green-700-contrasted)',
+ '800': 'var(--ods-color-green-800)',
+ '800-contrasted': 'var(--ods-color-green-800-contrasted)',
+ '900': 'var(--ods-color-green-900)',
+ '900-contrasted': 'var(--ods-color-green-900-contrasted)',
+ },
+ pink: {
+ '000': 'var(--ods-color-pink-000)',
+ '000-contrasted': 'var(--ods-color-pink-000-contrasted)',
+ '050': 'var(--ods-color-pink-050)',
+ '050-contrasted': 'var(--ods-color-pink-050-contrasted)',
+ '100': 'var(--ods-color-pink-100)',
+ '100-contrasted': 'var(--ods-color-pink-100-contrasted)',
+ '200': 'var(--ods-color-pink-200)',
+ '200-contrasted': 'var(--ods-color-pink-200-contrasted)',
+ '300': 'var(--ods-color-pink-300)',
+ '300-contrasted': 'var(--ods-color-pink-300-contrasted)',
+ '400': 'var(--ods-color-pink-400)',
+ '400-contrasted': 'var(--ods-color-pink-400-contrasted)',
+ '500': 'var(--ods-color-pink-500)',
+ '500-contrasted': 'var(--ods-color-pink-500-contrasted)',
+ '600': 'var(--ods-color-pink-600)',
+ '600-contrasted': 'var(--ods-color-pink-600-contrasted)',
+ '700': 'var(--ods-color-pink-700)',
+ '700-contrasted': 'var(--ods-color-pink-700-contrasted)',
+ '800': 'var(--ods-color-pink-800)',
+ '800-contrasted': 'var(--ods-color-pink-800-contrasted)',
+ '900': 'var(--ods-color-pink-900)',
+ '900-contrasted': 'var(--ods-color-pink-900-contrasted)',
+ },
+ secondary: {
+ DEFAULT: 'var(--ods-color-secondary-500))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'var(--ods-color-primary-500)',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'var(--ods-color-text-500)',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'var(--ods-color-text-500)',
+ },
+ },
+ borderRadius: {
+ sm: 'var(--ods-size-border-radius-01)',
+ DEFAULT: 'var(--ods-size-border-radius-01)',
+ md: 'var(--ods-size-border-radius-02)',
+ lg: 'var(--ods-size-border-radius-02)',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: { height: 0 },
+ to: { height: 'var(--radix-accordion-content-height)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height)' },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ },
+ },
+ },
+ // eslint-disable-next-line global-require
+ plugins: [require('tailwindcss-animate')],
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/tsconfig.build.json b/packages/manager/apps/pci-ai-notebooks/tsconfig.build.json
new file mode 100644
index 000000000000..4dc23fb9c4c5
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["src/__tests__"]
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/tsconfig.json b/packages/manager/apps/pci-ai-notebooks/tsconfig.json
new file mode 100644
index 000000000000..7c68745eb3f7
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "es2020", "ESNext"],
+ "noEmit": true,
+ "target": "ESNext",
+ "types": ["vite/client", "node", "jest", "@testing-library/jest-dom"],
+ "module": "esnext",
+ "moduleResolution": "Node",
+ "removeComments": true,
+ "outDir": "dist",
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "noImplicitAny": true,
+ "declaration": true,
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "baseUrl": ".",
+ // "strictNullChecks": true,
+ // "exactOptionalPropertyTypes": true,
+ "paths": {
+ "@/*": ["./src/*"],
+ "@playwright-helpers/*": ["../../../../playwright-helpers/*"]
+ }
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist", "types"]
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/vite.config.mjs b/packages/manager/apps/pci-ai-notebooks/vite.config.mjs
new file mode 100644
index 000000000000..5c317dcbab9d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/vite.config.mjs
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+import { getBaseConfig } from '@ovh-ux/manager-vite-config';
+
+const baseConfig = getBaseConfig({});
+
+export default defineConfig({
+ ...baseConfig,
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/vitest.config.js b/packages/manager/apps/pci-ai-notebooks/vitest.config.js
new file mode 100644
index 000000000000..617469048fdb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/vitest.config.js
@@ -0,0 +1,35 @@
+import path from 'path';
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./src/__tests__/setupTest.ts'],
+ coverage: {
+ include: ['src'],
+ exclude: [
+ 'src/interfaces',
+ 'src/__tests__',
+ 'src/vite-*.ts',
+ 'src/App.tsx',
+ 'src/i18n.ts',
+ 'src/index.tsx',
+ 'src/routes/routes.tsx',
+ 'src/routes/Router.tsx',
+ 'src/query.client.ts',
+ 'src/components/ui',
+ 'src/configuration',
+ ],
+ },
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ mainFields: ['module'],
+ },
+});