diff --git a/.eslintignore b/.eslintignore
index 1e490c52e9d5..d5b2a13f623d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -17,3 +17,4 @@ packages/manager/core/generator
packages/components/ovh-at-internet/src/ovh-at-internet.ts
packages/manager/apps/container
packages/manager/apps/pci-databases-analytics/src/components/ui
+packages/manager/apps/pci-ai-notebooks/src/components/ui
diff --git a/.prettierignore b/.prettierignore
index 587d59146e0e..d278b1fb052e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -6,4 +6,6 @@ loader
react
vue
packages/manager/apps/pci-databases-analytics/src/components/ui
-packages/manager/apps/pci-databases-analytics/src/lib/utils.ts
\ No newline at end of file
+packages/manager/apps/pci-databases-analytics/src/lib/utils.ts
+packages/manager/apps/pci-ai-notebooks/src/components/ui
+packages/manager/apps/pci-ai-notebooks/src/lib/utils.ts
diff --git a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts
index 9f9785a191df..9fffb392f1f4 100644
--- a/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts
+++ b/packages/manager/apps/container/src/container/legacy/server-sidebar/universe/public-cloud/pci-menu.ts
@@ -50,6 +50,7 @@ export const features = [
'ai-endpoints',
'key-management-service',
'pci-savings-plan',
+ 'pci-ai-notebooks'
];
export function getPciProjectMenu(
@@ -354,6 +355,7 @@ export function getPciProjectMenu(
'training',
'ai-apps',
'ai-endpoints',
+ 'pci-ai-notebooks',
)
) {
pciMenu.push({
@@ -373,6 +375,11 @@ export function getPciProjectMenu(
title: 'AI Notebooks',
href: getURL('public-cloud', `#/pci/projects/${projectId}/notebooks`),
},
+ isFeaturesAvailable('pci-ai-notebooks') && {
+ id: 'notebooks',
+ title: 'AI Notebooks React',
+ href: getURL('public-cloud', `#/pci/projects/${projectId}/ai/notebooks`),
+ },
isFeaturesAvailable('training') && {
id: 'training',
title: 'AI Training',
diff --git a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts
index 1543f86b98e6..510cd13296f8 100644
--- a/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts
+++ b/packages/manager/apps/container/src/container/nav-reshuffle/sidebar/navigation-tree/services/publicCloud.ts
@@ -496,7 +496,7 @@ pciNode.children = [
idAttr: 'pci-ai-link',
universe: PUBLICCLOUD_UNIVERSE_ID,
translation: 'sidebar_pci_ai',
- features: ['notebooks', 'ai-apps', 'training', 'ai-dashboard'],
+ features: ['notebooks', 'ai-apps', 'training', 'ai-dashboard', 'pci-ai-notebooks'],
forceVisibility: true,
children: [
{
@@ -525,6 +525,19 @@ pciNode.children = [
features: ['notebooks'],
forceVisibility: true,
},
+ {
+ id: 'pci-ai-notebooks',
+ idAttr: 'pci-ai-notebooks-link',
+ universe: PUBLICCLOUD_UNIVERSE_ID,
+ translation: 'sidebar_pci_ai_notebooks',
+ serviceType: 'CLOUD_PROJECT_AI_NOTEBOOK',
+ routing: {
+ application: 'public-cloud',
+ hash: '#/pci/projects/{projectId}/ai/notebooks',
+ },
+ features: ['pci-ai-notebooks'],
+ forceVisibility: true,
+ },
{
id: 'pci-ai-training',
idAttr: 'pci-ai-training-link',
diff --git a/packages/manager/apps/pci-ai-notebooks/.gitignore b/packages/manager/apps/pci-ai-notebooks/.gitignore
new file mode 100644
index 000000000000..32ef74fe6562
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/.gitignore
@@ -0,0 +1,2 @@
+coverage
+yarn-error.log
\ No newline at end of file
diff --git a/packages/manager/apps/pci-ai-notebooks/README.md b/packages/manager/apps/pci-ai-notebooks/README.md
new file mode 100644
index 000000000000..1c5efd0baccd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/README.md
@@ -0,0 +1,3 @@
+# @ovh-ux/manager-pci-ai-notebooks-app
+
+> pci-ai-notebooks project
diff --git a/packages/manager/apps/pci-ai-notebooks/components.json b/packages/manager/apps/pci-ai-notebooks/components.json
new file mode 100644
index 000000000000..5fa88201dfd4
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/global.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/index.html b/packages/manager/apps/pci-ai-notebooks/index.html
new file mode 100644
index 000000000000..5a905c968d6e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ OVHcloud
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/manager/apps/pci-ai-notebooks/package.json b/packages/manager/apps/pci-ai-notebooks/package.json
new file mode 100644
index 000000000000..172706c50c41
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/package.json
@@ -0,0 +1,100 @@
+{
+ "name": "@ovh-ux/manager-pci-ai-notebooks-app",
+ "version": "1.0.0",
+ "private": true,
+ "description": "pci-ai-notebooks",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ovh/manager.git",
+ "directory": "packages/manager/apps/pci-ai-notebooks"
+ },
+ "license": "BSD-3-Clause",
+ "author": "OVH SAS",
+ "type": "module",
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json && vite build",
+ "coverage": "vitest run --coverage",
+ "dev": "tsc && vite",
+ "start": "lerna exec --stream --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run build --if-present",
+ "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run dev --if-present",
+ "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-pci-ai-notebooks-app' --include-dependencies -- npm run dev:watch --if-present",
+ "test": "vitest",
+ "test-ui": "vitest --ui --coverage.enabled=true"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^3.3.4",
+ "@ovh-ux/manager-config": "^7.4.0",
+ "@ovh-ux/manager-core-api": "^0.8.0",
+ "@ovh-ux/manager-react-shell-client": "^0.7.2",
+ "@ovh-ux/request-tagger": "^0.3.0",
+ "@ovh-ux/shell": "^3.8.0",
+ "@ovhcloud/ods-common-core": "^17.1.0",
+ "@ovhcloud/ods-components": "^17.1.0",
+ "@ovhcloud/ods-theme-blue-jeans": "^17.1.0",
+ "@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-collapsible": "^1.0.3",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-progress": "^1.0.3",
+ "@radix-ui/react-radio-group": "^1.1.3",
+ "@radix-ui/react-scroll-area": "^1.0.5",
+ "@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-slider": "^1.1.2",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
+ "@radix-ui/react-tabs": "^1.0.4",
+ "@radix-ui/react-toast": "^1.1.5",
+ "@radix-ui/react-tooltip": "^1.0.7",
+ "@tanstack/react-query": "^5.51.21",
+ "@tanstack/react-table": "^8.20.1",
+ "chart.js": "^4.4.2",
+ "chartjs-adapter-date-fns": "^3.0.0",
+ "chartjs-plugin-zoom": "^2.0.1",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "cmdk": "^0.2.1",
+ "date-fns": "^3.6.0",
+ "duration-fns": "^3.0.2",
+ "i18next": "^23.8.2",
+ "i18next-http-backend": "^2.4.2",
+ "lucide-react": "^0.334.0",
+ "next-themes": "^0.2.1",
+ "qs": "^6.11.2",
+ "react": "^18.2.0",
+ "react-chartjs-2": "^5.2.0",
+ "react-day-picker": "^8.10.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.50.1",
+ "react-i18next": "^14.0.5",
+ "react-router": "^6.21.3",
+ "react-router-dom": "^6.3.0",
+ "sonner": "^1.4.0",
+ "tailwind-merge": "^2.2.1",
+ "tailwindcss": "^3.4.4",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@jest/globals": "^29.7.0",
+ "@ovh-ux/manager-vite-config": "^0.8.0",
+ "@testing-library/jest-dom": "^6.1.5",
+ "@testing-library/react": "^14.1.2",
+ "@testing-library/user-event": "^13.2.1",
+ "@types/jest": "^29.5.11",
+ "@types/node": "^20.10.6",
+ "@types/react": "^18.2.45",
+ "@types/react-dom": "^18.2.18",
+ "@vitejs/plugin-react": "^4.2.1",
+ "@vitest/ui": "^1.4.0",
+ "typescript": "^5.3.3",
+ "vite": "4.3.1",
+ "vitest": "^1.4.0"
+ },
+ "regions": [
+ "CA",
+ "EU"
+ ]
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs b/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs
new file mode 100644
index 000000000000..12a703d900da
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png b/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png
new file mode 100644
index 000000000000..413028afad19
Binary files /dev/null and b/packages/manager/apps/pci-ai-notebooks/public/assets/oops.png differ
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json
new file mode 100644
index 000000000000..5928e33d7bbe
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_de_DE.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "B",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "Byte",
+ "unitLong-B_other": "Byte",
+ "unitLong-KB_one": "Kilobyte",
+ "unitLong-KB_other": "Kilobyte",
+ "unitLong-MB_one": "Megabyte",
+ "unitLong-MB_other": "Megabyte",
+ "unitLong-GB_one": "Gigabyte",
+ "unitLong-GB_other": "Gigabyte",
+ "unitLong-TB_one": "Terabyte",
+ "unitLong-TB_other": "Terabyte",
+ "unitLong-PB_one": "Petabyte",
+ "unitLong-PB_other": "Petabyte"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json
new file mode 100644
index 000000000000..ff5ca5c4de65
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_en_GB.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "B",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "byte",
+ "unitLong-B_other": "bytes",
+ "unitLong-KB_one": "kilobyte",
+ "unitLong-KB_other": "kilobytes",
+ "unitLong-MB_one": "megabyte",
+ "unitLong-MB_other": "megabytes",
+ "unitLong-GB_one": "gigabyte",
+ "unitLong-GB_other": "gigabytes",
+ "unitLong-TB_one": "terabyte",
+ "unitLong-TB_other": "terabytes",
+ "unitLong-PB_one": "petabyte",
+ "unitLong-PB_other": "petabytes"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json
new file mode 100644
index 000000000000..ff5ca5c4de65
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_es_ES.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "B",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "byte",
+ "unitLong-B_other": "bytes",
+ "unitLong-KB_one": "kilobyte",
+ "unitLong-KB_other": "kilobytes",
+ "unitLong-MB_one": "megabyte",
+ "unitLong-MB_other": "megabytes",
+ "unitLong-GB_one": "gigabyte",
+ "unitLong-GB_other": "gigabytes",
+ "unitLong-TB_one": "terabyte",
+ "unitLong-TB_other": "terabytes",
+ "unitLong-PB_one": "petabyte",
+ "unitLong-PB_other": "petabytes"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json
new file mode 100644
index 000000000000..21953a45b499
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_CA.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "o",
+ "unitShort-KB": "Ko",
+ "unitShort-MB": "Mo",
+ "unitShort-GB": "Go",
+ "unitShort-TB": "To",
+ "unitShort-PB": "Po",
+ "unitLong-B_one": "octet",
+ "unitLong-B_other": "octets",
+ "unitLong-KB_one": "kilooctet",
+ "unitLong-KB_other": "kilooctets",
+ "unitLong-MB_one": "mégaoctet",
+ "unitLong-MB_other": "mégaoctets",
+ "unitLong-GB_one": "gigaoctet",
+ "unitLong-GB_other": "gigaoctets",
+ "unitLong-TB_one": "téraoctet",
+ "unitLong-TB_other": "téraoctets",
+ "unitLong-PB_one": "pétaoctet",
+ "unitLong-PB_other": "pétaoctets"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json
new file mode 100644
index 000000000000..21953a45b499
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_fr_FR.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "o",
+ "unitShort-KB": "Ko",
+ "unitShort-MB": "Mo",
+ "unitShort-GB": "Go",
+ "unitShort-TB": "To",
+ "unitShort-PB": "Po",
+ "unitLong-B_one": "octet",
+ "unitLong-B_other": "octets",
+ "unitLong-KB_one": "kilooctet",
+ "unitLong-KB_other": "kilooctets",
+ "unitLong-MB_one": "mégaoctet",
+ "unitLong-MB_other": "mégaoctets",
+ "unitLong-GB_one": "gigaoctet",
+ "unitLong-GB_other": "gigaoctets",
+ "unitLong-TB_one": "téraoctet",
+ "unitLong-TB_other": "téraoctets",
+ "unitLong-PB_one": "pétaoctet",
+ "unitLong-PB_other": "pétaoctets"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json
new file mode 100644
index 000000000000..375d7b8399d5
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_it_IT.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "b",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "byte",
+ "unitLong-B_other": "byte",
+ "unitLong-KB_one": "kilobyte",
+ "unitLong-KB_other": "kilobyte",
+ "unitLong-MB_one": "megabyte",
+ "unitLong-MB_other": "megabyte",
+ "unitLong-GB_one": "gigabyte",
+ "unitLong-GB_other": "gigabyte",
+ "unitLong-TB_one": "terabyte",
+ "unitLong-TB_other": "terabyte",
+ "unitLong-PB_one": "petabyte",
+ "unitLong-PB_other": "petabyte"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json
new file mode 100644
index 000000000000..1511808fc990
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pl_PL.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "B",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "bajt",
+ "unitLong-B_other": "bajty",
+ "unitLong-KB_one": "kilobajt",
+ "unitLong-KB_other": "kilobajty",
+ "unitLong-MB_one": "megabajt",
+ "unitLong-MB_other": "megabajty",
+ "unitLong-GB_one": "gigabajt",
+ "unitLong-GB_other": "gigabajty",
+ "unitLong-TB_one": "terabajt",
+ "unitLong-TB_other": "terabajty",
+ "unitLong-PB_one": "petabajt",
+ "unitLong-PB_other": "petabajty"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json
new file mode 100644
index 000000000000..ff5ca5c4de65
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/bytes/Messages_pt_PT.json
@@ -0,0 +1,20 @@
+{
+ "unitShort-B": "B",
+ "unitShort-KB": "KB",
+ "unitShort-MB": "MB",
+ "unitShort-GB": "GB",
+ "unitShort-TB": "TB",
+ "unitShort-PB": "PB",
+ "unitLong-B_one": "byte",
+ "unitLong-B_other": "bytes",
+ "unitLong-KB_one": "kilobyte",
+ "unitLong-KB_other": "kilobytes",
+ "unitLong-MB_one": "megabyte",
+ "unitLong-MB_other": "megabytes",
+ "unitLong-GB_one": "gigabyte",
+ "unitLong-GB_other": "gigabytes",
+ "unitLong-TB_one": "terabyte",
+ "unitLong-TB_other": "terabytes",
+ "unitLong-PB_one": "petabyte",
+ "unitLong-PB_other": "petabytes"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json
new file mode 100644
index 000000000000..8e10000b4500
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_de_DE.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Hoppla!",
+ "manager_error_page_button_cancel": "Abbrechen",
+ "manager_error_page_detail_code": "Fehlercode: ",
+ "manager_error_page_action_reload_label": "Erneut versuchen",
+ "manager_error_page_action_home_label": "Zurück zur Startseite",
+ "manager_error_page_default": "Beim Laden der Seite ist ein Fehler aufgetreten."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json
new file mode 100644
index 000000000000..b17691e2bc6d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_en_GB.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Oops!",
+ "manager_error_page_button_cancel": "Cancel",
+ "manager_error_page_detail_code": "Error code: ",
+ "manager_error_page_action_reload_label": "Try again",
+ "manager_error_page_action_home_label": "Back to homepage",
+ "manager_error_page_default": "An error has occurred loading the page."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json
new file mode 100644
index 000000000000..15fc5f79256d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_es_ES.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "¡Vaya!",
+ "manager_error_page_button_cancel": "Cancelar",
+ "manager_error_page_detail_code": "Código de error: ",
+ "manager_error_page_action_reload_label": "Volver a intentarlo",
+ "manager_error_page_action_home_label": "Volver a la página de inicio",
+ "manager_error_page_default": "Se ha producido un error al cargar la página."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json
new file mode 100644
index 000000000000..2c575c63588e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_CA.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Oops …!",
+ "manager_error_page_button_cancel": "Annuler",
+ "manager_error_page_detail_code": "Code d'erreur : ",
+ "manager_error_page_action_reload_label": "Réessayer",
+ "manager_error_page_action_home_label": "Retour à la page d'accueil",
+ "manager_error_page_default": "Une erreur est survenue lors du chargement de la page."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json
new file mode 100644
index 000000000000..2c575c63588e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_fr_FR.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Oops …!",
+ "manager_error_page_button_cancel": "Annuler",
+ "manager_error_page_detail_code": "Code d'erreur : ",
+ "manager_error_page_action_reload_label": "Réessayer",
+ "manager_error_page_action_home_label": "Retour à la page d'accueil",
+ "manager_error_page_default": "Une erreur est survenue lors du chargement de la page."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json
new file mode 100644
index 000000000000..fa5055b8cec5
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_it_IT.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Ops!",
+ "manager_error_page_button_cancel": "Annullare",
+ "manager_error_page_detail_code": "Codice di errore: ",
+ "manager_error_page_action_reload_label": "Riprova",
+ "manager_error_page_action_home_label": "Torna alla home page",
+ "manager_error_page_default": "Si è verificato un errore durante il caricamento della pagina."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json
new file mode 100644
index 000000000000..eceb9bcca2ca
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pl_PL.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Ojej...",
+ "manager_error_page_button_cancel": "Anuluj",
+ "manager_error_page_detail_code": "Kod błędu: ",
+ "manager_error_page_action_reload_label": "Spróbuj ponownie",
+ "manager_error_page_action_home_label": "Powrót do strony głównej",
+ "manager_error_page_default": "Wystąpił błąd podczas ładowania strony."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json
new file mode 100644
index 000000000000..25fac9551c86
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/error/Messages_pt_PT.json
@@ -0,0 +1,8 @@
+{
+ "manager_error_page_title": "Oops!",
+ "manager_error_page_button_cancel": "Anular",
+ "manager_error_page_detail_code": "Código de erro: ",
+ "manager_error_page_action_reload_label": "Tentar novamente",
+ "manager_error_page_action_home_label": "Voltar para a página inicial",
+ "manager_error_page_default": "Ocorreu um erro ao carregar a página."
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json
new file mode 100644
index 000000000000..f8078fff32f7
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_de_DE.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guides",
+ "searchBarPlaceholder": "Guide suchen",
+ "noResult": "Keine Guides gefunden",
+ "listHeading": "Guides"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json
new file mode 100644
index 000000000000..9cff75aef0ed
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_en_GB.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guides",
+ "searchBarPlaceholder": "Search for a guide",
+ "noResult": "No guides found",
+ "listHeading": "Guides"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json
new file mode 100644
index 000000000000..9fc61d66b542
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_es_ES.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guías",
+ "searchBarPlaceholder": "Buscar una guía",
+ "noResult": "No se ha encontrado ninguna guía.",
+ "listHeading": "Guías"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json
new file mode 100644
index 000000000000..6801e5860037
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_CA.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guides",
+ "searchBarPlaceholder": "Chercher un guide",
+ "noResult": "Aucun guide trouvé",
+ "listHeading": "Guides"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json
new file mode 100644
index 000000000000..6801e5860037
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_fr_FR.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guides",
+ "searchBarPlaceholder": "Chercher un guide",
+ "noResult": "Aucun guide trouvé",
+ "listHeading": "Guides"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json
new file mode 100644
index 000000000000..f9896a7300fb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_it_IT.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Guide",
+ "searchBarPlaceholder": "Cercare una guida",
+ "noResult": "Nessuna guida trovata",
+ "listHeading": "Guide"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json
new file mode 100644
index 000000000000..8a262eb97567
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pl_PL.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Przewodniki",
+ "searchBarPlaceholder": "Znajdź przewodnik",
+ "noResult": "Nie znaleziono żadnego przewodnika",
+ "listHeading": "Przewodniki"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json
new file mode 100644
index 000000000000..ef8f9264f6ec
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/guides/Messages_pt_PT.json
@@ -0,0 +1,6 @@
+{
+ "buttonLabel": "Manuais",
+ "searchBarPlaceholder": "Procurar um manual",
+ "noResult": "Nenhum manual encontrado",
+ "listHeading": "Manuais"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json
new file mode 100644
index 000000000000..633c03205bda
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/Messages_fr_FR.json
@@ -0,0 +1,3 @@
+{
+ "title": "AI Notebook"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json
new file mode 100644
index 000000000000..acbae21d513e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/auth/Messages_fr_FR.json
@@ -0,0 +1,3 @@
+{
+ "title": "Auth"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json
new file mode 100644
index 000000000000..ea70d8bc588f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/configuration/Messages_fr_FR.json
@@ -0,0 +1,22 @@
+{
+ "keyFieldLabel": "Clé",
+ "valueFieldLabel": "Valeur",
+ "existingKeyError": "Cette clé existe déjà",
+ "numberOfConfiguredLabels_zero": "{{count}} labels configurés",
+ "numberOfConfiguredLabels_one": "{{count}} label configuré sur {{max}}",
+ "numberOfConfiguredLabels_other": "{{count}} labels configurés sur {{max}}",
+ "duplicateKeyError": "Cette clé SSH est déjà configurée pour ce notebook",
+ "configuredKeyFieldLabel": "Clé SSH déjà paramétrée",
+ "sshKeyFieldPlaceholder": "Sélectionner une clé SSH",
+ "sshKeyFieldLabel": "Nom de la clé SSH",
+ "sskKeyValueFieldLabel": "Clé publique SSH",
+ "numberOfConfiguredKeys_zero": "{{count}} clés SSH configurées",
+ "numberOfConfiguredKeys_one": "{{count}} clé SSH configurée sur {{max}}",
+ "numberOfConfiguredKeys_other": "{{count}} clés SSH configurées sur {{max}}",
+ "addSshKeyToastErrorTitle": "Une erreur est survenue lors de la création de votre clé SSH",
+ "formConnectionPoolToastSuccessTitle": "Succés",
+ "addSshKeySuccessDescription": "Nouvelle clé SSH créée",
+ "addSshKeyTitle": "Configurer un nouvelle clé SSH",
+ "formSshKeyButtonCancel": "Annuler",
+ "formSshKeyButtonConfirm": "Confirmer"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json
new file mode 100644
index 000000000000..62ed5166351e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/flavor/Messages_fr_FR.json
@@ -0,0 +1,15 @@
+{
+ "tableHeadType": "Type",
+ "tableHeadDescription": "Description",
+ "tableHeadVcores": "Vcores",
+ "tableHeadMemory": "RAM",
+ "tableHeadStorage": "Stockage éphémère",
+ "tableHeadPrice": "Prix",
+ "gpuMemorySpec": "{{memory}} RAM",
+ "cpuCores_one": "CPU: {{count}} vCore",
+ "cpuCores_other": "CPU: {{count}} vCores",
+ "ramSpec": "RAM: {{ram}}",
+ "localStorageSpec": "Stockage local éphémère: {{disk}} SSD",
+ "networkSpec": "Réseau public: {{network}}/s",
+ "resourcePrice": "Prix ressources"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json
new file mode 100644
index 000000000000..c95e596fd3c4
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/framework/Messages_fr_FR.json
@@ -0,0 +1,4 @@
+{
+ "selectAVersion": "Sélectionner une version",
+ "noVersionFound": "Aucune version trouvée"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json
new file mode 100644
index 000000000000..ceef19bfe421
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/components/volumes/Messages_fr_FR.json
@@ -0,0 +1,19 @@
+{
+ "containerFieldLabel": "Container",
+ "containerFieldPlaceholder": "Séléctionner un container",
+ "gitBranchFieldLabel": "Branch Git",
+ "mountDirectoryFieldLabel": "Répertoire de montage",
+ "permissionsFieldLabel": "Permission",
+ "permissionDescription": "En `Lecture Seule` aucune synchronisation n'est lancée en fin de job. Le job se finalise ainsi plus rapidement.",
+ "permission_RO": "Lecture Seule",
+ "permission_RW": "Lecture & Ecriture",
+ "permission_RWD": "Lecture & Ecriture & Suppression",
+ "cachingFieldLabel": "Cache",
+ "cachingDescription": "Les conteneurs en cache peuvent être réutilisés par d'autres jobs sans synchronisation additionnelle. Les conteneurs sont retirés du cache après une période d'inactivité.",
+ "duplicateMountPathError": "Ce répertoire de montage existe déjà",
+ "mountPathError": "Ce répertoire de montage est réservé",
+ "mountPathErrorFormat": "Format incorrect (ex : /files/test)",
+ "numberOfConfiguredVolumes_zero": "{{count}} volumes configurés",
+ "numberOfConfiguredVolumes_one": "{{count}} volume configuré sur {{max}}",
+ "numberOfConfiguredVolumes_other": "{{count}} volumes configurés sur {{max}}"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json
new file mode 100644
index 000000000000..0c243601e95a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/Messages_fr_FR.json
@@ -0,0 +1,17 @@
+{
+ "title": "AI Notebook",
+ "createNewNotebook": "Créer un notebook",
+ "tableHeaderName": "Name",
+ "tableHeaderLocation": "Région",
+ "tableHeaderEnvironment": "Environnement",
+ "tableHeaderResources": "Ressources",
+ "tableHeaderPrivacy": "Confidentialité ",
+ "networkSecureTitle": "Privé",
+ "networkPublicTitle": "Public",
+ "tableHeaderDuration": "Durée de fonctionnement",
+ "tableHeaderStatus": "Statut",
+ "tableActionManage": "Gérer",
+ "tableActionStart": "Démarrer",
+ "tableActionStop": "Arrêter",
+ "tableActionDelete": "Supprimer"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json
new file mode 100644
index 000000000000..61c1938c535c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/create/Messages_fr_FR.json
@@ -0,0 +1,58 @@
+{
+ "title": "Créer un notebook",
+ "breadcrumb": "Créer un notebook",
+ "summaryTitle": "Votre commande",
+ "summaryFieldRegionLabel": "Région:",
+ "fieldDimensionLabel": "Dimmensionnez votre notebook",
+ "fieldRegionLabel": "Localisation",
+ "fieldFlavorLabel": "Ressources",
+ "fieldFlavorDescription": "Séléctionner le type de ressources",
+ "fieldFlavorQuantityDescription": "Sélectionner la quantité de ressources",
+ "fieldFlavorQuantityInformation": "Quantité maximale pour",
+ "summaryFieldFlavorLabel": "Ressources:",
+ "summaryFieldFlavorCores_one": "{{count}} vCore",
+ "summaryFieldFlavorCores_other": "{{count}} vCores",
+ "summaryFieldFlavorMemory": "{{memory}} RAM",
+ "summaryFieldStorage": "{{disk}} SSD",
+ "summaryFieldNetwork": "{{network}}/s",
+ "fieldCaracteristicLabel": "Choisissez les caractéristiques de votre notebook",
+ "fieldFrameworkLabel": "Frameworks",
+ "summaryFieldFrameworkLabel": "Framework:",
+ "fieldEditorLabel": "Editors",
+ "summaryFieldEditorLabel": "Editor:",
+ "fieldConfigurationLabel": "Configurez vos options",
+ "fieldConfigurationNameLabel": "Nom du notebook",
+ "fieldConfigurationPrivacyLabel": "Confidentialité",
+ "privateAccess": "Accès restreint",
+ "privateAccessDescription": "L’accès à votre notebook est réservé aux utilisateurs Public Cloud OVHcloud créés dans votre projet.",
+ "publicAccess": "Accès public",
+ "publicAccessDescription1": "Votre notebook est accessible à tout le monde grâce à votre URL. N’importe qui peut accéder à votre code et aux données qui y sont attachées.",
+ "publicAccessDescription2": "Soyez prudent·e avec les données sensibles.",
+ "summaryFieldNameLabel": "Nom:",
+ "summaryFieldPrivacyLabel": "Confidentialité:",
+ "summaryFieldPublicLabel": "Public",
+ "summaryFieldPrivateLabel": "Private",
+ "formButtonAdvancedConfiguration": "Configuration avancée",
+ "fieldVolumesLabel": "Volumes",
+ "summaryFieldVolumesLabel": "Volumes:",
+ "fieldVolumeDescription": "Faire un petit laius sur les volumes, à quoi ils servent, comment les configurer et tout ci tout ca",
+ "noVolumeDescription": "Vous n'avez pas de volumes configurés. Configurer un S3 ou Git / Configurer un container Swift",
+ "summaryFieldVolumes_one": "{{count}} volume configuré",
+ "summaryFieldVolumes_other": "{{count}} volumes configurés",
+ "fieldConfigurationLabelsLabel": "Labels",
+ "summaryFieldLabelsLabel": "Labels:",
+ "summaryFieldLabels_one": "{{count}} label configuré",
+ "summaryFieldLabels_other": "{{count}} labels configurés",
+ "fieldConfigurationSSHKeysLabel": "Clés publiques SSH",
+ "sshkeyAddButtonLabel": "Configurer une clé SSH",
+ "sshKeyConfigurationHelper": "Configurer une clé SSH que vous pourrez utiliser sur tout l'univers Public Cloud d'OVHcloud.",
+ "summaryFieldSSHLabel": "Clés SSH:",
+ "summaryFieldSshKey_one": "{{count}} clé configurée",
+ "summaryFieldSshKey_other": "{{count}} clés configurées",
+ "orderButton": "Commander",
+ "cliCode": "Equivalent CLI",
+ "errorGetCommandCli": "Une erreur est survenue lors de la génération de code équivalent pour la CLI",
+ "cliEquivalentModalTitle": "Création d’un notebook équivalent",
+ "cliEquivalentModalDescription": "Créer le même notebook via la CLI",
+ "cliEquivalentModalToastMessage": "Le code a été copié"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json
new file mode 100644
index 000000000000..afeff6751635
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/Messages_fr_FR.json
@@ -0,0 +1,26 @@
+{
+ "deleteNotebookTitle": "Supprimer le notebook",
+ "deleteNotebookDescription": "Etes-vous sur de vouloir supprimer le notebook {{name}} ?",
+ "notebookButtonCancel": "Annuler",
+ "deleteNotebookButtonConfirm": "Supprimer",
+ "notebookToastErrorTitle": "Echec",
+ "notebookToastSuccessTitle": "Succès",
+ "deleteNotebookToastSuccessDescription": "Le notebook {{name}} a été supprimé",
+ "startNotebookTitle": "Démarrer le notebook",
+ "startNotebookDescription": "Etes-vous sur de vouloir démarrer le notebook {{name}} ?",
+ "startNotebookButtonConfirm": "Démarrer",
+ "startNotebookToastSuccessDescription": "Le notebook {{name}} a été démarré",
+ "stopNotebookTitle": "Arrêter le notebook",
+ "stopNotebookDescription1": "Lorsque vous stoppez un AI Notebook :",
+ "stopNotebookDescriptionList1": "Les ressources de calculs sont libérées (vous ne payez plus pour les CPUs et GPUs)",
+ "stopNotebookDescriptionList2": "Nous sauvegardons votre Espace de travail (dossier /worskpace) de manière sécurisée (10 Gio gratuits, ensuite au coût de l'Object Storage).",
+ "stopNotebookDescriptionList3": "Tout ce qui est sur le stockage local éphémère sera perdu.",
+ "stopNotebookDescription2": "Une fois stoppé, vous pourrez soit",
+ "stopNotebookDescription2Bis": "supprimer",
+ "stopNotebookDescription2Ter": "votre AI Notebook soit le",
+ "stopNotebookDescription2Quater": "relancer",
+ "stopNotebookDescription3": "Nous restaurerons votre Espace de Travail et réallouerons des ressources de calculs.",
+ "stopNotebookConfirmation": "Etes-vous sur de vouloir arrêter le notebook {{name}} ?",
+ "stopNotebookButtonConfirm": "Arrêter",
+ "stopNotebookToastSuccessDescription": "Le notebook {{name}} a été arrêté"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json
new file mode 100644
index 000000000000..de9edf57924f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/status/Messages_fr_FR.json
@@ -0,0 +1,11 @@
+{
+ "status-DELETING": "En cours de suppression",
+ "status-RESTARTING": "Redémarrage en cours",
+ "status-STARTING": "Démarrage",
+ "status-STOPPING": "En cours d'extinction",
+ "status-FAILED": "Erreur",
+ "status-ERROR": "Erreur",
+ "status-STOPPED": "Arrêté",
+ "status-SYNC_FAILED": "Echec de synchronisation",
+ "status-RUNNING": "En service"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json
new file mode 100644
index 000000000000..5b317922445e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pricing/Messages_fr_FR.json
@@ -0,0 +1,6 @@
+{
+ "pricingHt": "{{ price }} HT",
+ "pricingInHour": "/ heure",
+ "pricingTtc": "{{ price }} TTC",
+ "pricingLabel": "Prix"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json
new file mode 100644
index 000000000000..24b6c3bde262
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_de_DE.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Straßburg (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Straßburg",
+ "region_SBG_micro": "Straßburg ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "London",
+ "region_ERI_micro": "London ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburg",
+ "region_LIM_micro": "Limburg ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Warschau",
+ "region_WAW_micro": "Warschau ({{ micro }})",
+ "region_DE": "Frankfurt",
+ "region_DE_micro": "Frankfurt ({{ micro }})",
+ "region_UK": "London",
+ "region_UK_micro": "London ({{ micro }})",
+ "region_SGP": "Singapur",
+ "region_SGP_micro": "Singapur ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "USA",
+ "region_US_micro": "USA ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Brüssel",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Brüssel ({{ micro }})",
+ "region_localize": "Lokalisieren",
+ "region_location_SBG": "Mitteleuropa (Frankreich)",
+ "region_location_WAW": "Mitteleuropa (Polen)",
+ "region_location_BHS": "Nordamerika (Kanada)",
+ "region_location_ERI": "Westeuropa (Vereinigtes Königreich)",
+ "region_location_GRA": "Westeuropa (Frankreich)",
+ "region_location_GS": "Westeuropa",
+ "region_location_MAD": "Westeuropa",
+ "region_location_BRU": "Westeuropa",
+ "region_location_LIM": "Mitteleuropa (Deutschland)",
+ "region_location_RBX": "Westeuropa (Frankreich)",
+ "region_location_DE": "Mitteleuropa (Deutschland)",
+ "region_location_UK": "Westeuropa (Vereinigtes Königreich)",
+ "region_location_SGP": "Südostasien (Singapur)",
+ "region_location_SYD": "Ozeanien (Australien)",
+ "region_location_US": "USA",
+ "region_continent_SBG": "Mitteleuropa",
+ "region_continent_WAW": "Mitteleuropa",
+ "region_continent_BHS": "Nordamerika",
+ "region_continent_GRA": "Westeuropa",
+ "region_continent_RBX": "Westeuropa",
+ "region_continent_GS": "Westeuropa",
+ "region_continent_MAD": "Westeuropa",
+ "region_continent_BRU": "Westeuropa",
+ "region_continent_DE": "Mitteleuropa",
+ "region_continent_UK": "Westeuropa",
+ "region_continent_SGP": "Südostasien",
+ "region_continent_SYD": "Ozeanien",
+ "region_continent_US": "USA",
+ "region_continent_SHA": "Westeuropa",
+ "region_continent_all": "Alle Standorte"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json
new file mode 100644
index 000000000000..f91fd8920068
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_en_GB.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Strasbourg (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Strasbourg",
+ "region_SBG_micro": "Strasbourg ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "London",
+ "region_ERI_micro": "London ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburg",
+ "region_LIM_micro": "Limburg ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Warsaw",
+ "region_WAW_micro": "Warsaw ({{ micro }})",
+ "region_DE": "Frankfurt",
+ "region_DE_micro": "Frankfurt ({{ micro }})",
+ "region_UK": "London",
+ "region_UK_micro": "London ({{ micro }})",
+ "region_SGP": "Singapore",
+ "region_SGP_micro": "Singapore ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "United States of America",
+ "region_US_micro": "United States ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Brussels",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Brussels ({{ micro }})",
+ "region_localize": "Locate",
+ "region_location_SBG": "Central Europe (France)",
+ "region_location_WAW": "Central Europe (Poland)",
+ "region_location_BHS": "North America (Canada)",
+ "region_location_ERI": "Western Europe (United Kingdom)",
+ "region_location_GRA": "Western Europe (France)",
+ "region_location_GS": "Western Europe",
+ "region_location_MAD": "Western Europe",
+ "region_location_BRU": "Western Europe",
+ "region_location_LIM": "Central Europe (Germany)",
+ "region_location_RBX": "Western Europe (France)",
+ "region_location_DE": "Central Europe (Germany)",
+ "region_location_UK": "Western Europe (United Kingdom)",
+ "region_location_SGP": "South-East Asia (Singapore)",
+ "region_location_SYD": "Oceania (Australia)",
+ "region_location_US": "United States of America",
+ "region_continent_SBG": "Central Europe",
+ "region_continent_WAW": "Central Europe",
+ "region_continent_BHS": "North America",
+ "region_continent_GRA": "Western Europe",
+ "region_continent_RBX": "Western Europe",
+ "region_continent_GS": "Western Europe",
+ "region_continent_MAD": "Western Europe",
+ "region_continent_BRU": "Western Europe",
+ "region_continent_DE": "Central Europe",
+ "region_continent_UK": "Western Europe",
+ "region_continent_SGP": "South-East Asia",
+ "region_continent_SYD": "Oceania",
+ "region_continent_US": "United States of America",
+ "region_continent_SHA": "Western Europe",
+ "region_continent_all": "All locations"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json
new file mode 100644
index 000000000000..d78d3808005d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_es_ES.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Estrasburgo (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Estrasburgo",
+ "region_SBG_micro": "Estrasburgo ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londres",
+ "region_ERI_micro": "Londres ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburgo",
+ "region_LIM_micro": "Limburgo ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Varsovia",
+ "region_WAW_micro": "Varsovia ({{ micro }})",
+ "region_DE": "Fráncfort",
+ "region_DE_micro": "Fráncfort ({{ micro }})",
+ "region_UK": "Londres",
+ "region_UK_micro": "Londres ({{ micro }})",
+ "region_SGP": "Singapur",
+ "region_SGP_micro": "Singapur ({{ micro }})",
+ "region_SYD": "Sídney",
+ "region_SYD_micro": "Sídney ({{ micro }})",
+ "region_US": "Estados Unidos",
+ "region_US_micro": "Estados Unidos ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Bruselas",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Bruselas ({{ micro }})",
+ "region_localize": "Localizar",
+ "region_location_SBG": "Europa Central (Francia)",
+ "region_location_WAW": "Europa Central (Polonia)",
+ "region_location_BHS": "Norteamérica (Canadá)",
+ "region_location_ERI": "Europa Occidental (Reino Unido)",
+ "region_location_GRA": "Europa Occidental (Francia)",
+ "region_location_GS": "Western Europe",
+ "region_location_MAD": "Western Europe",
+ "region_location_BRU": "Western Europe",
+ "region_location_LIM": "Europa Central (Alemania)",
+ "region_location_RBX": "Europa Occidental (Francia)",
+ "region_location_DE": "Europa Central (Alemania)",
+ "region_location_UK": "Europa Occidental (Reino Unido)",
+ "region_location_SGP": "Sudeste asiático (Singapur)",
+ "region_location_SYD": "Oceanía (Australia)",
+ "region_location_US": "Estados Unidos",
+ "region_continent_SBG": "Europa Central",
+ "region_continent_WAW": "Europa Central",
+ "region_continent_BHS": "Norteamérica",
+ "region_continent_GRA": "Europa Occidental",
+ "region_continent_RBX": "Europa Occidental",
+ "region_continent_GS": "Western Europe",
+ "region_continent_MAD": "Western Europe",
+ "region_continent_BRU": "Western Europe",
+ "region_continent_DE": "Europa Central",
+ "region_continent_UK": "Europa Occidental",
+ "region_continent_SGP": "Sudeste Asiático",
+ "region_continent_SYD": "Oceanía",
+ "region_continent_US": "Estados Unidos",
+ "region_continent_SHA": "Europa Occidental",
+ "region_continent_all": "Todas las localizaciones"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json
new file mode 100644
index 000000000000..8d6147ad82fb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_CA.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Strasbourg (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Strasbourg",
+ "region_SBG_micro": "Strasbourg ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londres",
+ "region_ERI_micro": "Londres ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburg",
+ "region_LIM_micro": "Limburg ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Varsovie",
+ "region_WAW_micro": "Varsovie ({{ micro }})",
+ "region_DE": "Francfort",
+ "region_DE_micro": "Francfort ({{ micro }})",
+ "region_UK": "Londres",
+ "region_UK_micro": "Londres ({{ micro }})",
+ "region_SGP": "Singapour",
+ "region_SGP_micro": "Singapour ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "États-Unis",
+ "region_US_micro": "États-Unis ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Bruxelles",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Bruxelles ({{ micro }})",
+ "region_localize": "Localiser",
+ "region_location_SBG": "Europe centrale (France)",
+ "region_location_WAW": "Europe centrale (Pologne)",
+ "region_location_BHS": "Amérique du Nord (Canada)",
+ "region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)",
+ "region_location_GRA": "Europe de l'Ouest (France)",
+ "region_location_GS": "Western Europe",
+ "region_location_MAD": "Western Europe",
+ "region_location_BRU": "Western Europe",
+ "region_location_LIM": "Europe centrale (Allemagne)",
+ "region_location_RBX": "Europe de l'Ouest (France)",
+ "region_location_DE": "Europe centrale (Allemagne)",
+ "region_location_UK": "Europe de l'Ouest (Grande-Bretagne)",
+ "region_location_SGP": "Asie du Sud-Est (Singapour)",
+ "region_location_SYD": "Océanie (Australie)",
+ "region_location_US": "États-Unis",
+ "region_continent_SBG": "Europe centrale",
+ "region_continent_WAW": "Europe centrale",
+ "region_continent_BHS": "Amérique du Nord",
+ "region_continent_GRA": "Europe de l'Ouest",
+ "region_continent_RBX": "Europe de l'Ouest",
+ "region_continent_GS": "Western Europe",
+ "region_continent_MAD": "Western Europe",
+ "region_continent_BRU": "Western Europe",
+ "region_continent_DE": "Europe centrale",
+ "region_continent_UK": "Europe de l'Ouest",
+ "region_continent_SGP": "Asie du Sud-Est",
+ "region_continent_SYD": "Océanie",
+ "region_continent_US": "États-Unis",
+ "region_continent_SHA": "Europe de l'Ouest",
+ "region_continent_all": "Toutes les localisations"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json
new file mode 100644
index 000000000000..8d6147ad82fb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_fr_FR.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Strasbourg (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Strasbourg",
+ "region_SBG_micro": "Strasbourg ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londres",
+ "region_ERI_micro": "Londres ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburg",
+ "region_LIM_micro": "Limburg ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Varsovie",
+ "region_WAW_micro": "Varsovie ({{ micro }})",
+ "region_DE": "Francfort",
+ "region_DE_micro": "Francfort ({{ micro }})",
+ "region_UK": "Londres",
+ "region_UK_micro": "Londres ({{ micro }})",
+ "region_SGP": "Singapour",
+ "region_SGP_micro": "Singapour ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "États-Unis",
+ "region_US_micro": "États-Unis ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Bruxelles",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Bruxelles ({{ micro }})",
+ "region_localize": "Localiser",
+ "region_location_SBG": "Europe centrale (France)",
+ "region_location_WAW": "Europe centrale (Pologne)",
+ "region_location_BHS": "Amérique du Nord (Canada)",
+ "region_location_ERI": "Europe de l'Ouest (Grande-Bretagne)",
+ "region_location_GRA": "Europe de l'Ouest (France)",
+ "region_location_GS": "Western Europe",
+ "region_location_MAD": "Western Europe",
+ "region_location_BRU": "Western Europe",
+ "region_location_LIM": "Europe centrale (Allemagne)",
+ "region_location_RBX": "Europe de l'Ouest (France)",
+ "region_location_DE": "Europe centrale (Allemagne)",
+ "region_location_UK": "Europe de l'Ouest (Grande-Bretagne)",
+ "region_location_SGP": "Asie du Sud-Est (Singapour)",
+ "region_location_SYD": "Océanie (Australie)",
+ "region_location_US": "États-Unis",
+ "region_continent_SBG": "Europe centrale",
+ "region_continent_WAW": "Europe centrale",
+ "region_continent_BHS": "Amérique du Nord",
+ "region_continent_GRA": "Europe de l'Ouest",
+ "region_continent_RBX": "Europe de l'Ouest",
+ "region_continent_GS": "Western Europe",
+ "region_continent_MAD": "Western Europe",
+ "region_continent_BRU": "Western Europe",
+ "region_continent_DE": "Europe centrale",
+ "region_continent_UK": "Europe de l'Ouest",
+ "region_continent_SGP": "Asie du Sud-Est",
+ "region_continent_SYD": "Océanie",
+ "region_continent_US": "États-Unis",
+ "region_continent_SHA": "Europe de l'Ouest",
+ "region_continent_all": "Toutes les localisations"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json
new file mode 100644
index 000000000000..2d2b67b9b45d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_it_IT.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Strasburgo (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Strasburgo",
+ "region_SBG_micro": "Strasburgo ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londra",
+ "region_ERI_micro": "Londra ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburgo",
+ "region_LIM_micro": "Limburgo ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Varsavia",
+ "region_WAW_micro": "Varsavia ({{ micro }})",
+ "region_DE": "Francoforte",
+ "region_DE_micro": "Francoforte ({{ micro }})",
+ "region_UK": "Londra",
+ "region_UK_micro": "Londra ({{ micro }})",
+ "region_SGP": "Singapore",
+ "region_SGP_micro": "Singapore ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "Stati Uniti",
+ "region_US_micro": "Stati Uniti ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Bruxelles",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Bruxelles ({{ micro }})",
+ "region_localize": "Localizza",
+ "region_location_SBG": "Europa centrale (Francia)",
+ "region_location_WAW": "Europa centrale (Polonia)",
+ "region_location_BHS": "Nord America (Canada)",
+ "region_location_ERI": "Europa Occidentale (Gran Bretagna)",
+ "region_location_GRA": "Europa Occidentale (Francia)",
+ "region_location_GS": "Europa Occidentale",
+ "region_location_MAD": "Europa Occidentale",
+ "region_location_BRU": "Europa Occidentale",
+ "region_location_LIM": "Europa centrale (Germania)",
+ "region_location_RBX": "Europa Occidentale (Francia)",
+ "region_location_DE": "Europa centrale (Germania)",
+ "region_location_UK": "Europa Occidentale (Gran Bretagna)",
+ "region_location_SGP": "Sud-est asiatico (Singapore)",
+ "region_location_SYD": "Oceania (Australia)",
+ "region_location_US": "Stati Uniti",
+ "region_continent_SBG": "Europa centrale",
+ "region_continent_WAW": "Europa centrale",
+ "region_continent_BHS": "Nord America ",
+ "region_continent_GRA": "Europa Occidentale",
+ "region_continent_RBX": "Europa Occidentale",
+ "region_continent_GS": "Europa Occidentale",
+ "region_continent_MAD": "Europa Occidentale",
+ "region_continent_BRU": "Europa Occidentale",
+ "region_continent_DE": "Europa centrale",
+ "region_continent_UK": "Europa Occidentale",
+ "region_continent_SGP": "Asia Pacifica",
+ "region_continent_SYD": "Oceania",
+ "region_continent_US": "Stati Uniti",
+ "region_continent_SHA": "Europa Occidentale",
+ "region_continent_all": "Tutte le Region"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json
new file mode 100644
index 000000000000..6f3791ace50e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pl_PL.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Strasburg (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Strasburg",
+ "region_SBG_micro": "Strasburg ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londyn",
+ "region_ERI_micro": "Londyn ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburg",
+ "region_LIM_micro": "Limburg ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Warszawa",
+ "region_WAW_micro": "Warszawa ({{ micro }})",
+ "region_DE": "Frankfurt",
+ "region_DE_micro": "Frankfurt ({{ micro }})",
+ "region_UK": "Londyn",
+ "region_UK_micro": "Londyn ({{ micro }})",
+ "region_SGP": "Singapur",
+ "region_SGP_micro": "Singapur ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "Stany Zjednoczone",
+ "region_US_micro": "Stany Zjednoczone ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madryt",
+ "region_BRU": "Bruksela",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{micro}})",
+ "region_MAD_micro": "Madryt ({{micro}})",
+ "region_BRU_micro": "Bruksela ({{micro}})",
+ "region_localize": "Lokalizacja",
+ "region_location_SBG": "Europa Środkowa (Francja)",
+ "region_location_WAW": "Europa Środkowa (Polska)",
+ "region_location_BHS": "Ameryka Północna (Kanada)",
+ "region_location_ERI": "Europa Zachodnia (Wielka Brytania)",
+ "region_location_GRA": "Europa Zachodnia (Francja)",
+ "region_location_GS": "Europa Zachodnia",
+ "region_location_MAD": "Europa Zachodnia",
+ "region_location_BRU": "Europa Zachodnia",
+ "region_location_LIM": "Europa Środkowa (Niemcy)",
+ "region_location_RBX": "Europa Zachodnia (Francja)",
+ "region_location_DE": "Europa Środkowa (Niemcy)",
+ "region_location_UK": "Europa Zachodnia (Wielka Brytania)",
+ "region_location_SGP": "Azja Południowo-Wschodnia (Singapur)",
+ "region_location_SYD": "Oceania (Australia)",
+ "region_location_US": "Stany Zjednoczone",
+ "region_continent_SBG": "Europa Środkowa",
+ "region_continent_WAW": "Europa Środkowa",
+ "region_continent_BHS": "Ameryka Północna",
+ "region_continent_GRA": "Europa Zachodnia",
+ "region_continent_RBX": "Europa Zachodnia",
+ "region_continent_GS": "Europa Zachodnia",
+ "region_continent_MAD": "Europa Zachodnia",
+ "region_continent_BRU": "Europa Zachodnia",
+ "region_continent_DE": "Europa Środkowa",
+ "region_continent_UK": "Europa Zachodnia",
+ "region_continent_SGP": "Azja Południowo-Wschodnia",
+ "region_continent_SYD": "Oceania",
+ "region_continent_US": "Stany Zjednoczone",
+ "region_continent_SHA": "Europa Zachodnia",
+ "region_continent_all": "Wszystkie lokalizacje"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json
new file mode 100644
index 000000000000..365abdb1cdac
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/public/translations/regions/Messages_pt_PT.json
@@ -0,0 +1,67 @@
+{
+ "region_SBG1": "Estrasburgo (SBG1)",
+ "region_BHS1": "Beauharnois (BHS1)",
+ "region_GRA1": "Gravelines (GRA1)",
+ "region_SBG": "Estrasburgo",
+ "region_SBG_micro": "Estrasburgo ({{ micro }})",
+ "region_BHS": "Beauharnois",
+ "region_BHS_micro": "Beauharnois ({{ micro }})",
+ "region_ERI": "Londres",
+ "region_ERI_micro": "Londres ({{ micro }})",
+ "region_GRA": "Gravelines",
+ "region_GRA_micro": "Gravelines ({{ micro }})",
+ "region_LIM": "Limburgo",
+ "region_LIM_micro": "Limburgo ({{ micro }})",
+ "region_RBX": "Roubaix",
+ "region_RBX_micro": "Roubaix ({{ micro }})",
+ "region_WAW": "Varsóvia",
+ "region_WAW_micro": "Varsóvia ({{ micro }})",
+ "region_DE": "Frankfurt ",
+ "region_DE_micro": "Frankfurt ({{ micro }})",
+ "region_UK": "Londres",
+ "region_UK_micro": "Londres ({{ micro }})",
+ "region_SGP": "Singapura",
+ "region_SGP_micro": "Singapura ({{ micro }})",
+ "region_SYD": "Sydney",
+ "region_SYD_micro": "Sydney ({{ micro }})",
+ "region_US": "Estados Unidos",
+ "region_US_micro": "Estados Unidos ({{ micro }})",
+ "region_GS": "GS",
+ "region_MAD": "Madrid",
+ "region_BRU": "Bruxelas",
+ "region_SHA_micro": "Gravelines (SHADOW-EU-1)",
+ "region_GS_micro": "Gridscale ({{ micro }})",
+ "region_MAD_micro": "Madrid ({{ micro }})",
+ "region_BRU_micro": "Bruxelas ({{ micro }})",
+ "region_localize": "Localizar",
+ "region_location_SBG": "Europa Central (França)",
+ "region_location_WAW": "Europa Central (Polónia)",
+ "region_location_BHS": "América do Norte (Canadá)",
+ "region_location_ERI": "Europa Ocidental (Reino Unido)",
+ "region_location_GRA": "Europa Ocidental (França)",
+ "region_location_GS": "Western Europe",
+ "region_location_MAD": "Western Europe",
+ "region_location_BRU": "Western Europe",
+ "region_location_LIM": "Europa Central (Alemanha)",
+ "region_location_RBX": "Europa Ocidental (França)",
+ "region_location_DE": "Europa Central (Alemanha)",
+ "region_location_UK": "Europa Ocidental (Reino Unido)",
+ "region_location_SGP": "Sudeste Asiático (Singapura)",
+ "region_location_SYD": "Oceânia (Austrália)",
+ "region_location_US": "Estados Unidos",
+ "region_continent_SBG": "Europa Central",
+ "region_continent_WAW": "Europa Central",
+ "region_continent_BHS": "América do Norte",
+ "region_continent_GRA": "Europa Ocidental",
+ "region_continent_RBX": "Europa Ocidental",
+ "region_continent_GS": "Western Europe",
+ "region_continent_MAD": "Western Europe",
+ "region_continent_BRU": "Western Europe",
+ "region_continent_DE": "Europa Central",
+ "region_continent_UK": "Europa Ocidental",
+ "region_continent_SGP": "Sudeste Asiático",
+ "region_continent_SYD": "Oceânia",
+ "region_continent_US": "Estados Unidos",
+ "region_continent_SHA": "Europa Ocidental",
+ "region_continent_all": "Todas as localizações"
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/App.tsx b/packages/manager/apps/pci-ai-notebooks/src/App.tsx
new file mode 100644
index 000000000000..0656cb0bf69e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/App.tsx
@@ -0,0 +1,34 @@
+import React, { useEffect } from 'react';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { odsSetup } from '@ovhcloud/ods-common-core';
+import { useShell } from '@ovh-ux/manager-react-shell-client';
+
+import '@ovhcloud/ods-theme-blue-jeans';
+import './global.css';
+
+import queryClient from './query.client';
+import Router from '@/routes/Router';
+import Loading from './components/loading/Loading.component';
+import { useLoadingIndicatorContext } from './contexts/LoadingIndicator.context';
+import ProgressLoader from './components/loading/ProgressLoader.component';
+
+odsSetup();
+
+function App() {
+ const { loading } = useLoadingIndicatorContext();
+ const shell = useShell();
+ useEffect(() => {
+ shell.ux.hidePreloader();
+ }, []);
+
+ return (
+
+ {loading && }
+ }>
+
+
+
+ );
+}
+
+export default App;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts
new file mode 100644
index 000000000000..a8c1253ec235
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/aiError.ts
@@ -0,0 +1,10 @@
+import { AIError } from '@/data/api';
+
+export const apiErrorMock = new AIError(
+ 'test',
+ 'test error',
+ new XMLHttpRequest(),
+ { message: 'api error message' },
+ 500,
+ 'statusText',
+);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts
new file mode 100644
index 000000000000..11b8fb4795be
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/authorization.ts
@@ -0,0 +1,5 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedAuthorization: ai.AuthorizationStatus = {
+ authorized: true,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts
new file mode 100644
index 000000000000..dccaac7ac55c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts
@@ -0,0 +1,192 @@
+import { nichandle, order } from '@/types/catalog';
+
+export const mockedCatalogPlan: order.publicOrder.Plan = {
+ addonFamilies: [
+ {
+ name: 'name',
+ },
+ ],
+ configurations: [
+ {
+ isCustom: true,
+ isMandatory: true,
+ name: 'name',
+ },
+ ],
+ invoiceName: 'invoiceName',
+ planCode: 'databases.mongodb-plan-flavor.hour.consumption',
+ pricingType: order.cart.GenericProductPricingTypeEnum.consumption,
+ pricings: [
+ {
+ capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption],
+ commitment: 1,
+ description: 'description',
+ interval: 2,
+ intervalUnit: order.cart.DurationUnitEnum.day,
+ mode: 'mode',
+ mustBeCompleted: true,
+ phase: 14,
+ price: 1,
+ quantity: {
+ min: 1,
+ },
+ repeat: {
+ min: 1,
+ },
+ strategy: order.cart.GenericProductPricingStrategyEnum.stairstep,
+ tax: 1,
+ type: order.cart.GenericProductPricingTypeEnum.consumption,
+ },
+ ],
+ product: 'product',
+};
+
+export const mockedCatalogPlanMonth: order.publicOrder.Plan = {
+ addonFamilies: [
+ {
+ name: 'name',
+ },
+ ],
+ configurations: [
+ {
+ isCustom: true,
+ isMandatory: true,
+ name: 'name1',
+ },
+ ],
+ invoiceName: 'invoiceName1',
+ planCode: 'databases.mongodb-plan-flavor.month.consumption',
+ pricingType: order.cart.GenericProductPricingTypeEnum.consumption,
+ pricings: [
+ {
+ capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption],
+ commitment: 1,
+ description: 'description',
+ interval: 2,
+ intervalUnit: order.cart.DurationUnitEnum.day,
+ mode: 'mode',
+ mustBeCompleted: true,
+ phase: 14,
+ price: 1,
+ quantity: {
+ min: 1,
+ },
+ repeat: {
+ min: 1,
+ },
+ strategy: order.cart.GenericProductPricingStrategyEnum.stairstep,
+ tax: 1,
+ type: order.cart.GenericProductPricingTypeEnum.consumption,
+ },
+ ],
+ product: 'product',
+};
+
+export const mockedCatalogStorageMonth: order.publicOrder.Plan = {
+ addonFamilies: [
+ {
+ name: 'name',
+ },
+ ],
+ configurations: [
+ {
+ isCustom: true,
+ isMandatory: true,
+ name: 'name2',
+ },
+ ],
+ invoiceName: 'invoiceName2',
+ planCode: 'databases.mongodb-plan-additionnal-storage-gb.month.consumption',
+ pricingType: order.cart.GenericProductPricingTypeEnum.consumption,
+ pricings: [
+ {
+ capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption],
+ commitment: 1,
+ description: 'description',
+ interval: 2,
+ intervalUnit: order.cart.DurationUnitEnum.day,
+ mode: 'mode',
+ mustBeCompleted: true,
+ phase: 14,
+ price: 1,
+ quantity: {
+ min: 1,
+ },
+ repeat: {
+ min: 1,
+ },
+ strategy: order.cart.GenericProductPricingStrategyEnum.stairstep,
+ tax: 1,
+ type: order.cart.GenericProductPricingTypeEnum.consumption,
+ },
+ ],
+ product: 'product',
+};
+
+export const mockedCatalogStorageHour: order.publicOrder.Plan = {
+ addonFamilies: [
+ {
+ name: 'name',
+ },
+ ],
+ configurations: [
+ {
+ isCustom: true,
+ isMandatory: true,
+ name: 'name3',
+ },
+ ],
+ invoiceName: 'invoiceName3',
+ planCode: 'databases.mongodb-plan-additionnal-storage-gb.hour.consumption',
+ pricingType: order.cart.GenericProductPricingTypeEnum.consumption,
+ pricings: [
+ {
+ capacities: [order.cart.GenericProductPricingCapacitiesEnum.consumption],
+ commitment: 1,
+ description: 'description',
+ interval: 2,
+ intervalUnit: order.cart.DurationUnitEnum.day,
+ mode: 'mode',
+ mustBeCompleted: true,
+ phase: 14,
+ price: 1,
+ quantity: {
+ min: 1,
+ },
+ repeat: {
+ min: 1,
+ },
+ strategy: order.cart.GenericProductPricingStrategyEnum.stairstep,
+ tax: 1,
+ type: order.cart.GenericProductPricingTypeEnum.consumption,
+ },
+ ],
+ product: 'product',
+};
+
+export const mockedCatalog: order.publicOrder.Catalog = {
+ addons: [
+ mockedCatalogPlan,
+ mockedCatalogPlanMonth,
+ mockedCatalogStorageMonth,
+ mockedCatalogStorageHour,
+ ],
+ catalogId: 1,
+ locale: {
+ currencyCode: order.CurrencyCodeEnum.EUR,
+ subsidiary: nichandle.OvhSubsidiaryEnum.EU,
+ taxRate: 40,
+ },
+ planFamilies: [
+ {
+ name: 'planFamiliesName',
+ },
+ ],
+ plans: [mockedCatalogPlan],
+ products: [
+ {
+ description: 'description',
+ name: 'name',
+ },
+ ],
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts
new file mode 100644
index 000000000000..fa244519142a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/container.ts
@@ -0,0 +1,7 @@
+import { Containers } from '@/types/orderFunnel';
+
+export const mockedContainer: Containers = {
+ status: 'ok',
+ message: 'message',
+ containers: ['container1', 'container2'],
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts
new file mode 100644
index 000000000000..17e9b9de61f0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/datastore.ts
@@ -0,0 +1,59 @@
+import { DataStoresWithContainers } from '@/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook';
+import { DataStoresWithRegion } from '@/hooks/api/ai/datastore/useGetDatastoresWithRegions.hook';
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedDatastoreInput: ai.DataStoreInput = {
+ alias: 'datastoreAlias',
+ credentials: {
+ git: {
+ basicAuth: {
+ password: 'password',
+ username: 'username',
+ },
+ },
+ },
+ endpoint: 'datastoreEndpoints',
+ owner: ai.DataStoreOwnerEnum.customer,
+ type: ai.DataStoreTypeEnum.git,
+};
+
+export const mockedDatastore: ai.DataStore = {
+ alias: 'alias',
+ endpoint: 'endpoint',
+ owner: ai.DataStoreOwnerEnum.customer,
+ type: ai.DataStoreTypeEnum.s3,
+};
+
+export const mockedDatastoreAuth: ai.DataStoreAuth = {
+ accessKey: 'accessKey',
+ region: 'region',
+ s3Url: 's3url',
+ secretKey: 'secretKey',
+ token: 'token',
+ url: 'url',
+};
+
+export const mockedDatastoreWithRegion: DataStoresWithRegion = {
+ alias: 'alias',
+ endpoint: 'endpoint',
+ owner: ai.DataStoreOwnerEnum.customer,
+ type: ai.DataStoreTypeEnum.s3,
+ region: 'GRA',
+};
+
+export const mockedGitWithRegion: DataStoresWithRegion = {
+ alias: 'alias',
+ endpoint: 'endpoint',
+ owner: ai.DataStoreOwnerEnum.customer,
+ type: ai.DataStoreTypeEnum.git,
+ region: 'GRA',
+};
+
+export const mockedDatastoreWithContainer: DataStoresWithContainers = {
+ alias: 'alias',
+ endpoint: 'endpoint',
+ owner: ai.DataStoreOwnerEnum.customer,
+ type: ai.DataStoreTypeEnum.s3,
+ id: 'id',
+ container: 'container2',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts
new file mode 100644
index 000000000000..94a9f84e327c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/flavor.ts
@@ -0,0 +1,21 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedCapabilitiesFlavor: ai.capabilities.Flavor = {
+ default: false,
+ description: 'descriptionFlav',
+ gpuInformation: {
+ gpuBrand: 'gpuBrand',
+ gpuMemory: 15,
+ gpuModel: 'gpuModel',
+ },
+ id: 'flavorId',
+ max: 1,
+ resourcesPerUnit: {
+ cpu: 11,
+ ephemeralStorage: 15,
+ memory: 5,
+ privateNetwork: 5,
+ publicNetwork: 10,
+ },
+ type: ai.capabilities.FlavorTypeEnum.cpu,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts
new file mode 100644
index 000000000000..4c520fc03bda
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/guides.ts
@@ -0,0 +1,19 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedGuides: ai.Guide = {
+ section: 'section',
+ lang: 'lang',
+ slug: 'slug',
+ title: 'title',
+ excerpt: 'excerpt',
+ url: 'https://monguide.ovhai.com',
+};
+
+export const mockedGuideOnboarding: ai.Guide = {
+ section: 'onbaording',
+ lang: 'fr_FR',
+ slug: 'slug',
+ title: 'how to start a notebook',
+ excerpt: 'excerpt',
+ url: 'https://monguide-onboarding.ovhai.com',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts
new file mode 100644
index 000000000000..eb786859b1b1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/job.ts
@@ -0,0 +1,68 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedJobSpec: ai.job.JobSpec = {
+ image: 'image',
+ name: 'name',
+ region: 'region',
+ resources: {
+ cpu: 1,
+ ephemeralStorage: 1,
+ flavor: 'flavor',
+ gpu: 1,
+ memory: 1,
+ privateNetwork: 1,
+ publicNetwork: 1,
+ },
+};
+export const mockedJobStatus: ai.job.JobStatus = {
+ dataSync: [
+ {
+ createdAt: 'createdAt',
+ id: 'datasyncId',
+ status: {
+ info: {
+ code: ai.InfoCodeEnum.APP_RUNNING,
+ message: 'message',
+ },
+ progress: [
+ {
+ completed: 1,
+ createdAt: 'createdAt',
+ deleted: 0,
+ direction: ai.volume.DataSyncEnum.push,
+ failed: 0,
+ id: 'progressId',
+ info: 'info',
+ processed: 3,
+ skipped: 1,
+ state: ai.volume.DataSyncProgressStateEnum.DONE,
+ total: 2,
+ transferredBytes: 30,
+ updatedAt: 'updatedAt',
+ },
+ ],
+ queuedAt: 'queuedAt',
+ state: ai.volume.DataSyncStateEnum.DONE,
+ },
+ updatedAt: 'updatedAt',
+ },
+ ],
+ history: [
+ {
+ date: 'date',
+ state: ai.job.JobStateEnum.DONE,
+ },
+ ],
+ info: {
+ code: ai.InfoCodeEnum.JOB_DONE,
+ message: 'message',
+ },
+};
+export const mockedJob: ai.job.Job = {
+ createdAt: 'createdAt',
+ id: 'jobId',
+ spec: mockedJobSpec,
+ status: mockedJobStatus,
+ updatedAt: 'updatedAt',
+ user: 'user',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts
new file mode 100644
index 000000000000..d3c1c78f9f02
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook.ts
@@ -0,0 +1,78 @@
+import * as ai from '@/types/cloud/project/ai';
+import { mockedJobStatus } from './job';
+
+export const mockedNotebookSpec: ai.notebook.NotebookSpec = {
+ env: {
+ editorId: 'editor',
+ frameworkId: 'frameworkId',
+ frameworkVersion: 'frameworkVersion',
+ },
+ envVars: [
+ {
+ name: 'envVarsName',
+ value: 'envVarsValue',
+ },
+ ],
+ name: 'name',
+ region: 'region',
+ resources: {
+ cpu: 1,
+ ephemeralStorage: 1,
+ flavor: 'flavor',
+ gpu: 1,
+ memory: 1,
+ privateNetwork: 1,
+ publicNetwork: 1,
+ },
+};
+
+export const mockedNotebookStatus: ai.notebook.NotebookStatus = {
+ dataSync: [
+ {
+ createdAt: 'createdAt',
+ id: 'datasyncId',
+ status: {
+ info: {
+ code: ai.InfoCodeEnum.APP_RUNNING,
+ message: 'message',
+ },
+ progress: [
+ {
+ completed: 1,
+ createdAt: 'createdAt',
+ deleted: 0,
+ direction: ai.volume.DataSyncEnum.push,
+ failed: 0,
+ id: 'progressId',
+ info: 'info',
+ processed: 3,
+ skipped: 1,
+ state: ai.volume.DataSyncProgressStateEnum.DONE,
+ total: 2,
+ transferredBytes: 30,
+ updatedAt: 'updatedAt',
+ },
+ ],
+ queuedAt: 'queuedAt',
+ state: ai.volume.DataSyncStateEnum.DONE,
+ },
+ updatedAt: 'updatedAt',
+ },
+ ],
+
+ info: {
+ code: ai.InfoCodeEnum.JOB_DONE,
+ message: 'message',
+ },
+
+ lastJobStatus: mockedJobStatus,
+};
+
+export const mockedNotebook: ai.notebook.Notebook = {
+ createdAt: 'createdAt',
+ id: 'notebookId',
+ spec: mockedNotebookSpec,
+ status: mockedNotebookStatus,
+ updatedAt: 'updatedAt',
+ user: 'user',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts
new file mode 100644
index 000000000000..e839836720ed
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts
@@ -0,0 +1,10 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedEditor: ai.notebook.Editor = {
+ description: 'description',
+ docUrl: 'docURl',
+ id: 'editorId',
+ logoUrl: 'logo',
+ name: 'EditorName',
+ version: 'version',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts
new file mode 100644
index 000000000000..cf546a9996eb
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts
@@ -0,0 +1,10 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedFramework: ai.notebook.Framework = {
+ description: 'description',
+ docUrl: 'docURl',
+ id: 'editorId',
+ logoUrl: 'logo',
+ name: 'EditorName',
+ versions: ['version'],
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts
new file mode 100644
index 000000000000..0430f04061ef
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/project.ts
@@ -0,0 +1,27 @@
+import { AccessTypeEnum } from '@/types/cloud/AccessTypeEnum';
+import { Project } from '@/types/cloud/Project';
+import { ProjectStatusEnum } from '@/types/cloud/project/ProjectStatusEnum';
+
+export const mockedPciProject: Project = {
+ access: AccessTypeEnum.full,
+ creationDate: 'creationDate',
+ manualQuota: false,
+ description: 'description',
+ projectName: 'projectName',
+ project_id: 'projectId',
+ status: ProjectStatusEnum.ok,
+ unleash: true,
+ planCode: 'project.2018',
+};
+
+export const mockedPciDiscoveryProject: Project = {
+ access: AccessTypeEnum.full,
+ creationDate: 'creationDate',
+ manualQuota: false,
+ description: 'description',
+ projectName: 'projectName',
+ project_id: 'projectId',
+ status: ProjectStatusEnum.ok,
+ unleash: true,
+ planCode: 'project.discovery',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts
new file mode 100644
index 000000000000..37d20e3303e4
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/region.ts
@@ -0,0 +1,9 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedCapabilitiesRegion: ai.capabilities.Region = {
+ cliInstallUrl: 'cliInstallUrl',
+ documentationUrl: 'documentationUrl',
+ id: 'GRA',
+ registryUrl: 'registryUrl',
+ version: 'version',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts
new file mode 100644
index 000000000000..df17aeefe6c9
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/registry.ts
@@ -0,0 +1,25 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedRegistryEdit: ai.registry.RegistryUpdateInput = {
+ password: 'password',
+ url: 'url',
+ username: 'username',
+};
+
+export const mockedRegistryInput = {
+ password: 'password',
+ url: 'url',
+ username: 'username',
+ region: 'GRA',
+};
+
+export const mockedRegistry: ai.registry.Registry = {
+ createdAt: '1989/04/08',
+ id: 'id',
+ region: 'GRA',
+ updatedAt: '1989/04/08',
+ user: 'user',
+ password: '',
+ url: 'registryUrl',
+ username: 'username',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts
new file mode 100644
index 000000000000..bf64920e5d25
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/sshkey.ts
@@ -0,0 +1,8 @@
+import * as sshkey from '@/types/cloud/sshkey';
+
+export const mockedSshKey: sshkey.SshKey = {
+ id: 'idSSHKEY',
+ name: 'nameSSHKEY',
+ publicKey: 'publicKey',
+ regions: ['GRA'],
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts
new file mode 100644
index 000000000000..203b13392910
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/storage.ts
@@ -0,0 +1,11 @@
+import * as storage from '@/types/cloud/storage';
+
+export const mockedStorage: storage.Container = {
+ archive: false,
+ containerType: storage.TypeEnum.private,
+ id: 'storageId',
+ name: 'storageName',
+ region: 'region',
+ storedBytes: 0,
+ storedObjects: 0,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts
new file mode 100644
index 000000000000..f71f897bd437
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts
@@ -0,0 +1,30 @@
+import { Suggestions } from '@/types/orderFunnel';
+
+export const mockedSuggestion: Suggestions[] = [
+ {
+ region: 'GRA',
+ ressources: {
+ nb: 1,
+ flavor: 'l4-1-gpu',
+ },
+ framework: {
+ id: 'one-for-all',
+ version: 'v98-ovh.beta.1',
+ },
+ editorId: 'jupyterlab',
+ unsecureHttp: false,
+ },
+ {
+ region: 'BHS',
+ ressources: {
+ nb: 1,
+ flavor: 'ai1-le-1-gpu',
+ },
+ framework: {
+ id: 'one-for-all',
+ version: 'v98-ovh.beta.1',
+ },
+ editorId: 'jupyterlab',
+ unsecureHttp: false,
+ },
+];
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts
new file mode 100644
index 000000000000..e8f4edb2bcfc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/token.ts
@@ -0,0 +1,24 @@
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedTokenCreation: ai.token.TokenSpec = {
+ labelSelector: 'labelSelector',
+ name: 'tokenName',
+ region: 'region',
+ role: ai.TokenRoleEnum.ai_training_operator,
+};
+
+export const mockedToken: ai.token.Token = {
+ createdAt: '1989/04/08',
+ id: 'id',
+ status: {
+ value: 'value',
+ version: 4,
+ },
+ spec: {
+ labelSelector: 'labelSelector',
+ name: 'tokenName',
+ region: 'GRA',
+ role: ai.TokenRoleEnum.ai_training_operator,
+ },
+ updatedAt: '1989/04/08',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts
new file mode 100644
index 000000000000..1880297782fd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user copy.ts
@@ -0,0 +1,42 @@
+import * as user from '@/types/cloud/user';
+import * as ai from '@/types/cloud/project/ai';
+
+export const mockedUserDetails: user.UserDetail = {
+ creationDate: '1989/04/08',
+ description: 'description',
+ id: 25,
+ openstackId: 'openStackId',
+ roles: [
+ {
+ description: 'description',
+ id: 'idRole',
+ name: 'roleName',
+ permissions: ['RO'],
+ },
+ ],
+ status: user.UserStatusEnum.ok,
+ username: 'username',
+ password: 'password',
+};
+
+export const mockedUser: user.User = {
+ creationDate: '1989/04/08',
+ description: 'description',
+ id: 25,
+ openstackId: 'openStackId',
+ roles: [
+ {
+ description: 'description',
+ id: 'idRole',
+ name: 'roleName',
+ permissions: ['RO'],
+ },
+ ],
+ status: user.UserStatusEnum.ok,
+ username: 'username',
+};
+
+export const mockedUserCreation = {
+ description: 'description',
+ role: ai.TokenRoleEnum.ai_training_operator,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts
new file mode 100644
index 000000000000..4efc2f5724dd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/user.ts
@@ -0,0 +1,52 @@
+import { User } from '@ovh-ux/manager-config';
+
+export const mockedUser: User = {
+ nichandle: 'test123',
+ address: 'address',
+ area: 'area',
+ auth: {
+ description: 'description',
+ method: 'method',
+ roles: [],
+ user: 'user',
+ account: '',
+ allowedRoutes: [],
+ identities: [],
+ },
+ birthCity: 'birthCity',
+ birthDay: 'birthDay',
+ certificates: [],
+ city: 'city',
+ companyNationalIdentificationNumber: 0,
+ corporationType: '',
+ country: '',
+ currency: {
+ code: '',
+ format: '',
+ symbol: '',
+ },
+ customerCode: '',
+ email: '',
+ enterprise: false,
+ fax: '',
+ firstname: '',
+ isTrusted: false,
+ italianSDI: '',
+ language: '',
+ legalform: '',
+ name: '',
+ nationalIdentificationNumber: 0,
+ organisation: '',
+ ovhCompany: '',
+ ovhSubsidiary: 'FR',
+ phone: '',
+ phoneCountry: '',
+ sex: '',
+ spareEmail: '',
+ state: '',
+ supportLevel: {
+ level: '',
+ },
+ vat: '',
+ zip: '',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts
new file mode 100644
index 000000000000..9dbaca05689b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/userOVH.ts
@@ -0,0 +1,57 @@
+import { User } from '@ovh-ux/manager-config';
+
+export const mockedUser: User = {
+ nichandle: 'test123',
+ address: 'address',
+ area: 'area',
+ auth: {
+ account: 'account',
+ description: 'description',
+ allowedRoutes: [
+ {
+ method: 'GET',
+ path: 'path',
+ },
+ ],
+ method: 'method',
+ identities: ['identities'],
+ roles: [],
+ user: 'user',
+ },
+ birthCity: 'birthCity',
+ birthDay: 'birthDay',
+ certificates: [],
+ city: 'city',
+ companyNationalIdentificationNumber: 0,
+ corporationType: '',
+ country: '',
+ currency: {
+ code: '',
+ format: '',
+ symbol: '',
+ },
+ customerCode: '',
+ email: '',
+ enterprise: false,
+ fax: '',
+ firstname: '',
+ isTrusted: false,
+ italianSDI: '',
+ language: '',
+ legalform: '',
+ name: '',
+ nationalIdentificationNumber: 0,
+ organisation: '',
+ ovhCompany: '',
+ ovhSubsidiary: '',
+ phone: '',
+ phoneCountry: '',
+ sex: '',
+ spareEmail: '',
+ state: '',
+ supportLevel: {
+ level: '',
+ },
+ vat: '',
+ zip: '',
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts
new file mode 100644
index 000000000000..a27898956a04
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/pointerEvent.ts
@@ -0,0 +1,19 @@
+interface PointerEventProps extends EventInit {
+ button: number;
+ ctrlKey: boolean;
+}
+export class PointerEvent extends Event {
+ button: number;
+
+ ctrlKey: boolean;
+
+ constructor(type: string, props: PointerEventProps) {
+ super(type, props);
+ if (props.button != null) {
+ this.button = props.button;
+ }
+ if (props.ctrlKey != null) {
+ this.ctrlKey = props.ctrlKey;
+ }
+ }
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts
new file mode 100644
index 000000000000..9941ec5ee168
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/selectHelper.ts
@@ -0,0 +1,39 @@
+import { act, fireEvent, screen, waitFor } from '@testing-library/react';
+
+/**
+ * Open a Select component and select the correct option
+ * @param triggerId testId of the trigger select
+ * @param options labels of the options that should be in the select
+ * @param optionToSelect the option to select
+ */
+export async function handleSelectOption(
+ triggerId: string,
+ options: string[],
+ optionToSelect: string,
+) {
+ // Open select
+ act(() => {
+ const trigger = screen.getByTestId(triggerId);
+ fireEvent.focus(trigger);
+ fireEvent.keyDown(trigger, { key: 'Enter', code: 13 });
+ });
+
+ // Check if select has the options
+ await waitFor(() => {
+ expect(screen.getByTestId(triggerId)).not.toHaveAttribute(
+ 'data-state',
+ 'closed',
+ );
+ options.forEach((optionLabel) => {
+ expect(screen.getByText(optionLabel)).toBeInTheDocument();
+ });
+ });
+ // Select the option
+ act(() => {
+ const optionsElements = screen.getAllByRole('option');
+ const elem = optionsElements.find((e) =>
+ e.innerHTML.includes(optionToSelect),
+ );
+ fireEvent.keyDown(elem, { key: 'Enter', code: 13 });
+ });
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx
new file mode 100644
index 000000000000..1b9e387c9f5a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/QueryClientWrapper.tsx
@@ -0,0 +1,10 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+const queryClient = new QueryClient();
+export const QueryClientWrapper = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => (
+ {children}
+);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx
new file mode 100644
index 000000000000..39e1752e38fd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithLocationWrapper.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import {
+ RouterProvider,
+ createHashRouter,
+ MemoryRouter,
+ useLocation,
+} from 'react-router-dom';
+import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context';
+
+/**
+ * Displays the current location in order to test the syncing between the state of the hook and the url
+ */
+export const LocationDisplay = () => {
+ const location = useLocation();
+ return (
+ {`${location.pathname}${location.search}`}
+ );
+};
+
+export const RouterWithLocationWrapper = ({
+ children,
+ initialEntries = ['/test'],
+}: {
+ children: React.ReactNode;
+ initialEntries: string[];
+}) => {
+ return (
+
+ {children}
+
+
+ );
+};
+
+export const HashRouterWithLocationWrapper = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const element = (
+ <>
+ {children}
+
+ >
+ );
+ const router = createHashRouter([
+ {
+ path: '/',
+ element,
+ },
+ ]);
+ return (
+
+
+
+ );
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx
new file mode 100644
index 000000000000..5728c38997bc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/wrappers/RouterWithQueryClientWrapper.tsx
@@ -0,0 +1,167 @@
+import { vi } from 'vitest';
+import { ShellClientApi } from '@ovh-ux/shell';
+import { Environment } from '@ovh-ux/manager-config';
+import { ShellProvider } from '@ovh-ux/manager-react-shell-client';
+import { HashRouterWithLocationWrapper } from './RouterWithLocationWrapper';
+import { QueryClientWrapper } from './QueryClientWrapper';
+import { USER_INACTIVITY_TIMEOUT } from '@/configuration/polling.constants';
+import { mockedUser } from '../mocks/user';
+import { UserActivityProvider } from '@/contexts/UserActivityContext';
+
+export const RouterWithQueryClientWrapper = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const client = {
+ environment: {
+ region: 'EU',
+ userLocale: 'en-GB',
+ version: '1',
+ user: mockedUser,
+ applicationName: 'pci-ai-notebooks',
+ universe: 'pci',
+ applicationURLs: {},
+ message: {
+ nl: {
+ description: '',
+ },
+ fr: {
+ description: '',
+ },
+ en: {
+ description: '',
+ },
+ de: {
+ description: '',
+ },
+ es: {
+ description: '',
+ },
+ it: {
+ description: '',
+ },
+ pl: {
+ description: '',
+ },
+ pt: {
+ description: '',
+ },
+ },
+ applications: {},
+ setRegion: vi.fn(),
+ getRegion: vi.fn(),
+ setUser: vi.fn(),
+ getUser: vi.fn(),
+ setUserLocale: vi.fn(),
+ getUserLocale: vi.fn(),
+ getUserLanguage: vi.fn(),
+ setVersion: vi.fn(),
+ getVersion: vi.fn(),
+ setApplicationName: vi.fn(),
+ getApplicationName: vi.fn(),
+ getApplication: vi.fn(),
+ setUniverse: vi.fn(),
+ setUniverseFromApplicationId: vi.fn(),
+ getUniverse: vi.fn(),
+ setApplicationURLs: vi.fn(),
+ getApplicationURLs: vi.fn(),
+ getApplicationURL: vi.fn(),
+ setMessage: vi.fn(),
+ getMessage: vi.fn(),
+ getApplications: vi.fn(),
+ setApplications: vi.fn(),
+ },
+ shell: {
+ environment: {
+ getEnvironment: vi.fn(),
+ setUniverse: vi.fn(),
+ setApplication: vi.fn(),
+ },
+ i18n: {
+ getLocale: vi.fn(),
+ onLocaleChange: vi.fn(),
+ setLocale: vi.fn(),
+ getAvailableLocales: vi.fn(),
+ },
+ routing: {
+ listenForHashChange: vi.fn(),
+ stopListenForHashChange: vi.fn(),
+ onHashChange: vi.fn(),
+ },
+ auth: {
+ login: vi.fn(),
+ logout: vi.fn(),
+ },
+ ux: {
+ showAccountSidebar: vi.fn(),
+ disableAccountSidebarToggle: vi.fn(),
+ enableAccountSidebarToggle: vi.fn(),
+ isAccountSidebarVisible: vi.fn(),
+ setForceAccountSiderBarDisplayOnLargeScreen: vi.fn(),
+ resetAccountSidebar: vi.fn(),
+ isMenuSidebarVisible: vi.fn(),
+ showMenuSidebar: vi.fn(),
+ updateMenuSidebarItemLabel: vi.fn(),
+ onRequestClientSidebarOpen: vi.fn(),
+ getSSOAuthModalMode: vi.fn(),
+ getUserIdCookie: vi.fn(),
+ onOpenChatbot: vi.fn(),
+ onCloseChatbot: vi.fn(),
+ onReduceChatbot: vi.fn(),
+ onChatbotOpen: vi.fn(),
+ onChatbotClose: vi.fn(),
+ openLiveChat: vi.fn(),
+ openChatbot: vi.fn(),
+ closeChatbot: vi.fn(),
+ isChatbotReduced: vi.fn(),
+ isChatbotVisible: vi.fn(),
+ startProgress: vi.fn(),
+ stopProgress: vi.fn(),
+ hidePreloader: vi.fn(),
+ showPreloader: vi.fn(),
+ },
+ navigation: {
+ getURL: vi.fn(),
+ navigateTo: vi.fn(),
+ reload: vi.fn(),
+ },
+ tracking: {
+ setPciProjectMode: vi.fn(),
+ init: vi.fn(),
+ onConsentModalDisplay: vi.fn(),
+ onUserConsentFromModal: vi.fn(),
+ setConfig: vi.fn(),
+ setDefaults: vi.fn(),
+ setRegion: vi.fn(),
+ trackClick: vi.fn(),
+ trackClickImpression: vi.fn(),
+ trackEvent: vi.fn(),
+ trackImpression: vi.fn(),
+ trackMVTest: vi.fn(),
+ trackPage: vi.fn(),
+ },
+ logger: {
+ log: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ debug: vi.fn(),
+ },
+ },
+ } as {
+ shell: ShellClientApi;
+ environment: Environment;
+ };
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts
new file mode 100644
index 000000000000..8cd2e90548c9
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/setupTest.ts
@@ -0,0 +1,7 @@
+import '@testing-library/jest-dom';
+import { PointerEvent } from './helpers/pointerEvent';
+
+// use a custom pointerEvent as jest does not implement it.
+// it is requiered for DropdownMenus
+// source: https://github.com/radix-ui/primitives/issues/856#issuecomment-928704064
+window.PointerEvent = PointerEvent as any;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx
new file mode 100644
index 000000000000..c1ec45111e9e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.component.tsx
@@ -0,0 +1,83 @@
+import React, { useState } from 'react';
+import { Params, useParams, useLocation, useMatches } from 'react-router-dom';
+import { useNavigation } from '@ovh-ux/manager-react-shell-client';
+import usePciProject from '@/hooks/api/project/usePciProject.hook';
+import { Skeleton } from '../ui/skeleton';
+import Link from '@/components/links/Link.component';
+import A from '@/components/links/A.component';
+
+export type BreadcrumbHandleParams = {
+ data: unknown;
+ params: Params;
+};
+
+export interface MatchWithBreadcrumb {
+ id: string;
+ pathname: string;
+ params: Params;
+ data: unknown;
+ handle: {
+ breadcrumb?: (breadcrumbParams: {
+ params: Params;
+ data: unknown;
+ }) => React.ReactElement | null;
+ };
+}
+
+function Breadcrumb(): JSX.Element {
+ const { projectId } = useParams();
+ const { data: project } = usePciProject();
+
+ const location = useLocation();
+
+ const navigation = useNavigation();
+ const [baseUrl, setBaseUrl] = useState('');
+ const matches = useMatches() as MatchWithBreadcrumb[];
+ const [breadcrumbData, setBreadcrumbData] = React.useState([]);
+
+ React.useEffect(() => {
+ const breadcrumbArray = matches
+ .filter((match) => Boolean(match.handle?.breadcrumb))
+ .map((match) => {
+ return {
+ path: match.pathname,
+ label: match.handle.breadcrumb(match),
+ };
+ });
+ setBreadcrumbData(breadcrumbArray);
+ }, [location.pathname]);
+
+ React.useEffect(() => {
+ const updateNav = async () => {
+ const url = await navigation.getURL('public-cloud', ``, {});
+ setBaseUrl(url as string);
+ };
+ updateNav();
+ }, [navigation]);
+
+ return (
+ <>
+
+ {project?.description ?? (
+
+ )}
+
+ {breadcrumbData.map((bc, index) => (
+
+ |
+ {index < breadcrumbData.length - 1 ? (
+
+ {bc.label}
+
+ ) : (
+
+ {bc.label}
+
+ )}
+
+ ))}
+ >
+ );
+}
+
+export default Breadcrumb;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx
new file mode 100644
index 000000000000..9eba67a82652
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/Breadcrumb.spec.tsx
@@ -0,0 +1,66 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import Breadcrumb from '@/components/breadcrumb/Breadcrumb.component';
+import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper';
+
+vi.mock('react-router-dom', async () => {
+ const mod = await vi.importActual('react-router-dom');
+ return {
+ ...mod,
+ useParams: () => ({
+ projectId: '123456',
+ }),
+ useMatches: () => [
+ {
+ id: '1',
+ pathname: 'users',
+ data: {},
+ handle: {
+ breadcrumb: () => users,
+ },
+ },
+ {
+ id: '2',
+ pathname: 'userName',
+ data: {},
+ handle: {
+ breadcrumb: () => userName,
+ },
+ },
+ ],
+ };
+});
+
+vi.mock('@/data/api/project/project.api', () => {
+ return {
+ getProject: vi.fn(() => ({
+ project_id: '123456',
+ projectName: 'projectName',
+ description: 'description',
+ })),
+ };
+});
+vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => {
+ const mod = await importOriginal<
+ typeof import('@ovh-ux/manager-react-shell-client')
+ >();
+ return {
+ ...mod,
+ useNavigation: () => ({
+ getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`),
+ }),
+ };
+});
+
+describe('Breadcrumb component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should display the breadcrumb component', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ await waitFor(() => {
+ expect(screen.getByText('description')).toBeInTheDocument();
+ expect(screen.getByText('users')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx
new file mode 100644
index 000000000000..9d7164f1b24d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.component.tsx
@@ -0,0 +1,12 @@
+import { useTranslation } from 'react-i18next';
+
+interface BreadcrumbItemProps {
+ translationKey: string;
+ namespace: string;
+}
+const BreadcrumbItem = ({ translationKey, namespace }: BreadcrumbItemProps) => {
+ const { t } = useTranslation(namespace);
+ return t(translationKey);
+};
+
+export default BreadcrumbItem;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx
new file mode 100644
index 000000000000..26c0cb50a235
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/breadcrumb/BreadcrumbItem.spec.tsx
@@ -0,0 +1,24 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component';
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe('BreadcrumbItem component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should display the breadcrumb item component', async () => {
+ const translationKey = 'testKey';
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByText(translationKey)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx
new file mode 100644
index 000000000000..a38aceed6c35
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/cli-code-block/CliCodeBlock.component.tsx
@@ -0,0 +1,49 @@
+import { Copy } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { useToast } from '../ui/use-toast';
+import { Button } from '../ui/button';
+import { ScrollArea } from '../ui/scroll-area';
+
+interface CliCodeBlockProps {
+ title: string;
+ code: string;
+ toastMessage?: string;
+}
+
+const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => {
+ const { t } = useTranslation('common');
+ const toast = useToast();
+ const handleCopyPass = (valueToCopy: string) => {
+ navigator.clipboard.writeText(valueToCopy);
+ toast.toast({
+ title: toastMessage || t('copied'),
+ });
+ };
+
+ return (
+
+
+
{title}
+
+
+
+
+ {code}
+
+
+
+ );
+};
+
+export default CliCodeBlock;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx
new file mode 100644
index 000000000000..c51a6da30cf8
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.component.tsx
@@ -0,0 +1,74 @@
+import { useNavigation } from '@ovh-ux/manager-react-shell-client';
+import { useRouteError } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import errorImgSrc from '@/../public/assets/oops.png';
+import { Alert, AlertDescription } from '../ui/alert';
+import { Button } from '../ui/button';
+
+const ErrorBoundary = () => {
+ const error = useRouteError();
+ const { t } = useTranslation('error');
+ const nav = useNavigation();
+
+ const navigateToHomepage = () => {
+ nav.navigateTo('public-cloud', '', {});
+ };
+ const reloadPage = () => {
+ nav.reload();
+ };
+
+ if (error instanceof Error) {
+ return (
+
+
+
+
+
+
{t('manager_error_page_title')}
+
+
+ {t('manager_error_page_default')}
+ {error.message}
+ {'xOvhQueryId' in error && (
+
+ {t('manager_error_page_detail_code', {
+ code: error.xOvhQueryId,
+ })}
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+ }
+ return <>>;
+};
+
+export default ErrorBoundary;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx
new file mode 100644
index 000000000000..6c60627a0fe6
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/error-boundary/ErrorBoundary.spec.tsx
@@ -0,0 +1,114 @@
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import * as RouterDom from 'react-router-dom';
+import { useNavigation } from '@ovh-ux/manager-react-shell-client';
+import ErrorBoundary from '@/components/error-boundary/ErrorBoundary.component';
+import { RouterWithQueryClientWrapper } from '../../__tests__/helpers/wrappers/RouterWithQueryClientWrapper';
+
+vi.mock('react-router-dom', async () => {
+ const mod = await vi.importActual('react-router-dom');
+ return {
+ ...mod,
+ useRouteError: vi.fn(() => new Error('Test error')),
+ };
+});
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string, replaceObject?: Record) => {
+ if (!replaceObject) {
+ return key;
+ }
+ const replacements = Object.keys(replaceObject)
+ .map((replaceKey) => `_${replaceKey}:${replaceObject[replaceKey]}`)
+ .join('');
+ return `${key}${replacements}`;
+ },
+ }),
+}));
+
+vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => {
+ const mod = await importOriginal<
+ typeof import('@ovh-ux/manager-react-shell-client')
+ >();
+ const navigateTo = vi.fn();
+ const reload = vi.fn();
+ return {
+ ...mod,
+ useNavigation: () => ({
+ navigateTo,
+ reload,
+ }),
+ };
+});
+
+describe('ErrorBoundary component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should display the error boundary component', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ await waitFor(() => {
+ expect(screen.getByText('Test error')).toBeInTheDocument();
+ });
+ });
+ it('should navigate to home page when button is clicked', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ act(() => {
+ fireEvent.click(screen.getByTestId('errorBoundaryGoToHomepage'));
+ });
+ await waitFor(async () => {
+ expect(screen.getByText('Test error')).toBeInTheDocument();
+ expect(useNavigation().navigateTo).toHaveBeenCalled();
+ expect(useNavigation().reload).not.toHaveBeenCalled();
+ });
+ });
+ it('should reload page when button is clicked', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ act(() => {
+ fireEvent.click(screen.getByTestId('errorBoundaryReload'));
+ });
+ await waitFor(async () => {
+ expect(screen.getByText('Test error')).toBeInTheDocument();
+ expect(useNavigation().navigateTo).not.toHaveBeenCalled();
+ expect(useNavigation().reload).toHaveBeenCalled();
+ });
+ });
+ it('should display the ovhquery if it is provided in the error', async () => {
+ class ErrorWithQueryId extends Error {
+ xOvhQueryId: string;
+
+ constructor(message: string, xOvhQueryId: string) {
+ super(message);
+ this.xOvhQueryId = xOvhQueryId;
+ }
+ }
+ const errorMessage = 'Error with id';
+ const queryId = 'mockedQueryId';
+ vi.mocked(RouterDom.useRouteError).mockImplementation(
+ () => new ErrorWithQueryId(errorMessage, queryId),
+ );
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ await waitFor(async () => {
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ expect(
+ screen.getByText((content) => content.includes(queryId)),
+ ).toBeInTheDocument();
+ });
+ });
+ it('should not render anything if error is not of type Error', async () => {
+ vi.mocked(RouterDom.useRouteError).mockImplementation(() => null);
+ const { container } = render(, {
+ wrapper: RouterWithQueryClientWrapper,
+ });
+ await waitFor(async () => {
+ expect(container.childNodes).toHaveLength(1);
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx
new file mode 100644
index 000000000000..703bd2fe396c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.component.tsx
@@ -0,0 +1,17 @@
+import { useMemo } from 'react';
+import { useLocale } from '@/hooks/useLocale';
+
+interface TableDateCellProps {
+ date: Date;
+ options?: Intl.DateTimeFormatOptions;
+}
+
+const FormattedDate = ({ date, options }: TableDateCellProps) => {
+ const locale = useLocale();
+ const formater = useMemo(
+ () => new Intl.DateTimeFormat(locale.replace('_', '-'), options),
+ [locale],
+ );
+ return formater.format(date);
+};
+export default FormattedDate;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx
new file mode 100644
index 000000000000..4bc90cde7770
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/formatted-date/FormattedDate.spec.tsx
@@ -0,0 +1,76 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import { useShell } from '@ovh-ux/manager-react-shell-client';
+import FormattedDate from '@/components/formatted-date/FormattedDate.component';
+import { Locale } from '@/hooks/useLocale';
+
+vi.mock('@ovh-ux/manager-react-shell-client', () => {
+ type CallbackType = (localePros: { locale: string }) => void;
+ let localeChangeCallback: CallbackType | null = null;
+ const onLocaleChange = (callback: CallbackType) => {
+ localeChangeCallback = callback;
+ };
+ const getLocale = vi.fn();
+ return {
+ useShell: vi.fn(() => ({
+ i18n: {
+ getLocale,
+ onLocaleChange,
+ setLocale: vi.fn((newLocale: string) => {
+ if (localeChangeCallback) {
+ localeChangeCallback({ locale: newLocale });
+ }
+ }),
+ },
+ })),
+ };
+});
+
+describe('FormattedDate component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should render the date with french locale', async () => {
+ const date = new Date(2024, 5, 8, 12, 42, 50, 151);
+ vi.mocked(useShell().i18n.getLocale).mockResolvedValue(Locale.fr_FR);
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('08/06/2024')).toBeInTheDocument();
+ });
+ });
+ it('should render the date with PT locale', async () => {
+ vi.mocked(useShell().i18n.getLocale).mockResolvedValue(() => Locale.pt_PT);
+ const date = new Date(2024, 5, 8, 12, 42, 50, 151);
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(
+ screen.getByText('8 de junho de 2024 às 12:42:50'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should render the date with EN locale', async () => {
+ vi.mocked(useShell().i18n.getLocale).mockResolvedValue(() => Locale.en_GB);
+ const date = new Date(2024, 5, 8, 12, 42, 50, 151);
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByText('8 June 2024 at 12:42:50')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx
new file mode 100644
index 000000000000..6cba69f1983f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.component.tsx
@@ -0,0 +1,103 @@
+import { useTranslation } from 'react-i18next';
+import { useEffect, useState } from 'react';
+import { BookOpen } from 'lucide-react';
+
+import { useParams } from 'react-router-dom';
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '../ui/command';
+
+import { Button } from '../ui/button';
+import { Skeleton } from '../ui/skeleton';
+import * as ai from '@/types/cloud/project/ai';
+import { useGetGuides } from '@/hooks/api/ai/guide/useGetGuides.hook';
+import { useLocale } from '@/hooks/useLocale';
+
+interface GuidesProps {
+ section?: string;
+ onGuideClick?: (guide: ai.Guide) => void;
+}
+const Guides = ({ section, onGuideClick }: GuidesProps) => {
+ const { projectId } = useParams();
+ const { t } = useTranslation('guides');
+ const locale = useLocale();
+ const [open, setOpen] = useState(false);
+ const guidesQuery = useGetGuides(
+ projectId,
+ section,
+ locale.toLocaleLowerCase().replace('_', '-'),
+ );
+ // open the menu on cmd + j
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((prevValue) => !prevValue);
+ }
+ };
+
+ document.addEventListener('keydown', down);
+ return () => document.removeEventListener('keydown', down);
+ }, []);
+ // open a guide in a new tab
+ const openGuide = (guide: ai.Guide) => {
+ if (onGuideClick) {
+ onGuideClick(guide);
+ }
+ const { url } = guide;
+ if (url && /^(http|https):\/\//i.test(url)) {
+ const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
+ if (newWindow) newWindow.opener = null;
+ }
+ };
+ if (guidesQuery.isFetching)
+ return ;
+ if (guidesQuery.data?.length === 0) return <>>;
+ return (
+ <>
+
+
+
+
+ {t('noResult')}
+
+ {guidesQuery.data
+ ?.sort((a, b) => a.title.localeCompare(b.title))
+ .map((guide) => (
+ openGuide(guide)}
+ className="cursor-pointer flex-col items-start"
+ >
+ {guide.title}
+ {guide.excerpt}
+
+ ))}
+
+
+
+ >
+ );
+};
+
+export default Guides;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx
new file mode 100644
index 000000000000..dabe80fd36ad
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/guides/Guides.spec.tsx
@@ -0,0 +1,121 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+
+import Guides from './Guides.component';
+
+import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper';
+
+import { Locale } from '@/hooks/useLocale';
+import {
+ mockedGuideOnboarding,
+ mockedGuides,
+} from '@/__tests__/helpers/mocks/guides';
+
+describe('Guides component', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+
+ // Mock necessary hooks and dependencies
+ vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+ }));
+ vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => {
+ const mod = await importOriginal<
+ typeof import('@ovh-ux/manager-react-shell-client')
+ >();
+ return {
+ ...mod,
+ useShell: vi.fn(() => ({
+ i18n: {
+ getLocale: vi.fn(() => Locale.fr_FR),
+ onLocaleChange: vi.fn(),
+ setLocale: vi.fn(),
+ },
+ })),
+ };
+ });
+
+ vi.mock('@/data/api/ai/guide.api', () => ({
+ getGuides: vi.fn(() => [mockedGuides, mockedGuideOnboarding]),
+ }));
+
+ const ResizeObserverMock = vi.fn(() => ({
+ observe: vi.fn(),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+ }));
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
+ });
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders the skeleton component while loading', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ await waitFor(() => {
+ expect(screen.getByTestId('guide-skeleton')).toBeInTheDocument();
+ });
+ });
+ it('renders the guide button, open with ctrl+k and close', async () => {
+ render(, { wrapper: RouterWithQueryClientWrapper });
+ await waitFor(() => {
+ expect(screen.getByTestId('guide-open-button')).toBeInTheDocument();
+ });
+ act(() => {
+ fireEvent.keyDown(window, { key: 'k', ctrlKey: true });
+ });
+ const trigger = screen.getByTestId('guide-open-button');
+ fireEvent.keyDown(trigger, {
+ key: 'k',
+ ctrlKey: true,
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('guide-header')).toBeInTheDocument();
+ });
+ act(() => {
+ fireEvent.keyDown(screen.getByTestId('guide-header'), {
+ key: 'Escape',
+ code: 'Escape',
+ });
+ });
+ await waitFor(() => {
+ expect(screen.queryByTestId('guide-header')).not.toBeInTheDocument();
+ });
+ });
+ it('open the guide component on button click and click on a guide', async () => {
+ Object.assign(window, {
+ open: vi.fn().mockImplementation(() => Promise.resolve()),
+ });
+ const mockedOnGuideClick = vi.fn();
+ render(, {
+ wrapper: RouterWithQueryClientWrapper,
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('guide-open-button')).toBeInTheDocument();
+ });
+ act(() => {
+ fireEvent.click(screen.getByTestId('guide-open-button'));
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('guide-header')).toBeInTheDocument();
+ expect(screen.getByTestId(mockedGuides.url)).toBeInTheDocument();
+ });
+ act(() => {
+ fireEvent.click(screen.getByTestId(mockedGuides.url));
+ });
+ await waitFor(() => {
+ expect(window.open).toHaveBeenCalledWith(
+ mockedGuides.url,
+ '_blank',
+ 'noopener,noreferrer',
+ );
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx
new file mode 100644
index 000000000000..fadd9f43be6d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.component.tsx
@@ -0,0 +1,38 @@
+import { cn } from '@/lib/utils';
+
+function A({
+ className,
+ children,
+ disabled,
+ ...props
+}: React.AnchorHTMLAttributes & { disabled?: boolean }) {
+ const baseClassName =
+ 'text-primary-500 font-semibold outiline-none cursor-pointer no-underline hover:text-primary-700 hover:underline';
+ const disabledClass = 'opacity-50 cursor-not-allowed hover:text-primary-500';
+ const combinedClassName = cn(
+ baseClassName,
+ className,
+ disabled && disabledClass,
+ );
+ if (disabled) {
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
+ const { href, onClick, tabIndex, ...otherProps } = props;
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+export default A;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx
new file mode 100644
index 000000000000..95a22c88888c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/A.spec.tsx
@@ -0,0 +1,34 @@
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { vi } from 'vitest';
+import A from '@/components/links/A.component';
+
+vi.mock('@ovh-ux/manager-react-shell-client', () => ({
+ useNavigation: () => ({
+ getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`),
+ }),
+}));
+
+describe('A', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders anchor element correctly', () => {
+ const { container } = render(Link);
+ const anchor = container.querySelector('a');
+ expect(anchor).toBeInTheDocument();
+ expect(anchor).toHaveTextContent('Link');
+ expect(anchor).toHaveAttribute('href', '#');
+ });
+ it('renders disabled anchor element correctly', () => {
+ const { container } = render(
+
+ Link
+ ,
+ );
+ const anchor = container.querySelector('a');
+ expect(anchor).toBeInTheDocument();
+ expect(anchor).toHaveTextContent('Link');
+ expect(anchor).not.toHaveAttribute('href');
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx
new file mode 100644
index 000000000000..21ac9352e5b0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.component.tsx
@@ -0,0 +1,39 @@
+import { LinkProps, Link as RouterLink } from 'react-router-dom';
+import { cn } from '@/lib/utils';
+import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context';
+
+function Link({
+ className,
+ disabled,
+ children,
+ to,
+ ...props
+}: LinkProps & { disabled?: boolean }) {
+ const { setLoading } = useLoadingIndicatorContext();
+ const baseClassName =
+ 'text-primary-500 font-semibold outiline-none cursor-pointer no-underline hover:text-primary-700 hover:underline';
+ const disabledClass = 'opacity-50 cursor-not-allowed hover:text-primary-500';
+ const combinedClassName = cn(
+ baseClassName,
+ className,
+ disabled && disabledClass,
+ );
+ return (
+ {
+ if (disabled) {
+ e.preventDefault();
+ } else {
+ setLoading(true);
+ }
+ }}
+ >
+ {children}
+
+ );
+}
+
+export default Link;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx
new file mode 100644
index 000000000000..bb32dd135da8
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/Link.spec.tsx
@@ -0,0 +1,85 @@
+import { act, fireEvent, render } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { vi } from 'vitest';
+import { BrowserRouter as Router } from 'react-router-dom';
+import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context';
+import Link from './Link.component';
+
+vi.mock('@ovh-ux/manager-react-shell-client', () => ({
+ useNavigation: () => ({
+ getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`),
+ }),
+}));
+
+describe('Link', () => {
+ const useLoadingIndicatorSpy = vi.spyOn(
+ useLoadingIndicator,
+ 'useLoadingIndicatorContext',
+ );
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders RouterLink element correctly', () => {
+ const { container } = render(
+
+
+ Link
+
+ ,
+ );
+ const routerLink = container.querySelector('a');
+ expect(routerLink).toBeInTheDocument();
+ expect(routerLink).toHaveTextContent('Link');
+ expect(routerLink).toHaveAttribute('href', '/route');
+ });
+
+ it('should set loading indicator on click', async () => {
+ const mockLoading = vi.fn();
+ useLoadingIndicatorSpy.mockImplementation(() => ({
+ loading: false,
+ setLoading: mockLoading,
+ }));
+ const { container } = render(
+
+
+ Link
+
+ ,
+ );
+
+ const routerLink = container.querySelector('a');
+ expect(mockLoading).not.toHaveBeenCalled();
+ act(() => {
+ if (routerLink) {
+ fireEvent.click(routerLink);
+ }
+ });
+ expect(mockLoading).toHaveBeenCalled();
+ });
+
+ it('should not set loading indicator when clicked on disabled link', async () => {
+ const mockLoading = vi.fn();
+ useLoadingIndicatorSpy.mockImplementation(() => ({
+ loading: false,
+ setLoading: mockLoading,
+ }));
+ const { container } = render(
+
+
+
+ Link
+
+
+ ,
+ );
+
+ const routerLink = container.querySelector('a');
+ expect(mockLoading).not.toHaveBeenCalled();
+ act(() => {
+ if (routerLink) {
+ fireEvent.click(routerLink);
+ }
+ });
+ expect(mockLoading).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx
new file mode 100644
index 000000000000..d094d9708f88
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.component.tsx
@@ -0,0 +1,45 @@
+import { NavLink as RouterNavLink, NavLinkProps } from 'react-router-dom';
+import { cn } from '@/lib/utils';
+import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context';
+
+function NavLink({
+ className,
+ disabled,
+ children,
+ to,
+ end,
+ ...props
+}: NavLinkProps & { disabled?: boolean }) {
+ const { setLoading } = useLoadingIndicatorContext();
+ const baseClassName =
+ 'whitespace-nowrap w-fit text-primary-500 text-base font-semibold m-0 py-2 hover:text-primary-700';
+ const activeClass = 'border-b-2 border-primary-500';
+ const disabledClass = 'cursor-not-allowed opacity-50 hover:text-primary-500';
+
+ return (
+
+ cn(
+ baseClassName,
+ isActive && activeClass,
+ className,
+ disabled && disabledClass,
+ )
+ }
+ onClick={(e) => {
+ if (disabled) {
+ e.preventDefault();
+ } else {
+ setLoading(true);
+ }
+ }}
+ {...props}
+ >
+ {children}
+
+ );
+}
+
+export default NavLink;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx
new file mode 100644
index 000000000000..a800713bd98f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/NavLink.spec.tsx
@@ -0,0 +1,84 @@
+import { act, fireEvent, render } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { vi } from 'vitest';
+import { BrowserRouter as Router } from 'react-router-dom';
+import NavLink from '@/components/links/NavLink.component';
+import * as useLoadingIndicator from '@/contexts/LoadingIndicator.context';
+
+vi.mock('@ovh-ux/manager-react-shell-client', () => ({
+ useNavigation: () => ({
+ getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`),
+ }),
+}));
+
+describe('NavLink', () => {
+ const useLoadingIndicatorSpy = vi.spyOn(
+ useLoadingIndicator,
+ 'useLoadingIndicatorContext',
+ );
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders NavLink element correctly', () => {
+ const { container } = render(
+
+
+ Link
+
+ ,
+ );
+ const routerLink = container.querySelector('a');
+ expect(routerLink).toBeInTheDocument();
+ expect(routerLink).toHaveTextContent('Link');
+ expect(routerLink).toHaveAttribute('href', '/route');
+ });
+ it('should set loading indicator on click', async () => {
+ const mockLoading = vi.fn();
+ useLoadingIndicatorSpy.mockImplementation(() => ({
+ loading: false,
+ setLoading: mockLoading,
+ }));
+ const { container } = render(
+
+
+ Link
+
+ ,
+ );
+
+ const routerLink = container.querySelector('a');
+ expect(mockLoading).not.toHaveBeenCalled();
+ act(() => {
+ if (routerLink) {
+ fireEvent.click(routerLink);
+ }
+ });
+ expect(mockLoading).toHaveBeenCalled();
+ });
+
+ it('should not set loading indicator when clicked on disabled link', async () => {
+ const mockLoading = vi.fn();
+ useLoadingIndicatorSpy.mockImplementation(() => ({
+ loading: false,
+ setLoading: mockLoading,
+ }));
+ const { container } = render(
+
+
+
+ Link
+
+
+ ,
+ );
+
+ const routerLink = container.querySelector('a');
+ expect(mockLoading).not.toHaveBeenCalled();
+ act(() => {
+ if (routerLink) {
+ fireEvent.click(routerLink);
+ }
+ });
+ expect(mockLoading).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx
new file mode 100644
index 000000000000..541426114437
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.component.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import { useNavigation } from '@ovh-ux/manager-react-shell-client';
+import A from './A.component';
+
+interface OvhLinkProps {
+ application: string;
+ path: string;
+ params?: Record;
+}
+function OvhLink({
+ application,
+ path,
+ params = {},
+ children,
+ ...props
+}: React.AnchorHTMLAttributes &
+ OvhLinkProps & { disabled?: boolean }) {
+ const navigation = useNavigation();
+ const [url, setUrl] = React.useState('');
+ React.useEffect(() => {
+ const fetchUrl = async (urlParams: OvhLinkProps) => {
+ const goTo = (await navigation.getURL(
+ urlParams.application,
+ urlParams.path,
+ urlParams.params,
+ )) as string;
+ setUrl(goTo);
+ };
+ fetchUrl({ application, path, params });
+ }, [application, path, params, navigation]);
+ return (
+
+ {children}
+
+ );
+}
+
+export default OvhLink;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx
new file mode 100644
index 000000000000..3fc02db92261
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/links/OvhLink.spec.tsx
@@ -0,0 +1,27 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import { vi } from 'vitest';
+import OvhLink from './OvhLink.component';
+
+vi.mock('@ovh-ux/manager-react-shell-client', () => ({
+ useNavigation: () => ({
+ getURL: vi.fn((app: string, path: string) => `#mockedurl-${app}${path}`),
+ }),
+}));
+
+describe('OvhLink component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should render anchor element with href fetched from navigation', async () => {
+ render(
+
+ Link
+ ,
+ );
+ const anchorElement = screen.getByText('Link');
+ await waitFor(() => {
+ expect(anchorElement).toHaveAttribute('href', '#mockedurl-app/some-path');
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx
new file mode 100644
index 000000000000..d534bff440af
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.component.tsx
@@ -0,0 +1,11 @@
+import { OsdsSpinner } from '@ovhcloud/ods-components/react';
+
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx
new file mode 100644
index 000000000000..a6a91d99c966
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/Loading.spec.tsx
@@ -0,0 +1,13 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import Loading from '@/components/loading/Loading.component';
+
+describe('Loading component', () => {
+ it('should display the loading component', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByTestId('loading-container')).toBeInTheDocument();
+ expect(screen.getByTestId('osds-spinner')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx
new file mode 100644
index 000000000000..7e0cca660f09
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.component.tsx
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+
+export default function ProgressLoader() {
+ const [progress, setProgress] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setProgress((prevProgress) => {
+ // Increment progress in a non-linear fashion for a more dynamic feel
+ const increment = Math.random() * 20;
+ const nextProgress = prevProgress + increment;
+ if (nextProgress < 95) {
+ return nextProgress;
+ }
+ clearInterval(interval); // Stop incrementing near 100% to mimic NProgress behavior
+ return prevProgress;
+ });
+ }, 500); // Increase progress more frequently
+
+ return () => clearInterval(interval); // Cleanup on component unmount
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx
new file mode 100644
index 000000000000..d68bbef047da
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/loading/ProgressLoader.spec.tsx
@@ -0,0 +1,76 @@
+import { act, render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import ProgressLoader from '@/components/loading/ProgressLoader.component';
+
+describe('ProgressLoader component', () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+ });
+
+ afterEach(() => {
+ vi.runOnlyPendingTimers();
+ vi.useRealTimers();
+ });
+ it('should display the loader component', async () => {
+ render();
+ act(() => {
+ vi.clearAllTimers();
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('progressLoaderContainer')).toBeInTheDocument();
+ expect(screen.getByTestId('progressLoaderBar')).toBeInTheDocument();
+ });
+ });
+ it('should increment the progress over time', async () => {
+ render();
+ const initialWidth = screen.getByTestId('progressLoaderBar').style.width;
+ act(() => {
+ vi.advanceTimersByTime(500); // Advance time by 500ms
+ vi.clearAllTimers();
+ });
+
+ await waitFor(() => {
+ const newWidth = screen.getByTestId('progressLoaderBar').style.width;
+ expect(newWidth).not.toBe(initialWidth);
+ });
+ });
+
+ it('stops incrementing progress near 100% and does not exceed it', async () => {
+ render();
+ act(() => {
+ vi.advanceTimersByTime(20000);
+ vi.clearAllTimers();
+ });
+ const progressBar = screen.getByTestId('progressLoaderBar');
+
+ await waitFor(() => {
+ const progressWidth = parseFloat(progressBar.style.width);
+ expect(progressWidth).toBeGreaterThan(90);
+ expect(progressWidth).toBeLessThan(100);
+ });
+ });
+
+ it('should reset progress to 0 after reaching 100%', async () => {
+ render();
+ act(() => {
+ vi.advanceTimersByTime(100);
+ vi.clearAllTimers();
+ });
+
+ await waitFor(() => {
+ const { width } = screen.getByTestId('progressLoaderBar').style;
+ expect(width).toBe('0%');
+ });
+ });
+
+ it('cleans up the interval when the component is unmounted', () => {
+ const { unmount } = render();
+ vi.clearAllTimers();
+ unmount();
+
+ // After unmounting the component, advancing timers should not cause any errors or warnings
+ expect(() => {
+ vi.advanceTimersByTime(500);
+ }).not.toThrow();
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx
new file mode 100644
index 000000000000..91d874f5352d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/LabelsForm.component.tsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import { PlusCircle, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import * as ai from '@/types/cloud/project/ai';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { CONFIGURATION_CONFIG } from './configuration.constants';
+
+interface LabelsFormProps {
+ labelValue: ai.Label[];
+ onChange: (newLabels: ai.Label[]) => void;
+ disabled?: boolean;
+}
+
+const LabelsForm = React.forwardRef(
+ ({ labelValue, onChange, disabled }, ref) => {
+ const { t } = useTranslation('pci-ai-notebooks/components/configuration');
+ const labelSchema = z.object({
+ name: z
+ .string()
+ .min(1)
+ .max(15)
+ .refine(
+ (newKey) =>
+ !labelValue.some(
+ (existingLabel) =>
+ existingLabel.name.toLowerCase() === newKey.toLowerCase(),
+ ),
+ {
+ message: t('existingKeyError'),
+ },
+ ),
+ value: z
+ .string()
+ .min(1)
+ .max(15),
+ });
+
+ const form = useForm({
+ resolver: zodResolver(labelSchema),
+ });
+
+ const onSubmit: SubmitHandler = (data: ai.Label) => {
+ const newLabels = [...labelValue, data];
+ onChange(newLabels);
+ form.reset();
+ };
+
+ const removeLabel = (indexToRemove: number) => {
+ const newLabels = labelValue.filter(
+ (_, index) => index !== indexToRemove,
+ );
+ onChange(newLabels);
+ };
+
+ return (
+
+ );
+ },
+);
+
+LabelsForm.displayName = 'LabelsForm';
+
+export default LabelsForm;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx
new file mode 100644
index 000000000000..1448eec54239
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/SshKeyForm.component.tsx
@@ -0,0 +1,161 @@
+import React, { useState } from 'react';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import { PlusCircle, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Button } from '@/components/ui/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { CONFIGURATION_CONFIG } from './configuration.constants';
+import { OrderSshKey } from '@/types/orderFunnel';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import * as sshkey from '@/types/cloud/sshkey';
+
+interface SshKeyFormProps {
+ configuredSshKeys: sshkey.SshKey[];
+ sshKeyList: OrderSshKey[];
+ onChange: (newSshKeyList: OrderSshKey[]) => void;
+ disabled?: boolean;
+}
+
+const SshKeyForm = React.forwardRef(
+ ({ configuredSshKeys, sshKeyList, onChange, disabled }, ref) => {
+ const { t } = useTranslation('pci-ai-notebooks/components/configuration');
+ const [selectedSSH, setSelectedSSH] = useState();
+ const sshKeySchema = z.object({
+ name: z
+ .string()
+ .min(1)
+ .max(15)
+ .refine(
+ (newKeyName) =>
+ !sshKeyList.some(
+ (existingSSHKey) => existingSSHKey.name === newKeyName,
+ ),
+ {
+ message: t('duplicateKeyError'),
+ },
+ ),
+ sshKey: z.string().min(1),
+ });
+
+ const form = useForm({
+ resolver: zodResolver(sshKeySchema),
+ });
+
+ const onSubmit: SubmitHandler = (data: OrderSshKey) => {
+ const newSSHKeys = [...sshKeyList, data];
+ onChange(newSSHKeys);
+ form.reset();
+ };
+
+ const removeSSHKey = (indexToRemove: number) => {
+ const newSSHKeys = sshKeyList.filter(
+ (_, index) => index !== indexToRemove,
+ );
+ onChange(newSSHKeys);
+ };
+
+ return (
+
+ );
+ },
+);
+
+SshKeyForm.displayName = 'SshKeysForm';
+
+export default SshKeyForm;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts
new file mode 100644
index 000000000000..780110b7928c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/configuration/configuration.constants.ts
@@ -0,0 +1,4 @@
+export const CONFIGURATION_CONFIG = {
+ maxLabelNumber: 10,
+ maxSshKeyNumber: 10,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx
new file mode 100644
index 000000000000..f6582ea98bf0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/editor/EditorSelect.component.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { UserRound, UsersRound } from 'lucide-react';
+import * as ai from '@/types/cloud/project/ai';
+import { cn } from '@/lib/utils';
+import RadioTile from '@/components/radio-tile/RadioTile.component';
+
+interface EditorsSelectProps {
+ editors: ai.capabilities.notebook.Editor[];
+ value: string;
+ onChange: (newEditor: string) => void;
+ className?: string;
+}
+
+const EditorsSelect = React.forwardRef(
+ ({ editors, value, onChange, className }, ref) => {
+ return (
+
+ {editors.map((editor) => (
+
onChange(editor.id)}
+ value={editor.id}
+ checked={editor.id === value}
+ >
+
+
+
+ {editor.name}
+
+ {editor.id === 'jupyterlabcollaborative' ? (
+
+ ) : (
+
+ )}
+
+ {editor.logoUrl && (
+
+ )}
+
+
+
+ {editor.description}
+
+
+ ))}
+
+ );
+ },
+);
+EditorsSelect.displayName = 'EditorsSelect';
+export default EditorsSelect;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx
new file mode 100644
index 000000000000..a047b8bb597b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.component.tsx
@@ -0,0 +1,24 @@
+import { FieldError, FieldErrors } from 'react-hook-form';
+
+const ErrorList = ({ error }: { error: FieldErrors }) => {
+ const renderErrorMessages = (errors: FieldErrors): React.ReactNode[] => {
+ return Object.entries(errors).flatMap(([key, value]) => {
+ if (typeof value === 'object' && value !== null) {
+ if ('message' in value) {
+ // Direct error message
+ return {(value as FieldError).message};
+ }
+ // Nested errors or array errors
+ const nestedErrors = renderErrorMessages(value as FieldErrors);
+ return nestedErrors.length > 0 ? nestedErrors : [];
+ }
+ return []; // For TypeScript compliance, return empty array for non-error cases
+ });
+ };
+
+ return (
+ {renderErrorMessages(error)}
+ );
+};
+
+export default ErrorList;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx
new file mode 100644
index 000000000000..d60c332b43dc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/error-list/ErrorList.spec.tsx
@@ -0,0 +1,52 @@
+import { FieldErrors } from 'react-hook-form';
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import ErrorList from '@/components/order/error-list/ErrorList.component';
+
+describe('ErrorList component', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ const requiredMessage = 'This field is required';
+ const minLenghtdMessage = 'Minimum 5 digits for this field';
+ const nestedError = 'This is a nestedError';
+ const mockFieldErrors: FieldErrors = {
+ // Erreurs au niveau des champs
+ field1: {
+ type: 'required',
+ message: requiredMessage,
+ },
+ field2: {
+ type: 'minLength',
+ message: minLenghtdMessage,
+ },
+ };
+
+ it('should display Error', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText(requiredMessage)).toBeInTheDocument();
+ expect(screen.getByText(minLenghtdMessage)).toBeInTheDocument();
+ });
+ });
+
+ it('Empty Error List should not display Error', async () => {
+ const mockedEmptyFieldErrors: FieldErrors = {};
+ render();
+ await waitFor(() => {
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ });
+ });
+
+ it('Nested Error list should not display Error', async () => {
+ const mockedNestedFieldErrors: FieldErrors = {
+ field1: {
+ type: nestedError,
+ },
+ };
+ render();
+ await waitFor(() => {
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx
new file mode 100644
index 000000000000..09f93282fa32
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/flavor/FlavorSelect.component.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Cpu, Zap } from 'lucide-react';
+import Price from '@/components/price/Price.component';
+import * as ai from '@/types/cloud/project/ai';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { cn } from '@/lib/utils';
+import { bytesConverter } from '@/lib/bytesHelper';
+import { Flavor } from '@/types/orderFunnel';
+
+interface FlavorsSelectProps {
+ flavors: Flavor[];
+ value: string;
+ resourcesQuantity: number;
+ onChange: (newFlavor: string) => void;
+ className?: string;
+}
+
+const FlavorsSelect = React.forwardRef(
+ ({ flavors, value, resourcesQuantity, onChange, className }, ref) => {
+ const { t } = useTranslation('pci-ai-notebooks/components/flavor');
+
+ flavors.sort((a, b) => a.pricing[0].price - b.pricing[0].price);
+
+ const clickInput = (flavorName: string) => {
+ const inputElement = document.getElementById(
+ `flavor-${flavorName}`,
+ ) as HTMLInputElement | null;
+ if (inputElement) {
+ inputElement.click();
+ }
+ };
+ const handleKeyDown = (
+ e: React.KeyboardEvent,
+ flavorName: string,
+ ) => {
+ if (e.key === 'Enter') {
+ clickInput(flavorName);
+ }
+ };
+ return (
+
+ );
+ },
+);
+
+FlavorsSelect.displayName = 'FlavorsSelect';
+
+export default FlavorsSelect;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx
new file mode 100644
index 000000000000..17cd20ecbf57
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkSelect.component.tsx
@@ -0,0 +1,74 @@
+import React, { useState } from 'react';
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import * as ai from '@/types/cloud/project/ai';
+import FrameworkTile from './FrameworkTile.component';
+import { FrameworkWithVersion } from '@/types/orderFunnel';
+
+interface FrameworkSelectProps {
+ frameworks: ai.capabilities.notebook.Framework[];
+ value: FrameworkWithVersion;
+ onChange: (newFrameworkWithVersion: FrameworkWithVersion) => void;
+}
+const FrameworksSelect = React.forwardRef<
+ HTMLInputElement,
+ FrameworkSelectProps
+>(({ frameworks, value, onChange }, ref) => {
+ const [selectedFrameworkTypeIndex, setSelectedFrameworkTypeIndex] = useState(
+ 0,
+ );
+ const mappedFramework = frameworks.map((fmk) => ({
+ ...fmk,
+ type: fmk.docUrl ? 'classical' : 'quantique',
+ }));
+ const fmkTypes = [...new Set([...mappedFramework.map((mr) => mr.type)])];
+ return (
+ <>
+
+
setSelectedFrameworkTypeIndex(+v)}
+ >
+
+ {fmkTypes.map((fmkType, index) => (
+
+ {fmkType}
+
+ ))}
+
+
+ {mappedFramework
+ .filter((r) => r.type === fmkTypes[selectedFrameworkTypeIndex])
+ .map((fmk) => (
+ v === value.version)
+ : fmk.versions[0]
+ }
+ selected={fmk.id === value.framework}
+ onChange={(
+ newFramework: ai.capabilities.notebook.Framework,
+ newVersion: string,
+ ) => {
+ onChange({
+ framework: newFramework.id,
+ version: newVersion,
+ });
+ }}
+ />
+ ))}
+
+
+
+ >
+ );
+});
+
+FrameworksSelect.displayName = 'FrameworksSelect';
+export default FrameworksSelect;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx
new file mode 100644
index 000000000000..8025133c9d9d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTile.component.tsx
@@ -0,0 +1,67 @@
+import { useEffect, useState } from 'react';
+import RadioTile from '@/components/radio-tile/RadioTile.component';
+import * as ai from '@/types/cloud/project/ai';
+import VersionSelector from './FrameworkTileVersion.component';
+
+export const FrameworkTile = ({
+ framework,
+ version,
+ selected,
+ onChange,
+}: {
+ framework: ai.capabilities.notebook.Framework;
+ version: string;
+ selected: boolean;
+ onChange: (
+ framework: ai.capabilities.notebook.Framework,
+ version: string,
+ ) => void;
+}) => {
+ const [selectedVersion, setSelectedVersion] = useState(version);
+ const handleFrameworkClick = () => {
+ onChange(framework, selectedVersion);
+ };
+ useEffect(() => {
+ onChange(framework, selectedVersion);
+ }, [selectedVersion]);
+ return (
+
+
+
+
+ {framework.name}
+
+
+ {framework.logoUrl && (
+
+ )}
+
+
+ {
+ setSelectedVersion(framework.versions.find((v) => v === versionName));
+ }}
+ />
+
+
+ {framework.description}
+
+
+ );
+};
+
+export default FrameworkTile;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx
new file mode 100644
index 000000000000..28a2aca1fd30
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/framework/FrameworkTileVersion.component.tsx
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { Check, ChevronsUpDown } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+interface VersionSelectorProps {
+ versions: string[];
+ selectedVersion: string;
+ isFrameworkSelected: boolean;
+ onChange: (version: string) => void;
+}
+const VersionSelector = ({
+ versions,
+ selectedVersion,
+ isFrameworkSelected,
+ onChange,
+}: VersionSelectorProps) => {
+ const [open, setOpen] = useState(false);
+ const { t } = useTranslation('pci-ai-notebooks/components/framework');
+ return (
+
+
+ {versions.map((fmkVersion) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ {t('noVersionFound')}
+
+ {versions.map((fmkVersion) => (
+ {
+ onChange(value);
+ setOpen(false);
+ }}
+ >
+
+
+ {fmkVersion}
+
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default VersionSelector;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx
new file mode 100644
index 000000000000..c070788996e8
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/price/OrderPrice.component.tsx
@@ -0,0 +1,30 @@
+import { useTranslation } from 'react-i18next';
+import Price from '@/components/price/Price.component';
+import { order } from '@/types/catalog';
+
+interface OrderPriceProps {
+ minuteConverter: number;
+ price: order.publicOrder.Pricing;
+ quantity: number;
+}
+const OrderPrice = ({ minuteConverter, price, quantity }: OrderPriceProps) => {
+ const { t } = useTranslation('pricing');
+ return (
+
+
{t('pricingLabel')}
+
+
+ );
+};
+
+export default OrderPrice;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx
new file mode 100644
index 000000000000..442a211a8c1a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionSelect.component.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import * as ai from '@/types/cloud/project/ai';
+import { cn } from '@/lib/utils';
+import RegionTile from './RegionTile.component';
+
+interface RegionsSelectProps {
+ regions: ai.capabilities.Region[];
+ value: string;
+ onChange: (newRegion: string) => void;
+ className?: string;
+}
+
+const RegionsSelect = React.forwardRef(
+ ({ regions, value, onChange, className }, ref) => {
+ return (
+
+ {regions.map((region) => (
+ onChange(newValue)}
+ />
+ ))}
+
+ );
+ },
+);
+RegionsSelect.displayName = 'RegionsSelect';
+export default RegionsSelect;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx
new file mode 100644
index 000000000000..d1f96841a811
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/region/RegionTile.component.tsx
@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next';
+import RadioTile from '@/components/radio-tile/RadioTile.component';
+import * as ai from '@/types/cloud/project/ai';
+
+export const RegionTile = ({
+ region,
+ selected,
+ onChange,
+}: {
+ region: ai.capabilities.Region;
+ selected: boolean;
+ onChange: (newRegion: string) => void;
+}) => {
+ const { t: tRegions } = useTranslation('regions');
+ return (
+ onChange(region.id)}
+ value={region.id}
+ checked={selected}
+ >
+
+
+
+ {tRegions(`region_${region.id}`)}
+ {` (${region.id})`}
+
+
+
+
+ );
+};
+
+export default RegionTile;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx
new file mode 100644
index 000000000000..b1cdaa1162b2
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/VolumesForm.component.tsx
@@ -0,0 +1,345 @@
+import React, { useState } from 'react';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import {
+ GitBranchIcon,
+ HelpCircle,
+ Package,
+ PlusCircle,
+ Trash2,
+} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import * as ai from '@/types/cloud/project/ai';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+
+import { DataStoresWithContainers } from '@/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook';
+import { FormVolumes, OrderVolumes } from '@/types/orderFunnel';
+import { VOLUMES_CONFIG } from './volume.const';
+import { Switch } from '@/components/ui/switch';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+
+interface VolumesFormProps {
+ configuredVolumesList: DataStoresWithContainers[];
+ selectedVolumesList: OrderVolumes[];
+ onChange: (newVolumesList: OrderVolumes[]) => void;
+ disabled?: boolean;
+}
+
+const VolumeForm = React.forwardRef(
+ ({ configuredVolumesList, selectedVolumesList, onChange, disabled }, ref) => {
+ const { t } = useTranslation('pci-ai-notebooks/components/volumes');
+ const [selectedVolume, setSelectedVolume] = useState<
+ DataStoresWithContainers
+ >();
+ const volumesSchema = z.object({
+ gitBranch: z.string().optional(),
+ mountDirectory: z
+ .string()
+ .min(VOLUMES_CONFIG.mountDirectory.min)
+ .max(VOLUMES_CONFIG.mountDirectory.max)
+ .regex(VOLUMES_CONFIG.mountDirectory.pattern, {
+ message: t('mountPathErrorFormat'),
+ })
+ .refine(
+ (newMountDirectory) =>
+ !selectedVolumesList.some(
+ (vol) => vol.mountPath === newMountDirectory,
+ ),
+ {
+ message: t('duplicateMountPathError'),
+ },
+ )
+ .refine(
+ (data) => {
+ if (data === '/workspace') {
+ return false;
+ }
+ return true;
+ },
+ { message: t('mountPathError') },
+ ),
+ permission: z.nativeEnum(ai.VolumePermissionEnum),
+ cache: z.boolean(),
+ });
+
+ const form = useForm({
+ resolver: zodResolver(volumesSchema),
+ });
+
+ const onSubmit: SubmitHandler = (data: FormVolumes) => {
+ const newVolumes: OrderVolumes = {
+ dataStore: {
+ alias: selectedVolume.alias,
+ container:
+ selectedVolume.type === ai.DataStoreTypeEnum.git
+ ? data.gitBranch
+ : selectedVolume.container,
+ type: selectedVolume.type,
+ },
+ cache: data.cache,
+ mountPath: data.mountDirectory,
+ permission: data.permission,
+ };
+
+ const newVolumesList = [...selectedVolumesList, newVolumes];
+ form.reset();
+ onChange(newVolumesList);
+ };
+
+ const removeVolume = (indexToRemove: number) => {
+ const newVolumesList = selectedVolumesList.filter(
+ (_, index) => index !== indexToRemove,
+ );
+ onChange(newVolumesList);
+ };
+
+ return (
+
+ );
+ },
+);
+
+VolumeForm.displayName = 'VolumeForm';
+
+export default VolumeForm;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts
new file mode 100644
index 000000000000..c567d8b54228
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/order/volumes/volume.const.ts
@@ -0,0 +1,8 @@
+export const VOLUMES_CONFIG = {
+ mountDirectory: {
+ min: 1,
+ max: 50,
+ pattern: /^\/(\S)*$/,
+ },
+ maxVolumes: 10,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx
new file mode 100644
index 000000000000..df32d00961fc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.component.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+const PageLayout: React.FC = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default PageLayout;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx
new file mode 100644
index 000000000000..2f33ab59aeae
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/page-layout/PageLayout.spec.tsx
@@ -0,0 +1,12 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it } from 'vitest';
+import PageLayout from '@/components/page-layout/PageLayout.component';
+
+describe('PageLayout component', () => {
+ it('should display the page layout', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByTestId('pageLayout')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx
new file mode 100644
index 000000000000..cbba04e59424
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.component.tsx
@@ -0,0 +1,56 @@
+import { useTranslation } from 'react-i18next';
+import { useLocale } from '@/hooks/useLocale';
+import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook';
+
+interface PriceProps {
+ priceInUcents: number;
+ taxInUcents: number;
+ decimals: number;
+ displayInHour: boolean;
+}
+const Price = ({
+ priceInUcents,
+ taxInUcents,
+ decimals = 2,
+ displayInHour = true,
+}: PriceProps) => {
+ const { t } = useTranslation('pricing');
+ const catalog = useGetCatalog();
+ const locale = useLocale();
+
+ if (!catalog.isSuccess) {
+ return (
+
+ );
+ }
+
+ const ucentToEur = 100_000_000;
+ const price = priceInUcents / ucentToEur;
+ const priceWithTax = (priceInUcents + taxInUcents) / ucentToEur;
+ const formatPrice = (value: number) => {
+ const formatter = new Intl.NumberFormat(locale.replace('_', '-'), {
+ style: 'currency',
+ currency: catalog.data.locale.currencyCode,
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ });
+ return formatter.format(value);
+ };
+
+ return (
+ <>
+
+ {t('pricingHt', { price: formatPrice(price) })}
+ {' '}
+ {displayInHour && {t('pricingInHour')}}{' '}
+
+ ({t('pricingTtc', { price: formatPrice(priceWithTax) })})
+
+ >
+ );
+};
+
+export default Price;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx
new file mode 100644
index 000000000000..b9e64b3b074a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/price/Price.spec.tsx
@@ -0,0 +1,115 @@
+import { render, screen } from '@testing-library/react';
+import { vi } from 'vitest';
+import Price from '@/components/price/Price.component';
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string, options: Record): string => {
+ if (key === 'pricingInHour') return '';
+ return `${key} ${options.price}`;
+ },
+ }),
+ Trans: ({ children }: { children: React.ReactNode }) => children,
+}));
+vi.mock('@/hooks/api/catalog/useGetCatalog.hook', () => {
+ return {
+ useGetCatalog: vi.fn(() => ({
+ isSuccess: true,
+ data: {
+ locale: {
+ currencyCode: 'EUR',
+ },
+ },
+ })),
+ };
+});
+vi.mock('@/hooks/useLocale', () => {
+ return {
+ useLocale: vi.fn(() => 'fr_FR'),
+ };
+});
+
+describe('Price component renders', () => {
+ it('should display the 2 span', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('pricing-ht')).toBeInTheDocument();
+ expect(screen.getByTestId('pricing-ttc')).toBeInTheDocument();
+ });
+});
+
+describe('Price component value', () => {
+ it('should display price with tax', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('pricing-ht')).toHaveTextContent(
+ 'pricingHt 10,00 €',
+ );
+ expect(screen.getByTestId('pricing-ttc')).toHaveTextContent(
+ '(pricingTtc 12,00 €)',
+ );
+ });
+
+ it('should display price without tax', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('pricing-ht')).toHaveTextContent(
+ 'pricingHt 10,00 €',
+ );
+ expect(screen.getByTestId('pricing-ttc')).toHaveTextContent(
+ '(pricingTtc 10,00 €)',
+ );
+ });
+
+ it('should display price with 3 decimals', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('pricing-ht')).toHaveTextContent(
+ 'pricingHt 10,000 €',
+ );
+ expect(screen.getByTestId('pricing-ttc')).toHaveTextContent(
+ '(pricingTtc 12,000 €)',
+ );
+ });
+
+ it('should display 0,00 when given 0', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('pricing-ht')).toHaveTextContent(
+ 'pricingHt 0,00 €',
+ );
+ expect(screen.getByTestId('pricing-ttc')).toHaveTextContent(
+ '(pricingTtc 0,00 €)',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx
new file mode 100644
index 000000000000..73c47fa1516b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.component.tsx
@@ -0,0 +1,62 @@
+import { useId } from 'react';
+import { cn } from '@/lib/utils';
+
+interface RadioTileProps extends React.InputHTMLAttributes {
+ children: React.ReactNode | React.ReactNode[];
+ className?: string;
+}
+const RadioTile = ({ children, className, ...props }: RadioTileProps) => {
+ const id = useId();
+ const handleLabelKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ const inputElement = document.getElementById(
+ id,
+ ) as HTMLInputElement | null;
+ if (inputElement) {
+ inputElement.click();
+ }
+ }
+ };
+ return (
+
+ (props.onChange ? props.onChange(e) : null)}
+ className="hidden"
+ type="radio"
+ id={id}
+ checked={props.checked}
+ {...props}
+ />
+
+
+ );
+};
+
+RadioTile.Separator = function RadioTileSeparator() {
+ return (
+
+ );
+};
+
+export default RadioTile;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx
new file mode 100644
index 000000000000..8d0cbe0a0e5f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/radio-tile/RadioTile.spec.tsx
@@ -0,0 +1,88 @@
+import {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+import { describe, it, vi } from 'vitest';
+import RadioTile from '@/components/radio-tile/RadioTile.component';
+
+describe('RadioTile component', () => {
+ it('should display the radio tile', async () => {
+ render(
+
+ title
+
+ content
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByText('title')).toBeInTheDocument();
+ expect(screen.getByText('content')).toBeInTheDocument();
+ });
+ });
+
+ it('should trigger callback when selected', async () => {
+ const onChange = vi.fn();
+ render(
+
+ title
+
+ content
+ ,
+ );
+ act(() => {
+ fireEvent.click(screen.getByTestId('radio-tile-input'));
+ });
+ await waitFor(() => {
+ expect(onChange).toHaveBeenCalled();
+ });
+ });
+
+ it('should select on key down', async () => {
+ const onChange = vi.fn();
+ render(
+
+ title
+
+ content
+ ,
+ );
+ act(() => {
+ fireEvent.keyDown(screen.getByTestId('radio-tile-container'), {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ charCode: 13,
+ });
+ });
+ await waitFor(() => {
+ expect(onChange).toHaveBeenCalled();
+ });
+ });
+
+ it('should have a different style when selected', async () => {
+ const onChange = vi.fn();
+ const { rerender } = render(
+
+ foo
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByTestId('radio-tile-label')).not.toHaveClass(
+ 'selected',
+ );
+ });
+ act(() => {
+ rerender(
+
+ foo
+ ,
+ );
+ });
+ await waitFor(() => {
+ expect(screen.getByTestId('radio-tile-label')).toHaveClass('selected');
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx
new file mode 100644
index 000000000000..41277d3eb7ab
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.component.tsx
@@ -0,0 +1,85 @@
+import { useRef } from 'react';
+import { Skeleton } from '../ui/skeleton';
+import { ScrollArea, ScrollBar } from '../ui/scroll-area';
+import NavLink from '@/components/links/NavLink.component';
+import { Badge } from '../ui/badge';
+
+export interface Tab {
+ href: string;
+ label: string;
+ count?: number;
+ end?: boolean;
+ disabled?: boolean;
+}
+interface TabsMenuProps {
+ tabs: Tab[];
+}
+const TabsMenu = ({ tabs }: TabsMenuProps) => {
+ const s = useRef(null);
+ return (
+ {
+ s.current.children[1].scrollLeft += e.deltaY;
+ }}
+ >
+
+ {tabs.map((tab, index) => (
+
+ {({ isActive }) => (
+ {
+ if (node && isActive)
+ node?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ }}
+ >
+ {tab.label}
+ {tab.count > 0 && (
+
+ {tab.count}
+
+ )}
+
+ )}
+
+ ))}
+
+
+
+ );
+};
+
+TabsMenu.Skeleton = function TabsMenuSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TabsMenu;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx
new file mode 100644
index 000000000000..a2e043de8e1a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/tabs-menu/TabsMenu.spec.tsx
@@ -0,0 +1,92 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import TabsMenu, { Tab } from '@/components/tabs-menu/TabsMenu.component';
+import { RouterWithQueryClientWrapper } from '../../__tests__/helpers/wrappers/RouterWithQueryClientWrapper';
+
+function setup(tabs: Tab[] = []) {
+ return render(, {
+ wrapper: RouterWithQueryClientWrapper,
+ });
+}
+
+describe('TabsMenu component', () => {
+ beforeEach(() => {
+ const ResizeObserverMock = vi.fn(() => ({
+ observe: vi.fn(),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+ }));
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('renders skeletons correctly', async () => {
+ render();
+ expect(screen.getByTestId('skeleton-container')).toBeInTheDocument();
+ });
+
+ it('renders correctly with no tabs', () => {
+ setup();
+ const scrollArea = screen.getByTestId('tab-container');
+ expect(scrollArea).toBeEmptyDOMElement();
+ });
+
+ it('renders tabs with labels and optional count badges', () => {
+ const tabs = [
+ { href: '/tab1', label: 'Tab 1', count: 10 },
+ { href: '/tab2', label: 'Tab 2' },
+ { href: '/tab3', label: 'Tab 3', count: 5 },
+ ];
+ setup(tabs);
+
+ tabs.forEach((tab) => {
+ const link = screen.getByText(tab.label);
+ expect(link).toBeInTheDocument();
+ if (tab.count) {
+ const badge = screen.getAllByText(tab.count.toString())[0];
+ expect(badge).toBeInTheDocument();
+ }
+ });
+ });
+
+ it('handles scroll on wheel event', async () => {
+ const tabs = [
+ { href: '/tab1', label: 'Tab 1', count: 10 },
+ { href: '/tab2', label: 'Tab 2' },
+ { href: '/tab3', label: 'Tab 3', count: 5 },
+ { href: '/tab4', label: 'Tab 4', count: 20 },
+ ];
+ setup(tabs);
+ const scrollArea = screen.getByTestId('scrollbar');
+ const scrollLeftInitial = scrollArea.children[1].scrollLeft;
+
+ fireEvent.wheel(scrollArea, { deltaY: 50 });
+ await waitFor(() => {
+ expect(scrollArea.children[1].scrollLeft).not.toBe(scrollLeftInitial);
+ });
+ });
+
+ it('active tab gets scrolled into view on click', async () => {
+ const scrollMock = vi.fn();
+ window.HTMLElement.prototype.scrollIntoView = scrollMock;
+ const tabs = [
+ { href: '/', label: 'Tab 1', count: 10 },
+ { href: '/tab2', label: 'Tab 2' },
+ { href: '/tab3', label: 'Tab 3', count: 5 },
+ ];
+ setup(tabs);
+ const tab1 = screen.getByText('Tab 1').closest('span');
+
+ await userEvent.click(tab1);
+
+ await waitFor(() => {
+ expect(scrollMock).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx
new file mode 100644
index 000000000000..83d58e2e18ff
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.component.tsx
@@ -0,0 +1,162 @@
+import React, { useRef } from 'react';
+import { z } from 'zod';
+import { X, PlusCircle } from 'lucide-react';
+import { useForm, SubmitHandler } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Input } from '../ui/input';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '../ui/form';
+import { Button } from '../ui/button';
+
+interface TagsInputProps {
+ value: string[];
+ onChange: (newTags: string[]) => void;
+ min?: number;
+ max?: number;
+ pattern?: RegExp;
+ placeholder?: string;
+ disabled?: boolean;
+ schema?: z.ZodString;
+ children?: React.ReactNode;
+}
+
+const TagsInput = React.forwardRef(
+ (
+ {
+ value,
+ onChange,
+ min,
+ max,
+ pattern,
+ placeholder,
+ disabled = false,
+ schema: inputSchema,
+ children,
+ },
+ ref,
+ ) => {
+ const addTagBtnRef = useRef(null);
+ let inputRules = z.string();
+
+ if (inputSchema) {
+ inputRules = inputSchema;
+ } else {
+ if (typeof min === 'number') {
+ inputRules = inputRules.min(min);
+ }
+ if (typeof max === 'number') {
+ inputRules = inputRules.max(max);
+ }
+ if (pattern instanceof RegExp) {
+ inputRules = inputRules.regex(pattern, {
+ message: 'Invalid pattern',
+ });
+ }
+ }
+ const schema = z
+ .object({
+ tag: inputRules,
+ })
+ .refine((newTag) => !value.includes(newTag.tag), {
+ message: 'No duplicate value',
+ path: ['tag'],
+ });
+ type ValidationSchema = z.infer;
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ tag: '',
+ },
+ });
+
+ const handleAddTag: SubmitHandler = (formValues) => {
+ const newTags = [...value, formValues.tag];
+ onChange(newTags);
+ form.reset();
+ };
+
+ const handleRemoveTag = (index: number) => {
+ const updatedTags = [...value];
+ updatedTags.splice(index, 1);
+ onChange(updatedTags);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ addTagBtnRef.current?.click();
+ event.preventDefault();
+ }
+ };
+
+ return (
+
+ );
+ },
+);
+
+TagsInput.displayName = 'TagsInput';
+
+export default TagsInput;
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx
new file mode 100644
index 000000000000..9fa4482246da
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/tags-input/TagsInput.spec.tsx
@@ -0,0 +1,287 @@
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+ act,
+} from '@testing-library/react';
+import { vi } from 'vitest';
+import TagsInput from '@/components/tags-input/TagsInput.component';
+
+describe('TagsInput component', () => {
+ it('should render correctly with initial tags', () => {
+ const handleChange = vi.fn();
+ render(
+ ,
+ );
+
+ const inputElement = screen.getByPlaceholderText('Enter tag');
+ expect(inputElement).toBeInTheDocument();
+
+ const tag1Element = screen.getByText('tag1');
+ expect(tag1Element).toBeInTheDocument();
+
+ const tag2Element = screen.getByText('tag2');
+ expect(tag2Element).toBeInTheDocument();
+ });
+
+ it('should remove tag when remove button is clicked', () => {
+ const handleChange = vi.fn();
+ render(
+ ,
+ );
+ const removeButton = screen.getByTestId('remove_tag_button_0');
+ fireEvent.click(removeButton);
+ expect(handleChange).toHaveBeenCalledWith(['tag2']);
+ });
+
+ it('should add new tag when user presses button', async () => {
+ // initial state
+ let tags: string[] = ['tag1', 'tag2'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn((newTags: string[]) => {
+ tags = [...newTags];
+ });
+ // initialize renderer
+ const component = render(
+ dom((newTags) => {
+ mockOnChange(newTags);
+ component.rerender(dom(() => {}));
+ }),
+ );
+
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ const addTagButton = screen.getByTestId('add_tag_button');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tag3',
+ },
+ });
+ fireEvent.click(addTagButton);
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalledWith(['tag1', 'tag2', 'tag3']);
+ expect(screen.getByText('tag3')).toBeInTheDocument();
+ expect(inputElement).toHaveValue('');
+ });
+ });
+
+ it('should add new tag when user presses enter', async () => {
+ // initial state
+ let tags: string[] = ['tag1', 'tag2'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn((newTags: string[]) => {
+ tags = [...newTags];
+ });
+ // initialize renderer
+ const component = render(
+ dom((newTags) => {
+ mockOnChange(newTags);
+ component.rerender(dom(() => {}));
+ }),
+ );
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tag3',
+ },
+ });
+ fireEvent.keyDown(inputElement, { key: 'Enter', code: 13, charCode: 13 });
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalledWith(['tag1', 'tag2', 'tag3']);
+ expect(screen.getByText('tag3')).toBeInTheDocument();
+ expect(inputElement).toHaveValue('');
+ });
+ });
+
+ it('should display an error when tag is too short', async () => {
+ // initial state
+ const tags: string[] = ['tag1'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn();
+ // initialize renderer
+ render(dom(mockOnChange));
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ const addTagButton = screen.getByTestId('add_tag_button');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tag2',
+ },
+ });
+ fireEvent.click(addTagButton);
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).not.toHaveBeenCalled();
+ expect(
+ screen.getByText('String must contain at least 5 character(s)'),
+ ).toBeInTheDocument();
+ expect(inputElement).toHaveValue('tag2');
+ });
+ });
+
+ it('should display an error when tag is too long', async () => {
+ // initial state
+ const tags: string[] = ['tag1'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn();
+ // initialize renderer
+ render(dom(mockOnChange));
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ const addTagButton = screen.getByTestId('add_tag_button');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tagwithaverylongvaluethatexceedsmaximum',
+ },
+ });
+ fireEvent.click(addTagButton);
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).not.toHaveBeenCalled();
+ expect(
+ screen.getByText('String must contain at most 10 character(s)'),
+ ).toBeInTheDocument();
+ expect(inputElement).toHaveValue(
+ 'tagwithaverylongvaluethatexceedsmaximum',
+ );
+ });
+ });
+
+ it('should display an error when tag does not match pattern', async () => {
+ // initial state
+ const tags: string[] = ['tag1'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn();
+ // initialize renderer
+ render(dom(mockOnChange));
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ const addTagButton = screen.getByTestId('add_tag_button');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tag@forbidden',
+ },
+ });
+ fireEvent.click(addTagButton);
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).not.toHaveBeenCalled();
+ expect(screen.getByText('Invalid pattern')).toBeInTheDocument();
+ expect(inputElement).toHaveValue('tag@forbidden');
+ });
+ });
+
+ it('should display an error when adding a duplicate tag', async () => {
+ // initial state
+ const tags: string[] = ['tag1'];
+ // dom
+ const dom = (onChange: (newTags: string[]) => void) => (
+
+ );
+ // spy, rerender component on call
+ const mockOnChange = vi.fn();
+ // initialize renderer
+ render(dom(mockOnChange));
+ // act
+ const inputElement = screen.getByTestId('input_tag');
+ const addTagButton = screen.getByTestId('add_tag_button');
+ act(() => {
+ fireEvent.input(inputElement, {
+ target: {
+ value: 'tag1',
+ },
+ });
+ fireEvent.click(addTagButton);
+ });
+ // assert
+ await waitFor(() => {
+ expect(mockOnChange).not.toHaveBeenCalled();
+ expect(screen.getByText('No duplicate value')).toBeInTheDocument();
+ expect(inputElement).toHaveValue('tag1');
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx
new file mode 100644
index 000000000000..e6a723d06574
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx
new file mode 100644
index 000000000000..8722561cf6bd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx
new file mode 100644
index 000000000000..924b130c73e3
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/alert.tsx
@@ -0,0 +1,69 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ info:
+ "border-transparent bg-blue-100 text-blue-700 [&>svg]:text-blue-700",
+ success:
+ "border-transparent bg-green-100 text-green-700 [&>svg]:text-green-700",
+ warning:
+ "border-transparent bg-yellow-200 text-yellow-800 [&>svg]:text-green-800",
+ error:
+ "border-transparent bg-red-100 text-red-700 [&>svg]:text-red-700",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx
new file mode 100644
index 000000000000..c2ed15071965
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/badge.tsx
@@ -0,0 +1,44 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ success:
+ "border-transparent bg-green-100 text-green-700 hover:bg-green-100/80",
+ info:
+ "border-transparent bg-blue-100 text-blue-700 hover:bg-blue-100/80",
+ warning:
+ "border-transparent bg-yellow-300 text-yellow-800 hover:bg-yellow-100/80",
+ error:
+ "border-transparent bg-red-100 text-red-700 hover:bg-red-100/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx
new file mode 100644
index 000000000000..57f4a373a512
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/button.tsx
@@ -0,0 +1,64 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary-600",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border text-primary border-primary border-2 bg-background font-semibold hover:bg-primary-100",
+ menu:
+ "border text-primary border-primary border-2 bg-background font-semibold hover:bg-primary-100 rounded-full",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ table: "hover:bg-primary-100 hover:text-primary-700 hover:font-semibold",
+ link: "text-primary underline-offset-4 hover:underline",
+ input: "border border-input bg-background",
+ },
+ size: {
+ default: "h-10 px-4 py-2 text-base",
+ sm: "h-9 rounded-md px-3 text-sm",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ link: "text-base",
+ table: "h-4 w-4 my-auto",
+ menu: 'size-8 p-0',
+ input: "h-10 w-full rounded-md px-3 py-2 text-sm"
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx
new file mode 100644
index 000000000000..b065f8e0cd2b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/calendar.tsx
@@ -0,0 +1,64 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx
new file mode 100644
index 000000000000..60b8cbe52fa1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => (
+ {children}
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx
new file mode 100644
index 000000000000..a23e7a281287
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/collapsible.tsx
@@ -0,0 +1,9 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx
new file mode 100644
index 000000000000..d4a5338a3d59
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/command.tsx
@@ -0,0 +1,154 @@
+/* eslint-disable react/no-unknown-property */
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+ )
+}
+
+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/configuration/polling.constants.ts b/packages/manager/apps/pci-ai-notebooks/src/configuration/polling.constants.ts
new file mode 100644
index 000000000000..17067264d00d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/polling.constants.ts
@@ -0,0 +1,8 @@
+export const POLLING = {
+ NOTEBOOKS: 30_000,
+ NOTEBOOK: 30_000,
+};
+
+export const USER_INACTIVITY_TIMEOUT = 5 * 60_000; // inactivity after 5 minutes
+
+export const TERMINATE_CONFIRMATION = 'TERMINATE';
diff --git a/packages/manager/apps/pci-ai-notebooks/src/configuration/project.ts b/packages/manager/apps/pci-ai-notebooks/src/configuration/project.ts
new file mode 100644
index 000000000000..947457b9a819
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/project.ts
@@ -0,0 +1,4 @@
+export enum PlanCode {
+ DISCOVERY = 'project.discovery',
+ STANDARD = 'project.2018',
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/configuration/toast.constants.ts b/packages/manager/apps/pci-ai-notebooks/src/configuration/toast.constants.ts
new file mode 100644
index 000000000000..13dec949ca89
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/toast.constants.ts
@@ -0,0 +1,3 @@
+export const TOAST = {
+ ERROR_DURATION: 15000,
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/configuration/tracking.constants.ts b/packages/manager/apps/pci-ai-notebooks/src/configuration/tracking.constants.ts
new file mode 100644
index 000000000000..e422f6a30b60
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/tracking.constants.ts
@@ -0,0 +1,11 @@
+const APP_TRACKING_PREFIX = 'PublicCloud::databases_analytics::databases';
+export const PCI_LEVEL2 = '86';
+export const TRACKING = {
+ onboarding: {
+ page: () => `${APP_TRACKING_PREFIX}::databases::onboarding`,
+ createDatabaseClick: () =>
+ `${APP_TRACKING_PREFIX}::page::button::create_databases`,
+ guideClick: (guideName: string) =>
+ `${APP_TRACKING_PREFIX}::page::tile-tutorial::go-to-${guideName}`,
+ },
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/contexts/LoadingIndicator.context.tsx b/packages/manager/apps/pci-ai-notebooks/src/contexts/LoadingIndicator.context.tsx
new file mode 100644
index 000000000000..baec62764931
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/contexts/LoadingIndicator.context.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/contexts/UserActivityContext.tsx b/packages/manager/apps/pci-ai-notebooks/src/contexts/UserActivityContext.tsx
new file mode 100644
index 000000000000..28f197e10af1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/contexts/UserActivityContext.tsx
@@ -0,0 +1,45 @@
+import { createContext, useContext, ReactNode } from 'react';
+import useUserActivity from '@/hooks/useUserActivity';
+
+interface UserActivityContextProps {
+ isUserActive: boolean;
+}
+
+const UserActivityContext = createContext(
+ undefined,
+);
+
+interface UserActivityProviderProps {
+ timeout: number;
+ onInactive?: () => void;
+ onActive?: () => void;
+ children: ReactNode;
+}
+export const UserActivityProvider = ({
+ timeout,
+ children,
+ onActive = () => {},
+ onInactive = () => {},
+}: UserActivityProviderProps) => {
+ const isUserActive = useUserActivity({
+ timeout,
+ onInactive,
+ onActive,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useUserActivityContext = () => {
+ const context = useContext(UserActivityContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useUserActivityContext must be used within a UserActivityProvider',
+ );
+ }
+ return context;
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.api.ts
new file mode 100644
index 000000000000..5f2fea254262
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.api.ts
@@ -0,0 +1,13 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+
+export const getAuthorization = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/authorization`)
+ .then((res) => res.data as ai.AuthorizationStatus);
+
+export const postAuthorization = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/authorization`)
+ .then((res) => res.data as ai.AuthorizationStatus);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.spec.tsx
new file mode 100644
index 000000000000..ef9ab21c6eb6
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/authorization.spec.tsx
@@ -0,0 +1,46 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getAuthorization, postAuthorization } from './authorization.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ },
+ },
+ };
+});
+
+describe('Authorization functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getAuthorization', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getAuthorization({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/authorization',
+ );
+ });
+
+ it('should call postAuthorization', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await postAuthorization({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/authorization',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.api.ts
new file mode 100644
index 000000000000..20c8052602e6
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.api.ts
@@ -0,0 +1,22 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+
+export const getRegions = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/capabilities/region`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ })
+ .then((res) => res.data as ai.capabilities.Region[]);
+
+export interface GetFlavor extends PCIAi {
+ region: string;
+}
+
+export const getFlavor = async ({ projectId, region }: GetFlavor) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/capabilities/region/${region}/flavor`)
+ .then((res) => res.data as ai.capabilities.Flavor[]);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.spec.tsx
new file mode 100644
index 000000000000..ec023ff5740e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/capabilities.spec.tsx
@@ -0,0 +1,55 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getRegions, getFlavor } from './capabilities.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('Capabilities functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getRegions', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getRegions({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/capabilities/region',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+});
+
+describe('Capabilities Flavor functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getFlavor', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getFlavor({
+ projectId: 'projectId',
+ region: 'regionId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/capabilities/region/regionId/flavor',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.api.ts
new file mode 100644
index 000000000000..f2b383f77d64
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.api.ts
@@ -0,0 +1,95 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+import { Containers } from '@/types/orderFunnel';
+
+interface DatastoresProps extends PCIAi {
+ region: string;
+}
+
+export const getDatastores = async ({ projectId, region }: DatastoresProps) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/data/region/${region}/alias`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ })
+ .then((res) => res.data as ai.DataStore[]);
+
+export interface DatastoreProps extends DatastoresProps {
+ alias: string;
+}
+
+export const getDatastore = async ({
+ projectId,
+ region,
+ alias,
+}: DatastoreProps) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/data/region/${region}/alias/${alias}`)
+ .then((res) => res.data as ai.DataStore);
+
+export interface AddDatastoreProps extends PCIAi {
+ region: string;
+ datastore: ai.DataStoreInput;
+}
+export const addDatastore = async ({
+ projectId,
+ region,
+ datastore,
+}: AddDatastoreProps) =>
+ apiClient.v6
+ .post(
+ `/cloud/project/${projectId}/ai/data/region/${region}/alias`,
+ datastore,
+ )
+ .then((res) => res.data as ai.DataStore);
+
+export interface EditDatastoreProps extends AddDatastoreProps {
+ alias: string;
+}
+export const editDatastore = async ({
+ projectId,
+ region,
+ alias,
+ datastore,
+}: EditDatastoreProps) =>
+ apiClient.v6
+ .put(
+ `/cloud/project/${projectId}/ai/data/region/${region}/alias/${alias}`,
+ datastore,
+ )
+ .then((res) => res.data as ai.DataStore);
+
+export const deleteDatastore = async ({
+ projectId,
+ region,
+ alias,
+}: DatastoreProps) =>
+ apiClient.v6.delete(
+ `/cloud/project/${projectId}/ai/data/region/${region}/alias/${alias}`,
+ );
+
+export const getDatastoreAuth = async ({
+ projectId,
+ region,
+ alias,
+}: DatastoreProps) =>
+ apiClient.v6
+ .get(
+ `/cloud/project/${projectId}/ai/data/region/${region}/alias/${alias}/auth`,
+ )
+ .then((res) => res.data as ai.DataStoreAuth);
+
+export const getDatastoreContainer = async ({
+ projectId,
+ region,
+ alias,
+}: DatastoreProps) =>
+ apiClient.v6
+ .get(
+ `/cloud/project/${projectId}/ai/data/region/${region}/alias/${alias}/containers`,
+ )
+ .then((res) => res.data as Containers);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.spec.tsx
new file mode 100644
index 000000000000..9952013c3e78
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/datastore.spec.tsx
@@ -0,0 +1,123 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import {
+ addDatastore,
+ deleteDatastore,
+ editDatastore,
+ getDatastore,
+ getDatastoreAuth,
+ getDatastores,
+} from '@/data/api/ai/datastore.api';
+import { mockedDatastoreInput } from '@/__tests__/helpers/mocks/datastore';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const put = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const del = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ put,
+ delete: del,
+ },
+ },
+ };
+});
+
+describe('Datastore functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getDatastores', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getDatastores({
+ projectId: 'projectId',
+ region: 'region',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ },
+ );
+ });
+
+ it('should call getDatastore', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getDatastore({
+ projectId: 'projectId',
+ region: 'region',
+ alias: 'monAlias',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias/monAlias',
+ );
+ });
+
+ it('should call addDatasore with mockedDatastoreInput', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await addDatastore({
+ projectId: 'projectId',
+ region: 'region',
+ datastore: mockedDatastoreInput,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias',
+ mockedDatastoreInput,
+ );
+ });
+
+ it('should call editDatasore with mockedDatastoreInput', async () => {
+ expect(apiClient.v6.put).not.toHaveBeenCalled();
+ await editDatastore({
+ projectId: 'projectId',
+ region: 'region',
+ datastore: mockedDatastoreInput,
+ alias: 'monAlias',
+ });
+ expect(apiClient.v6.put).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias/monAlias',
+ mockedDatastoreInput,
+ );
+ });
+
+ it('should call deleteDatasore with aliasID', async () => {
+ expect(apiClient.v6.delete).not.toHaveBeenCalled();
+ await deleteDatastore({
+ projectId: 'projectId',
+ region: 'region',
+ alias: 'aliasID',
+ });
+ expect(apiClient.v6.delete).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias/aliasID',
+ );
+ });
+
+ it('should call getDatastoreAuth', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getDatastoreAuth({
+ projectId: 'projectId',
+ region: 'region',
+ alias: 'aliasID',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/data/region/region/alias/aliasID/auth',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.api.ts
new file mode 100644
index 000000000000..a9908b29d208
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.api.ts
@@ -0,0 +1,33 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+
+export interface GetGuidesProps extends PCIAi {
+ section?: string;
+ lang?: string;
+}
+export const getGuides = async ({
+ projectId,
+ section,
+ lang,
+}: GetGuidesProps) => {
+ const headers: Record = {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ };
+ const filters = [];
+ if (section) {
+ filters.push(`section:eq=${section}`);
+ }
+ if (lang) {
+ filters.push(`lang:eq=${lang}`);
+ }
+ if (filters.length > 0) {
+ headers['X-Pagination-Filter'] = filters.join('&');
+ }
+ return apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/guides`, {
+ headers,
+ })
+ .then((res) => res.data as ai.Guide[]);
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.spec.tsx
new file mode 100644
index 000000000000..dbed300e4884
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/guide.spec.tsx
@@ -0,0 +1,57 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getGuides } from '@/data/api/ai/guide.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('Guides functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getGuide with no filter', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getGuides({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/guides',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+
+ it('should call getGuide with filter', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getGuides({
+ projectId: 'projectId',
+ section: 'cli',
+ lang: 'fr-FR',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/guides',
+ {
+ headers: {
+ 'X-Pagination-Filter': 'section:eq=cli&lang:eq=fr-FR',
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.api.ts
new file mode 100644
index 000000000000..eaf67bb9f26b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.api.ts
@@ -0,0 +1,13 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '@/data/api';
+
+export const getEditor = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/notebook/capabilities/editor`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ })
+ .then((res) => res.data as ai.notebook.Editor[]);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.spec.tsx
new file mode 100644
index 000000000000..c22614634bab
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/editor.spec.tsx
@@ -0,0 +1,38 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getEditor } from './editor.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('notebook editor functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getEditor', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getEditor({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/capabilities/editor',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.api.ts
new file mode 100644
index 000000000000..618ee7513919
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.api.ts
@@ -0,0 +1,13 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '@/data/api';
+
+export const getFramework = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/notebook/capabilities/framework`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ })
+ .then((res) => res.data as ai.notebook.Framework[]);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.spec.tsx
new file mode 100644
index 000000000000..09ac5d709c6e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/capabilities/framework.spec.tsx
@@ -0,0 +1,38 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getFramework } from './framework.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('notebook framework functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getFramework', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getFramework({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/capabilities/framework',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.api.ts
new file mode 100644
index 000000000000..937faaa55121
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.api.ts
@@ -0,0 +1,51 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+
+import { NotebookData, PCIAi } from '../..';
+
+export const getNotebooks = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/notebook`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ })
+ .then((res) => res.data as ai.notebook.Notebook[]);
+
+export const getNotebook = async ({ projectId, notebookId }: NotebookData) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/notebook/${notebookId}`)
+ .then((res) => res.data as ai.notebook.Notebook);
+
+export interface AddNotebook extends PCIAi {
+ notebookInfo: ai.notebook.NotebookSpec;
+}
+export const addNotebook = async ({ projectId, notebookInfo }: AddNotebook) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/notebook`, notebookInfo)
+ .then((res) => res.data as ai.notebook.Notebook);
+
+export const startNotebook = async ({
+ projectId,
+ notebookId,
+}: NotebookData) => {
+ return apiClient.v6
+ .put(`/cloud/project/${projectId}/ai/notebook/${notebookId}/start`)
+ .then((res) => res.data as ai.notebook.Notebook);
+};
+
+export const stopNotebook = async ({ projectId, notebookId }: NotebookData) => {
+ return apiClient.v6
+ .put(`/cloud/project/${projectId}/ai/notebook/${notebookId}/stop`)
+ .then((res) => res.data as ai.notebook.Notebook);
+};
+
+export const deleteNotebook = async ({ projectId, notebookId }: NotebookData) =>
+ apiClient.v6.delete(`/cloud/project/${projectId}/ai/notebook/${notebookId}`);
+
+export const getCommand = async ({ projectId, notebookInfo }: AddNotebook) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/notebook/command`, notebookInfo)
+ .then((res) => res.data as ai.Command);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.spec.tsx
new file mode 100644
index 000000000000..b987f6a4b44d
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/notebook.spec.tsx
@@ -0,0 +1,174 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import {
+ addNotebook,
+ deleteNotebook,
+ getCommand,
+ getNotebook,
+ getNotebooks,
+ startNotebook,
+ stopNotebook,
+} from '@/data/api/ai/notebook/notebook.api';
+import { mockedNotebookSpec } from '@/__tests__/helpers/mocks/notebook';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const put = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const del = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ put,
+ delete: del,
+ },
+ },
+ };
+});
+
+describe('notebook functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getNotebooks', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getNotebooks({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ },
+ );
+ });
+
+ it('should call getNotebook', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getNotebook({
+ projectId: 'projectId',
+ notebookId: 'notebookId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/notebookId',
+ );
+ });
+
+ it('should call addNotebook', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await addNotebook({
+ projectId: 'projectId',
+ notebookInfo: mockedNotebookSpec,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook',
+ {
+ env: {
+ editorId: 'editor',
+ frameworkId: 'frameworkId',
+ frameworkVersion: 'frameworkVersion',
+ },
+ envVars: [
+ {
+ name: 'envVarsName',
+ value: 'envVarsValue',
+ },
+ ],
+ name: 'name',
+ region: 'region',
+ resources: {
+ cpu: 1,
+ ephemeralStorage: 1,
+ flavor: 'flavor',
+ gpu: 1,
+ memory: 1,
+ privateNetwork: 1,
+ publicNetwork: 1,
+ },
+ },
+ );
+ });
+
+ it('should call startNotebook', async () => {
+ expect(apiClient.v6.put).not.toHaveBeenCalled();
+ await startNotebook({
+ projectId: 'projectId',
+ notebookId: 'notebookId',
+ });
+ expect(apiClient.v6.put).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/notebookId/start',
+ );
+ });
+
+ it('should call stopNotebook', async () => {
+ expect(apiClient.v6.put).not.toHaveBeenCalled();
+ await stopNotebook({
+ projectId: 'projectId',
+ notebookId: 'notebookId',
+ });
+ expect(apiClient.v6.put).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/notebookId/stop',
+ );
+ });
+
+ it('should call deleteNotebook', async () => {
+ expect(apiClient.v6.put).not.toHaveBeenCalled();
+ await deleteNotebook({
+ projectId: 'projectId',
+ notebookId: 'notebookId',
+ });
+ expect(apiClient.v6.delete).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/notebookId',
+ );
+ });
+
+ it('should call getCommand', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await getCommand({
+ projectId: 'projectId',
+ notebookInfo: mockedNotebookSpec,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/notebook/command',
+ {
+ env: {
+ editorId: 'editor',
+ frameworkId: 'frameworkId',
+ frameworkVersion: 'frameworkVersion',
+ },
+ envVars: [
+ {
+ name: 'envVarsName',
+ value: 'envVarsValue',
+ },
+ ],
+ name: 'name',
+ region: 'region',
+ resources: {
+ cpu: 1,
+ ephemeralStorage: 1,
+ flavor: 'flavor',
+ gpu: 1,
+ memory: 1,
+ privateNetwork: 1,
+ publicNetwork: 1,
+ },
+ },
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/suggestions.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/suggestions.api.ts
new file mode 100644
index 000000000000..949b3c01d64b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/suggestions.api.ts
@@ -0,0 +1,14 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+
+import { PCIAi } from '../..';
+import { Suggestions } from '@/types/orderFunnel';
+import { mockedSuggestion } from '@/__tests__/helpers/mocks/suggestion';
+
+export const getSuggestions = async ({ projectId }: PCIAi) => {
+ /*
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/notebook/suggestions`)
+ .then((res) => res.data as Suggestions[]);
+ */
+ return mockedSuggestion;
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.api.ts
new file mode 100644
index 000000000000..cad41ece7687
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.api.ts
@@ -0,0 +1,50 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+
+export const getRegistries = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/registry`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ })
+ .then((res) => res.data as ai.registry.Registry[]);
+
+export interface AddRegistryProps extends PCIAi {
+ registry: {
+ password: string;
+ region: string;
+ url: string;
+ username: string;
+ };
+}
+export const addRegistry = async ({ projectId, registry }: AddRegistryProps) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/registry`, registry)
+ .then((res) => res.data as ai.registry.Registry);
+
+export interface EditRegistryProps extends PCIAi {
+ registryId: string;
+ registry: ai.registry.RegistryUpdateInput;
+}
+export const editRegistry = async ({
+ projectId,
+ registryId,
+ registry,
+}: EditRegistryProps) =>
+ apiClient.v6
+ .put(`/cloud/project/${projectId}/ai/registry/${registryId}`, registry)
+ .then((res) => res.data as ai.registry.Registry);
+
+export interface DeleteRegistryProps extends PCIAi {
+ registryId: string;
+}
+export const deleteRegistry = async ({
+ projectId,
+ registryId,
+}: DeleteRegistryProps) =>
+ apiClient.v6.delete(`/cloud/project/${projectId}/ai/registry/${registryId}`);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.spec.tsx
new file mode 100644
index 000000000000..b08303356d53
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/registry.spec.tsx
@@ -0,0 +1,96 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import {
+ addRegistry,
+ deleteRegistry,
+ editRegistry,
+ getRegistries,
+} from '@/data/api/ai/registry.api';
+import {
+ mockedRegistryEdit,
+ mockedRegistryInput,
+} from '@/__tests__/helpers/mocks/registry';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const put = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const del = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ put,
+ delete: del,
+ },
+ },
+ };
+});
+
+describe('Registry functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getRegistries', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getRegistries({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/registry',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ },
+ );
+ });
+
+ it('should call addRegistry with mockedRegistryInput', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await addRegistry({
+ projectId: 'projectId',
+ registry: mockedRegistryInput,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/registry',
+ mockedRegistryInput,
+ );
+ });
+
+ it('should call editRegistry with mockedRegistryEdit', async () => {
+ expect(apiClient.v6.put).not.toHaveBeenCalled();
+ await editRegistry({
+ projectId: 'projectId',
+ registry: mockedRegistryEdit,
+ registryId: 'registryId',
+ });
+ expect(apiClient.v6.put).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/registry/registryId',
+ mockedRegistryEdit,
+ );
+ });
+
+ it('should call deleteDatasore with registryID', async () => {
+ expect(apiClient.v6.delete).not.toHaveBeenCalled();
+ await deleteRegistry({
+ projectId: 'projectId',
+ registryId: 'registryId',
+ });
+ expect(apiClient.v6.delete).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/registry/registryId',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.api.ts
new file mode 100644
index 000000000000..62a17ca9b1af
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.api.ts
@@ -0,0 +1,33 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as ai from '@/types/cloud/project/ai';
+import { PCIAi } from '..';
+
+export const getTokens = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/ai/token`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ })
+ .then((res) => res.data as ai.token.Token[]);
+
+export interface AddTokenProps extends PCIAi {
+ token: ai.token.TokenSpec;
+}
+export const addToken = async ({ projectId, token }: AddTokenProps) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/token`, token)
+ .then((res) => res.data as ai.token.Token);
+
+export interface TokenProps extends PCIAi {
+ tokenId: string;
+}
+export const deleteToken = async ({ projectId, tokenId }: TokenProps) =>
+ apiClient.v6.delete(`/cloud/project/${projectId}/ai/token/${tokenId}`);
+
+export const renewToken = async ({ projectId, tokenId }: TokenProps) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/ai/token/${tokenId}/renew`)
+ .then((res) => res.data as ai.token.Token);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.spec.tsx
new file mode 100644
index 000000000000..e5abff299117
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/token.spec.tsx
@@ -0,0 +1,87 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import {
+ addToken,
+ deleteToken,
+ getTokens,
+ renewToken,
+} from '@/data/api/ai/token.api';
+import { mockedTokenCreation } from '@/__tests__/helpers/mocks/token';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const del = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ delete: del,
+ },
+ },
+ };
+});
+
+describe('Token functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getTokens', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getTokens({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/token',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ Pragma: 'no-cache',
+ },
+ },
+ );
+ });
+
+ it('should call addToken with mockedTockenCreation', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await addToken({
+ projectId: 'projectId',
+ token: mockedTokenCreation,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/token',
+ mockedTokenCreation,
+ );
+ });
+
+ it('should call deleteToken with tokenID', async () => {
+ expect(apiClient.v6.delete).not.toHaveBeenCalled();
+ await deleteToken({
+ projectId: 'projectId',
+ tokenId: 'tokenId',
+ });
+ expect(apiClient.v6.delete).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/token/tokenId',
+ );
+ });
+
+ it('should call renewToken with tokenId', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await renewToken({
+ projectId: 'projectId',
+ tokenId: 'tokenId',
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/ai/token/tokenId/renew',
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.api.ts
new file mode 100644
index 000000000000..a8d137a62c65
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.api.ts
@@ -0,0 +1,11 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { order } from '@/types/catalog';
+
+export const catalogApi = {
+ getCatalog: async (subsidiary = 'FR', productName = 'cloud') =>
+ apiClient.v6
+ .get(
+ `/order/catalog/public/cloud?ovhSubsidiary=${subsidiary}&productName=${productName}`,
+ )
+ .then((res) => res.data as order.publicOrder.Catalog[]),
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.spec.tsx
new file mode 100644
index 000000000000..e193a5214f30
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/catalog/catalog.spec.tsx
@@ -0,0 +1,29 @@
+import { describe, expect, vi } from 'vitest';
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { catalogApi } from '@/data/api/catalog/catalog.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('catalog api', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should call catalog apiv6 with subsidiary and productName', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ catalogApi.getCatalog('FR', 'cloud');
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/order/catalog/public/cloud?ovhSubsidiary=FR&productName=cloud',
+ );
+ });
+});
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..350e366a35b0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/index.tsx
@@ -0,0 +1,44 @@
+export interface PCIAi {
+ projectId: string;
+}
+
+export interface NotebookData extends PCIAi {
+ notebookId: 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/project/project.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/project/project.api.ts
new file mode 100644
index 000000000000..89cc6fb57cf3
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/project/project.api.ts
@@ -0,0 +1,7 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { Project } from '@/types/cloud/Project';
+
+export const getProject = async (projectId: string): Promise => {
+ const response = await apiClient.v6.get(`/cloud/project/${projectId}`);
+ return response.data as Project;
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/project/project.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/project/project.spec.tsx
new file mode 100644
index 000000000000..c8f892373463
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/project/project.spec.tsx
@@ -0,0 +1,27 @@
+import { describe, expect, vi } from 'vitest';
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { getProject } from '@/data/api/project/project.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('project api', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+ it('should call cloud project apiv6 with project id', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ getProject('foo');
+ expect(apiClient.v6.get).toHaveBeenCalledWith('/cloud/project/foo');
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.api.ts
new file mode 100644
index 000000000000..f0e7303d0923
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.api.ts
@@ -0,0 +1,22 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import * as sshkey from '@/types/cloud/sshkey';
+import { PCIAi } from '..';
+
+export const getSshkey = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/sshkey`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ })
+ .then((res) => res.data as sshkey.SshKey[]);
+
+export interface AddSSHKey extends PCIAi {
+ sshKey: sshkey.SshKey;
+}
+
+export const addSSHKey = async ({ projectId, sshKey }: AddSSHKey) =>
+ apiClient.v6
+ .post(`/cloud/project/${projectId}/sshkey`, sshKey)
+ .then((res) => res.data as sshkey.SshKeyDetail);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.spec.tsx
new file mode 100644
index 000000000000..2a31e3a2b360
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/sshkey/sshkey.spec.tsx
@@ -0,0 +1,60 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { addSSHKey, getSshkey } from '@/data/api/sshkey/sshkey.api';
+import { mockedSshKey } from '@/__tests__/helpers/mocks/sshkey';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ const post = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ post,
+ },
+ },
+ };
+});
+
+describe('sshkey functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getSshkey', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getSshkey({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/sshkey',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+
+ it('should call addSshKey', async () => {
+ expect(apiClient.v6.post).not.toHaveBeenCalled();
+ await addSSHKey({
+ projectId: 'projectId',
+ sshKey: mockedSshKey,
+ });
+ expect(apiClient.v6.post).toHaveBeenCalledWith(
+ '/cloud/project/projectId/sshkey',
+ {
+ id: 'idSSHKEY',
+ name: 'nameSSHKEY',
+ publicKey: 'publicKey',
+ regions: ['GRA'],
+ },
+ );
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.api.ts
new file mode 100644
index 000000000000..086988c037e1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.api.ts
@@ -0,0 +1,13 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { PCIAi } from '..';
+import * as storage from '@/types/cloud/storage';
+
+export const getStorage = async ({ projectId }: PCIAi) =>
+ apiClient.v6
+ .get(`/cloud/project/${projectId}/storage`, {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ })
+ .then((res) => res.data as storage.Container[]);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.spec.tsx
new file mode 100644
index 000000000000..c618a0eb8c3c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/storage/storage.spec.tsx
@@ -0,0 +1,38 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { getStorage } from './storage.api';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+ const get = vi.fn(() => {
+ return Promise.resolve({ data: null });
+ });
+ return {
+ apiClient: {
+ v6: {
+ get,
+ },
+ },
+ };
+});
+
+describe('storage functions', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should call getStorage', async () => {
+ expect(apiClient.v6.get).not.toHaveBeenCalled();
+ await getStorage({
+ projectId: 'projectId',
+ });
+ expect(apiClient.v6.get).toHaveBeenCalledWith(
+ '/cloud/project/projectId/storage',
+ {
+ headers: {
+ 'X-Pagination-Mode': 'CachedObjectList-Pages',
+ 'X-Pagination-Size': '50000',
+ },
+ },
+ );
+ });
+});
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/api/ai/authorization/useGetAuthorization.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/useGetAuthorization.hook.tsx
new file mode 100644
index 000000000000..8c8e7b9e0367
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/useGetAuthorization.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+import * as ai from '@/types/cloud/project/ai';
+
+import { getAuthorization } from '@/data/api/ai/authorization.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetAuthorization(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'authorization'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getAuthorization({ projectId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/useGetAuthorization.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/useGetAuthorization.spec.tsx
new file mode 100644
index 000000000000..7f0277c345b6
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/useGetAuthorization.spec.tsx
@@ -0,0 +1,30 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as authApi from '@/data/api/ai/authorization.api';
+import { useGetAuthorization } from './useGetAuthorization.hook';
+import { mockedAuthorization } from '@/__tests__/helpers/mocks/authorization';
+
+vi.mock('@/data/api/ai/authorization.api', () => ({
+ getAuthorization: vi.fn(),
+}));
+
+describe('useGetAuthorization', () => {
+ it('should return Authorization', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(authApi.getAuthorization).mockResolvedValue(mockedAuthorization);
+
+ const { result } = renderHook(() => useGetAuthorization(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockedAuthorization);
+ expect(authApi.getAuthorization).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.hook.tsx
new file mode 100644
index 000000000000..237fb8494c1b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.hook.tsx
@@ -0,0 +1,29 @@
+import { useMutation } from '@tanstack/react-query';
+import { AIError, PCIAi } from '@/data/api';
+import * as ai from '@/types/cloud/project/ai';
+import { postAuthorization } from '@/data/api/ai/authorization.api';
+
+export interface PostMutateAuthorizationProps {
+ onError: (cause: AIError) => void;
+ onSuccess: (auth: ai.AuthorizationStatus) => void;
+}
+
+export function usePostAuthorization({
+ onError,
+ onSuccess,
+}: PostMutateAuthorizationProps) {
+ const mutation = useMutation({
+ mutationFn: (projectId: PCIAi) => {
+ return postAuthorization(projectId);
+ },
+ onError,
+ onSuccess,
+ });
+
+ return {
+ postAuthorization: (projectId: PCIAi) => {
+ return mutation.mutate(projectId);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.spec.tsx
new file mode 100644
index 000000000000..b6e25068b4a1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/authorization/usePostAuthorization.spec.tsx
@@ -0,0 +1,40 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as authApi from '@/data/api/ai/authorization.api';
+import { usePostAuthorization } from './usePostAuthorization.hook';
+import { mockedAuthorization } from '@/__tests__/helpers/mocks/authorization';
+
+vi.mock('@/data/api/ai/authorization.api', () => ({
+ postAuthorization: vi.fn(),
+}));
+
+describe('usePostAuthorization', () => {
+ it('should call usePostAuthorization on mutation with data', async () => {
+ const projectId = 'projectId';
+ const onSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(authApi.postAuthorization).mockResolvedValue(mockedAuthorization);
+ const { result } = renderHook(
+ () => usePostAuthorization({ onError, onSuccess }),
+ { wrapper: QueryClientWrapper },
+ );
+
+ const postDatastoreProps = {
+ projectId,
+ };
+ result.current.postAuthorization(postDatastoreProps);
+
+ await waitFor(() => {
+ expect(authApi.postAuthorization).toHaveBeenCalledWith(
+ postDatastoreProps,
+ );
+ expect(onSuccess).toHaveBeenCalledWith(
+ mockedAuthorization,
+ postDatastoreProps,
+ undefined,
+ );
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.hook.tsx
new file mode 100644
index 000000000000..9a6c4b9052b8
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.hook.tsx
@@ -0,0 +1,25 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+import { getFlavor } from '@/data/api/ai/capabilities.api';
+
+export function useGetFlavor(
+ projectId: string,
+ region: string,
+ options: Omit = {},
+) {
+ const queryKey = [
+ projectId,
+ 'ai',
+ 'capabilities',
+ 'region',
+ region,
+ 'flavor',
+ ];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getFlavor({ projectId, region }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.spec.tsx
new file mode 100644
index 000000000000..7eb9b7700719
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetFlavor.spec.tsx
@@ -0,0 +1,35 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as capabilitiesApi from '@/data/api/ai/capabilities.api';
+import { mockedCapabilitiesFlavor } from '@/__tests__/helpers/mocks/flavor';
+import { useGetFlavor } from './useGetFlavor.hook';
+
+vi.mock('@/data/api/ai/capabilities.api', () => ({
+ getFlavor: vi.fn(),
+}));
+
+describe('useGetFlavor', () => {
+ it('should return flavor capabilities by region', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+
+ vi.mocked(capabilitiesApi.getFlavor).mockResolvedValue([
+ mockedCapabilitiesFlavor,
+ ]);
+
+ const { result } = renderHook(() => useGetFlavor(projectId, region), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedCapabilitiesFlavor]);
+ expect(capabilitiesApi.getFlavor).toHaveBeenCalledWith({
+ projectId,
+ region,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.hook.tsx
new file mode 100644
index 000000000000..8a8bc114ffa0
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { getRegions } from '@/data/api/ai/capabilities.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetRegions(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'capabilities', 'region'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getRegions({ projectId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.spec.tsx
new file mode 100644
index 000000000000..0eeeff0b50e6
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/capabilities/useGetRegions.spec.tsx
@@ -0,0 +1,33 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as capabilitiesApi from '@/data/api/ai/capabilities.api';
+import { mockedCapabilitiesRegion } from '@/__tests__/helpers/mocks/region';
+import { useGetRegions } from './useGetRegions.hook';
+
+vi.mock('@/data/api/ai/capabilities.api', () => ({
+ getRegions: vi.fn(),
+}));
+
+describe('useGetRegions', () => {
+ it('should return capabilities region', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(capabilitiesApi.getRegions).mockResolvedValue([
+ mockedCapabilitiesRegion,
+ ]);
+
+ const { result } = renderHook(() => useGetRegions(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedCapabilitiesRegion]);
+ expect(capabilitiesApi.getRegions).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.hook.tsx
new file mode 100644
index 000000000000..39df2495d978
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.hook.tsx
@@ -0,0 +1,29 @@
+import { useMutation } from '@tanstack/react-query';
+import { AIError } from '@/data/api';
+import { AddDatastoreProps, addDatastore } from '@/data/api/ai/datastore.api';
+import * as ai from '@/types/cloud/project/ai';
+
+export interface AddEditMutateDatastoreProps {
+ onError: (cause: AIError) => void;
+ onSuccess: (datastore: ai.DataStore) => void;
+}
+
+export function useAddDatastore({
+ onError,
+ onSuccess,
+}: AddEditMutateDatastoreProps) {
+ const mutation = useMutation({
+ mutationFn: (datastoreInfo: AddDatastoreProps) => {
+ return addDatastore(datastoreInfo);
+ },
+ onError,
+ onSuccess,
+ });
+
+ return {
+ addDatastore: (datastoreInfo: AddDatastoreProps) => {
+ return mutation.mutate(datastoreInfo);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.spec.tsx
new file mode 100644
index 000000000000..423814733add
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useAddDatastore.spec.tsx
@@ -0,0 +1,44 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import {
+ mockedDatastore,
+ mockedDatastoreInput,
+} from '@/__tests__/helpers/mocks/datastore';
+import { useAddDatastore } from './useAddDatastore.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ addDatastore: vi.fn(),
+}));
+
+describe('useAddDatastore', () => {
+ it('should call useAddDatastore on mutation with data', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const onSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(datastoreApi.addDatastore).mockResolvedValue(mockedDatastore);
+ const { result } = renderHook(
+ () => useAddDatastore({ onError, onSuccess }),
+ { wrapper: QueryClientWrapper },
+ );
+
+ const addDatastoreProps = {
+ projectId,
+ region,
+ datastore: mockedDatastoreInput,
+ };
+ result.current.addDatastore(addDatastoreProps);
+
+ await waitFor(() => {
+ expect(datastoreApi.addDatastore).toHaveBeenCalledWith(addDatastoreProps);
+ expect(onSuccess).toHaveBeenCalledWith(
+ mockedDatastore,
+ addDatastoreProps,
+ undefined,
+ );
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.hook.tsx
new file mode 100644
index 000000000000..a112f6bddc0b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.hook.tsx
@@ -0,0 +1,28 @@
+import { useMutation } from '@tanstack/react-query';
+import { AIError } from '@/data/api';
+import { deleteDatastore, DatastoreProps } from '@/data/api/ai/datastore.api';
+
+export interface DeleteMutateDatastoreProps {
+ onError: (cause: AIError) => void;
+ onSuccess: () => void;
+}
+
+export function useDeleteDatastore({
+ onError,
+ onSuccess,
+}: DeleteMutateDatastoreProps) {
+ const mutation = useMutation({
+ mutationFn: (datastore: DatastoreProps) => {
+ return deleteDatastore(datastore);
+ },
+ onError,
+ onSuccess,
+ });
+
+ return {
+ deleteDatastore: (datastore: DatastoreProps) => {
+ return mutation.mutate(datastore);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.spec.tsx
new file mode 100644
index 000000000000..b47e2695d7f7
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useDeleteDatastore.spec.tsx
@@ -0,0 +1,44 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import { useDeleteDatastore } from './useDeleteDatastore.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ deleteDatastore: vi.fn(),
+}));
+
+describe('useDeleteDatastore', () => {
+ it('should call useDeleteDatastore on mutation with data', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const alias = 'alias';
+ const onSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(datastoreApi.deleteDatastore).mockResolvedValue(undefined);
+ const { result } = renderHook(
+ () => useDeleteDatastore({ onError, onSuccess }),
+ { wrapper: QueryClientWrapper },
+ );
+
+ const deleteDatastoreProps = {
+ projectId,
+ region,
+ alias,
+ };
+ result.current.deleteDatastore(deleteDatastoreProps);
+
+ await waitFor(() => {
+ expect(datastoreApi.deleteDatastore).toHaveBeenCalledWith(
+ deleteDatastoreProps,
+ );
+ expect(onSuccess).toHaveBeenCalledWith(
+ undefined,
+ deleteDatastoreProps,
+ undefined,
+ );
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.hook.tsx
new file mode 100644
index 000000000000..79edde4afa04
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.hook.tsx
@@ -0,0 +1,23 @@
+import { useMutation } from '@tanstack/react-query';
+import { AddEditMutateDatastoreProps } from './useAddDatastore.hook';
+import { EditDatastoreProps, editDatastore } from '@/data/api/ai/datastore.api';
+
+export function useEditDatastore({
+ onError,
+ onSuccess,
+}: AddEditMutateDatastoreProps) {
+ const mutation = useMutation({
+ mutationFn: (datastoreInfo: EditDatastoreProps) => {
+ return editDatastore(datastoreInfo);
+ },
+ onError,
+ onSuccess,
+ });
+
+ return {
+ editDatastore: (datastoreInfo: EditDatastoreProps) => {
+ return mutation.mutate(datastoreInfo);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.spec.tsx
new file mode 100644
index 000000000000..7aae767ef167
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useEditDatastore.spec.tsx
@@ -0,0 +1,48 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import {
+ mockedDatastore,
+ mockedDatastoreInput,
+} from '@/__tests__/helpers/mocks/datastore';
+import { useEditDatastore } from './useEditDatastore.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ editDatastore: vi.fn(),
+}));
+
+describe('useEditDatastore', () => {
+ it('should call useEditDatastore on mutation with data', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const alias = 'alias';
+ const onSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(datastoreApi.editDatastore).mockResolvedValue(mockedDatastore);
+ const { result } = renderHook(
+ () => useEditDatastore({ onError, onSuccess }),
+ { wrapper: QueryClientWrapper },
+ );
+
+ const editDatastoreProps = {
+ projectId,
+ region,
+ alias,
+ datastore: mockedDatastoreInput,
+ };
+ result.current.editDatastore(editDatastoreProps);
+
+ await waitFor(() => {
+ expect(datastoreApi.editDatastore).toHaveBeenCalledWith(
+ editDatastoreProps,
+ );
+ expect(onSuccess).toHaveBeenCalledWith(
+ mockedDatastore,
+ editDatastoreProps,
+ undefined,
+ );
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.hook.tsx
new file mode 100644
index 000000000000..1e366bc0c4aa
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.hook.tsx
@@ -0,0 +1,19 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { getDatastore } from '@/data/api/ai/datastore.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetDatastore(
+ projectId: string,
+ region: string,
+ alias: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'data', 'region', region, 'alias', alias];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getDatastore({ projectId, region, alias }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.spec.tsx
new file mode 100644
index 000000000000..9ffaadab8a9c
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastore.spec.tsx
@@ -0,0 +1,35 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import { mockedDatastore } from '@/__tests__/helpers/mocks/datastore';
+import { useGetDatastore } from './useGetDatastore.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ getDatastore: vi.fn(),
+}));
+
+describe('useGetDatastores', () => {
+ it('should return Datastores', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const alias = 'alias';
+
+ vi.mocked(datastoreApi.getDatastore).mockResolvedValue(mockedDatastore);
+
+ const { result } = renderHook(
+ () => useGetDatastore(projectId, region, alias),
+ { wrapper: QueryClientWrapper },
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockedDatastore);
+ expect(datastoreApi.getDatastore).toHaveBeenCalledWith({
+ projectId,
+ region,
+ alias,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.hook.tsx
new file mode 100644
index 000000000000..62a925c644ec
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.hook.tsx
@@ -0,0 +1,28 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { getDatastoreAuth } from '@/data/api/ai/datastore.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetDatastoreAuth(
+ projectId: string,
+ region: string,
+ alias: string,
+ options: Omit = {},
+) {
+ const queryKey = [
+ projectId,
+ 'ai',
+ 'data',
+ 'region',
+ region,
+ 'alias',
+ alias,
+ 'auth',
+ ];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getDatastoreAuth({ projectId, region, alias }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.spec.tsx
new file mode 100644
index 000000000000..304e6a1954fd
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreAuth.spec.tsx
@@ -0,0 +1,37 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import { mockedDatastoreAuth } from '@/__tests__/helpers/mocks/datastore';
+import { useGetDatastoreAuth } from './useGetDatastoreAuth.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ getDatastoreAuth: vi.fn(),
+}));
+
+describe('useGetDatastoreAuth', () => {
+ it('should return Datastore Auth', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const alias = 'alias';
+
+ vi.mocked(datastoreApi.getDatastoreAuth).mockResolvedValue(
+ mockedDatastoreAuth,
+ );
+
+ const { result } = renderHook(
+ () => useGetDatastoreAuth(projectId, region, alias),
+ { wrapper: QueryClientWrapper },
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockedDatastoreAuth);
+ expect(datastoreApi.getDatastoreAuth).toHaveBeenCalledWith({
+ projectId,
+ region,
+ alias,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.hook.tsx
new file mode 100644
index 000000000000..cc1c28af0fb1
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.hook.tsx
@@ -0,0 +1,28 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import { getDatastoreContainer } from '@/data/api/ai/datastore.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+import { Containers } from '@/types/orderFunnel';
+
+export function useGetDatastoreContainer(
+ projectId: string,
+ region: string,
+ alias: string,
+ options: Omit = {},
+) {
+ const queryKey = [
+ projectId,
+ 'ai',
+ 'data',
+ 'region',
+ region,
+ 'alias',
+ alias,
+ 'containers',
+ ];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getDatastoreContainer({ projectId, region, alias }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.spec.tsx
new file mode 100644
index 000000000000..b8906e4d5400
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoreContainer.spec.tsx
@@ -0,0 +1,40 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import { getDatastoreContainer } from '@/data/api/ai/datastore.api';
+import { mockedContainer } from '@/__tests__/helpers/mocks/container';
+import { useGetDatastoreContainer } from './useGetDatastoreContainer.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ getDatastoreContainer: vi.fn(),
+}));
+
+describe('useGetDatastoreContainer', () => {
+ it('should return Container in Datastore', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+ const alias = 'alias';
+
+ vi.mocked(datastoreApi.getDatastoreContainer).mockResolvedValue(
+ mockedContainer,
+ );
+
+ const { result } = renderHook(
+ () => useGetDatastoreContainer(projectId, region, alias),
+ {
+ wrapper: QueryClientWrapper,
+ },
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockedContainer);
+ expect(datastoreApi.getDatastoreContainer).toHaveBeenCalledWith({
+ projectId,
+ region,
+ alias,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.hook.tsx
new file mode 100644
index 000000000000..4122480ae877
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.hook.tsx
@@ -0,0 +1,18 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { getDatastores } from '@/data/api/ai/datastore.api';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetDatastores(
+ projectId: string,
+ region: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'data', 'region', region, 'alias'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getDatastores({ projectId, region }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.spec.tsx
new file mode 100644
index 000000000000..3f998ce5fe79
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastores.spec.tsx
@@ -0,0 +1,32 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import { mockedDatastore } from '@/__tests__/helpers/mocks/datastore';
+import { useGetDatastores } from './useGetDatastores.hook';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ getDatastores: vi.fn(),
+}));
+
+describe('useGetDatastores', () => {
+ it('should return Datastores', async () => {
+ const projectId = 'projectId';
+ const region = 'region';
+
+ vi.mocked(datastoreApi.getDatastores).mockResolvedValue([mockedDatastore]);
+
+ const { result } = renderHook(() => useGetDatastores(projectId, region), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedDatastore]);
+ expect(datastoreApi.getDatastores).toHaveBeenCalledWith({
+ projectId,
+ region,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook.tsx
new file mode 100644
index 000000000000..a0a88de567dc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithContainers.hook.tsx
@@ -0,0 +1,95 @@
+import { QueryObserverOptions, useQueries } from '@tanstack/react-query';
+import { useCallback, useEffect, useRef } from 'react';
+import {
+ getDatastoreContainer,
+ getDatastores,
+} from '@/data/api/ai/datastore.api';
+import * as ai from '@/types/cloud/project/ai';
+import { Containers } from '@/types/orderFunnel';
+
+export interface DataStoresWithContainers extends ai.DataStore {
+ container?: string;
+ id: string;
+}
+
+export const useGetDatastoresWithContainers = (
+ projectId: string,
+ region: string,
+ datastores: ai.DataStore[],
+ options: Omit = {},
+) => {
+ const containersQueries = useQueries({
+ queries: datastores
+ ? datastores
+ // filter on type because git datastore does not have container
+ .filter((ds) => ds.type !== ai.DataStoreTypeEnum.git)
+ .map((ds) => {
+ return {
+ queryKey: [
+ projectId,
+ 'ai/data/region',
+ region,
+ 'alias',
+ ds.alias,
+ 'containers',
+ ],
+ queryFn: () =>
+ getDatastoreContainer({
+ projectId,
+ region,
+ alias: ds.alias,
+ }),
+ ...options,
+ select: (container: Containers) =>
+ container.containers.map((ct: string) => ({
+ id: `${ds.type} - ${ds.alias} - ${ct}`,
+ container: ct,
+ alias: ds.alias,
+ type: ds.type,
+ endpoint: ds.endpoint,
+ owner: ds.owner,
+ })),
+ };
+ })
+ : [],
+ });
+
+ const refetchAll = useCallback(() => {
+ containersQueries.forEach((ds) => ds.refetch());
+ }, [containersQueries]);
+
+ const dataStoresWithContainers: DataStoresWithContainers[] = containersQueries.flatMap(
+ (container) => {
+ return container.data || [];
+ },
+ );
+
+ // Add git datastore to the final object
+ datastores
+ ?.filter((ds) => ds.type === ai.DataStoreTypeEnum.git)
+ ?.forEach((gitDs) => {
+ const gitwithContainer: DataStoresWithContainers = {
+ id: `${gitDs.type} - ${gitDs.alias}`,
+ ...gitDs,
+ };
+ dataStoresWithContainers.push(gitwithContainer);
+ });
+
+ // refetch if pooling changes
+ const prevRefetchInterval = useRef(options?.refetchInterval);
+ useEffect(() => {
+ if (
+ options?.enabled !== false &&
+ options.refetchInterval !== undefined &&
+ options.refetchInterval !== prevRefetchInterval.current
+ ) {
+ refetchAll();
+ }
+ prevRefetchInterval.current = options.refetchInterval;
+ }, [options.refetchInterval, options.enabled, refetchAll]);
+
+ return {
+ data: dataStoresWithContainers,
+ refetchAll,
+ };
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.hook.tsx
new file mode 100644
index 000000000000..e6da7d5daf11
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.hook.tsx
@@ -0,0 +1,67 @@
+import { QueryObserverOptions, useQueries } from '@tanstack/react-query';
+import { useCallback, useEffect, useRef } from 'react';
+import { getDatastores } from '@/data/api/ai/datastore.api';
+import * as ai from '@/types/cloud/project/ai';
+
+export interface DataStoresWithRegion extends ai.DataStore {
+ region: string;
+}
+
+export const useGetDatastoresWithRegions = (
+ projectId: string,
+ regions: ai.capabilities.Region[],
+ options: Omit = {},
+) => {
+ const dataStoresQueries = useQueries({
+ queries: regions
+ ? regions.map((reg) => {
+ return {
+ queryKey: [projectId, 'ai/data/region', reg.id, 'alias'],
+ queryFn: () => getDatastores({ projectId, region: reg.id }),
+ ...options,
+ select: (datastore: ai.DataStore[]) =>
+ datastore.map((store: ai.DataStore) => ({
+ ...store,
+ region: reg.id,
+ })),
+ };
+ })
+ : [],
+ });
+
+ const refetchAll = useCallback(() => {
+ dataStoresQueries.forEach((ds) => ds.refetch());
+ }, [dataStoresQueries]);
+
+ const dataStoresWithRegion: DataStoresWithRegion[] = dataStoresQueries
+ .flatMap((dataStore) => {
+ return (
+ dataStore.data?.map((ds) => ({
+ endpoint: ds.endpoint,
+ region: ds.region,
+ alias: ds.alias,
+ owner: ds.owner,
+ type: ds.type,
+ })) || []
+ );
+ })
+ .filter((ds) => ds.owner === ai.DataStoreOwnerEnum.customer);
+
+ // refetch if pooling changes
+ const prevRefetchInterval = useRef(options?.refetchInterval);
+ useEffect(() => {
+ if (
+ options?.enabled !== false &&
+ options.refetchInterval !== undefined &&
+ options.refetchInterval !== prevRefetchInterval.current
+ ) {
+ refetchAll();
+ }
+ prevRefetchInterval.current = options.refetchInterval;
+ }, [options.refetchInterval, options.enabled, refetchAll]);
+
+ return {
+ data: dataStoresWithRegion,
+ refetchAll,
+ };
+};
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.spec.tsx
new file mode 100644
index 000000000000..60cd3d2bc85e
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/datastore/useGetDatastoresWithRegions.spec.tsx
@@ -0,0 +1,38 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as datastoreApi from '@/data/api/ai/datastore.api';
+import {
+ mockedDatastore,
+ mockedDatastoreWithRegion,
+} from '@/__tests__/helpers/mocks/datastore';
+import { useGetDatastoresWithRegions } from './useGetDatastoresWithRegions.hook';
+import { mockedCapabilitiesRegion } from '@/__tests__/helpers/mocks/region';
+
+vi.mock('@/data/api/ai/datastore.api', () => ({
+ getDatastores: vi.fn(),
+}));
+
+describe('useGetDatastoresWithRegions', () => {
+ it('should return Datastores', async () => {
+ const projectId = 'projectId';
+ const regions = [mockedCapabilitiesRegion];
+
+ vi.mocked(datastoreApi.getDatastores).mockResolvedValue([mockedDatastore]);
+
+ const { result } = renderHook(
+ () => useGetDatastoresWithRegions(projectId, regions),
+ {
+ wrapper: QueryClientWrapper,
+ },
+ );
+
+ await waitFor(() => {
+ expect(result.current.data).toEqual([mockedDatastoreWithRegion]);
+ expect(datastoreApi.getDatastores).toHaveBeenCalledWith({
+ projectId,
+ region: mockedCapabilitiesRegion.id,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.hook.tsx
new file mode 100644
index 000000000000..78540601f665
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.hook.tsx
@@ -0,0 +1,23 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+import { getGuides } from '@/data/api/ai/guide.api';
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetGuides(
+ projectId: string,
+ section?: string,
+ lang?: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'guides', section];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () =>
+ getGuides({
+ projectId,
+ section,
+ lang,
+ }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.spec.tsx
new file mode 100644
index 000000000000..8bd20b06cc40
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/guide/useGetGuides.spec.tsx
@@ -0,0 +1,30 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as guideApi from '@/data/api/ai/guide.api';
+import { useGetGuides } from './useGetGuides.hook';
+import { mockedGuides } from '@/__tests__/helpers/mocks/guides';
+
+vi.mock('@/data/api/ai/guide.api', () => ({
+ getGuides: vi.fn(),
+}));
+
+describe('useGetGuides', () => {
+ it('should return Guides', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(guideApi.getGuides).mockResolvedValue([mockedGuides]);
+
+ const { result } = renderHook(() => useGetGuides(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedGuides]);
+ expect(guideApi.getGuides).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.hook.tsx
new file mode 100644
index 000000000000..d2bf5ca43343
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '@/hooks/api/useImmediateRefetch';
+import { getEditor } from '@/data/api/ai/notebook/capabilities/editor.api';
+
+export function useGetEditor(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'notebook', 'capabilities', 'editor'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getEditor({ projectId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.spec.tsx
new file mode 100644
index 000000000000..02d0109fb9cc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetEditor.spec.tsx
@@ -0,0 +1,30 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as editorApi from '@/data/api/ai/notebook/capabilities/editor.api';
+import { useGetEditor } from './useGetEditor.hook';
+import { mockedEditor } from '@/__tests__/helpers/mocks/notebook/editor';
+
+vi.mock('@/data/api/ai/notebook/capabilities/editor.api', () => ({
+ getEditor: vi.fn(),
+}));
+
+describe('useGetEditor', () => {
+ it('should return Editor', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(editorApi.getEditor).mockResolvedValue([mockedEditor]);
+
+ const { result } = renderHook(() => useGetEditor(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedEditor]);
+ expect(editorApi.getEditor).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.hook.tsx
new file mode 100644
index 000000000000..9dc505e22f1a
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '@/hooks/api/useImmediateRefetch';
+import { getFramework } from '@/data/api/ai/notebook/capabilities/framework.api';
+
+export function useGetFramework(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'notebook', 'capabilities', 'framework'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getFramework({ projectId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.spec.tsx
new file mode 100644
index 000000000000..64d8591fe7bf
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/capabilities/useGetFramework.spec.tsx
@@ -0,0 +1,30 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as frameworkApi from '@/data/api/ai/notebook/capabilities/framework.api';
+import { useGetFramework } from './useGetFramework.hook';
+import { mockedFramework } from '@/__tests__/helpers/mocks/notebook/framework';
+
+vi.mock('@/data/api/ai/notebook/capabilities/framework.api', () => ({
+ getFramework: vi.fn(),
+}));
+
+describe('useGetFramework', () => {
+ it('should return Framework', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(frameworkApi.getFramework).mockResolvedValue([mockedFramework]);
+
+ const { result } = renderHook(() => useGetFramework(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedFramework]);
+ expect(frameworkApi.getFramework).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.hook.tsx
new file mode 100644
index 000000000000..c80866a6aed7
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.hook.tsx
@@ -0,0 +1,38 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { useParams } from 'react-router-dom';
+import { addNotebook } from '@/data/api/ai/notebook/notebook.api';
+import * as ai from '@/types/cloud/project/ai';
+import { AIError } from '@/data/api';
+
+interface AddNotebookProps {
+ onError: (cause: AIError) => void;
+ onSuccess: (notebook: ai.notebook.Notebook) => void;
+}
+
+export function useAddNotebook({ onError, onSuccess }: AddNotebookProps) {
+ const queryClient = useQueryClient();
+ const { projectId } = useParams();
+ const mutation = useMutation({
+ mutationFn: (notebookInfo: ai.notebook.NotebookSpec) => {
+ return addNotebook({ projectId, notebookInfo });
+ },
+ onError,
+ onSuccess: (data) => {
+ // invalidate services list to avoid displaying
+ // old list
+ queryClient.invalidateQueries({
+ queryKey: [projectId, 'ai/notebook'],
+ refetchType: 'none',
+ });
+ onSuccess(data);
+ },
+ });
+
+ return {
+ addNotebook: (notebookInfo: ai.notebook.NotebookSpec) => {
+ return mutation.mutate(notebookInfo);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.spec.tsx
new file mode 100644
index 000000000000..6391ca9120fc
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useAddNotebook.spec.tsx
@@ -0,0 +1,39 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as notebookApi from '@/data/api/ai/notebook/notebook.api';
+import {
+ mockedNotebook,
+ mockedNotebookSpec,
+} from '@/__tests__/helpers/mocks/notebook';
+import { useAddNotebook } from './useAddNotebook.hook';
+
+vi.mock('@/data/api/ai/notebook/notebook.api', () => ({
+ addNotebook: vi.fn(),
+}));
+
+describe('useAddNotebooks', () => {
+ it('should create a Notebook', async () => {
+ const onSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(notebookApi.addNotebook).mockResolvedValue(mockedNotebook);
+
+ const { result } = renderHook(
+ () => useAddNotebook({ onError, onSuccess }),
+ {
+ wrapper: QueryClientWrapper,
+ },
+ );
+
+ result.current.addNotebook(mockedNotebookSpec);
+
+ await waitFor(() => {
+ expect(notebookApi.addNotebook).toHaveBeenCalledWith({
+ projectId: undefined,
+ notebookInfo: mockedNotebookSpec,
+ });
+ expect(onSuccess).toHaveBeenCalledWith(mockedNotebook);
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.hook.tsx
new file mode 100644
index 000000000000..1418c206c50f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.hook.tsx
@@ -0,0 +1,37 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useParams } from 'react-router-dom';
+import { AIError, NotebookData } from '@/data/api';
+import { deleteNotebook } from '@/data/api/ai/notebook/notebook.api';
+
+interface UseDeleteNotebook {
+ onError: (cause: AIError) => void;
+ onDeleteSuccess: () => void;
+}
+
+export function useDeleteNotebook({
+ onError,
+ onDeleteSuccess,
+}: UseDeleteNotebook) {
+ const queryClient = useQueryClient();
+ const { projectId } = useParams();
+ const mutation = useMutation({
+ mutationFn: (notebookInfo: NotebookData) => {
+ return deleteNotebook(notebookInfo);
+ },
+ onError,
+ onSuccess: () => {
+ onDeleteSuccess();
+ // Invalidate service list query to get the latest data
+ queryClient.invalidateQueries({
+ queryKey: [projectId, 'ai/notebook'],
+ });
+ },
+ });
+
+ return {
+ deleteNotebook: (notebookInfo: NotebookData) => {
+ return mutation.mutate(notebookInfo);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.spec.tsx
new file mode 100644
index 000000000000..5e72394b154b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useDeleteNotebook.spec.tsx
@@ -0,0 +1,40 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as notebookApi from '@/data/api/ai/notebook/notebook.api';
+import { useDeleteNotebook } from './useDeleteNotebook.hook';
+
+vi.mock('@/data/api/ai/notebook/notebook.api', () => ({
+ deleteNotebook: vi.fn(),
+}));
+
+describe('useDeleteNotebooks', () => {
+ it('should delete a Notebook', async () => {
+ const projectId = 'projectId';
+ const notebookId = 'notebookId';
+ const onDeleteSuccess = vi.fn();
+ const onError = vi.fn();
+
+ vi.mocked(notebookApi.deleteNotebook).mockResolvedValue(undefined);
+
+ const { result } = renderHook(
+ () => useDeleteNotebook({ onError, onDeleteSuccess }),
+ {
+ wrapper: QueryClientWrapper,
+ },
+ );
+
+ const deleteNotebookProps = {
+ projectId,
+ notebookId,
+ };
+ result.current.deleteNotebook(deleteNotebookProps);
+
+ await waitFor(() => {
+ expect(notebookApi.deleteNotebook).toHaveBeenCalledWith(
+ deleteNotebookProps,
+ );
+ expect(onDeleteSuccess).toHaveBeenCalledWith();
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetCommand.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetCommand.hook.tsx
new file mode 100644
index 000000000000..0b5dfa8bb614
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetCommand.hook.tsx
@@ -0,0 +1,38 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { useParams } from 'react-router-dom';
+import * as ai from '@/types/cloud/project/ai';
+import { AIError } from '@/data/api';
+import { getCommand } from '@/data/api/ai/notebook/notebook.api';
+
+interface GetCommandProps {
+ onError: (cause: AIError) => void;
+ onSuccess: (notebook: ai.Command) => void;
+}
+
+export function useGetCommand({ onError, onSuccess }: GetCommandProps) {
+ const queryClient = useQueryClient();
+ const { projectId } = useParams();
+ const mutation = useMutation({
+ mutationFn: (notebookInfo: ai.notebook.NotebookSpec) => {
+ return getCommand({ projectId, notebookInfo });
+ },
+ onError,
+ onSuccess: (data) => {
+ // invalidate services list to avoid displaying
+ // old list
+ queryClient.invalidateQueries({
+ queryKey: [projectId, 'ai/notebook'],
+ refetchType: 'none',
+ });
+ onSuccess(data);
+ },
+ });
+
+ return {
+ getCommand: (notebookInfo: ai.notebook.NotebookSpec) => {
+ return mutation.mutate(notebookInfo);
+ },
+ ...mutation,
+ };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.hook.tsx
new file mode 100644
index 000000000000..690a6ff5474f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.hook.tsx
@@ -0,0 +1,18 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '@/hooks/api/useImmediateRefetch';
+import { getNotebook } from '@/data/api/ai/notebook/notebook.api';
+import { AIError } from '@/data/api';
+
+export function useGetNotebook(
+ projectId: string,
+ notebookId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai/notebook', notebookId];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getNotebook({ projectId, notebookId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.spec.tsx
new file mode 100644
index 000000000000..5ecd8308bf47
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebook.spec.tsx
@@ -0,0 +1,32 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as notebookApi from '@/data/api/ai/notebook/notebook.api';
+import { mockedNotebook } from '@/__tests__/helpers/mocks/notebook';
+import { useGetNotebook } from './useGetNotebook.hook';
+
+vi.mock('@/data/api/ai/notebook/notebook.api', () => ({
+ getNotebook: vi.fn(),
+}));
+
+describe('useGetNotebook', () => {
+ it('should return Notebook', async () => {
+ const projectId = 'projectId';
+ const notebookId = 'notebookId';
+
+ vi.mocked(notebookApi.getNotebook).mockResolvedValue(mockedNotebook);
+
+ const { result } = renderHook(() => useGetNotebook(projectId, notebookId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockedNotebook);
+ expect(notebookApi.getNotebook).toHaveBeenCalledWith({
+ projectId,
+ notebookId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.hook.tsx
new file mode 100644
index 000000000000..f5ffed2cf769
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import { getNotebooks } from '@/data/api/ai/notebook/notebook.api';
+import * as ai from '@/types/cloud/project/ai';
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+
+export function useGetNotebooks(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'notebook'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getNotebooks({ projectId }),
+ ...options,
+ }) as UseQueryResult;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.spec.tsx
new file mode 100644
index 000000000000..79fe75381550
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetNotebooks.spec.tsx
@@ -0,0 +1,30 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper';
+import * as notebookApi from '@/data/api/ai/notebook/notebook.api';
+import { mockedNotebook } from '@/__tests__/helpers/mocks/notebook';
+import { useGetNotebooks } from './useGetNotebooks.hook';
+
+vi.mock('@/data/api/ai/notebook/notebook.api', () => ({
+ getNotebooks: vi.fn(),
+}));
+
+describe('useGetNotebooks', () => {
+ it('should return Notebooks', async () => {
+ const projectId = 'projectId';
+
+ vi.mocked(notebookApi.getNotebooks).mockResolvedValue([mockedNotebook]);
+
+ const { result } = renderHook(() => useGetNotebooks(projectId), {
+ wrapper: QueryClientWrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual([mockedNotebook]);
+ expect(notebookApi.getNotebooks).toHaveBeenCalledWith({
+ projectId,
+ });
+ });
+ });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetSuggestions.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetSuggestions.hook.tsx
new file mode 100644
index 000000000000..6aeb5484df44
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useGetSuggestions.hook.tsx
@@ -0,0 +1,17 @@
+import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query';
+
+import { useQueryImmediateRefetch } from '../../useImmediateRefetch';
+import { getSuggestions } from '@/data/api/ai/notebook/suggestions.api';
+import { Suggestions } from '@/types/orderFunnel';
+
+export function useGetSuggestions(
+ projectId: string,
+ options: Omit = {},
+) {
+ const queryKey = [projectId, 'ai', 'notebook', 'suggestions'];
+ return useQueryImmediateRefetch({
+ queryKey,
+ queryFn: () => getSuggestions({ projectId }),
+ ...options,
+ }) as UseQueryResult