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 index 0c243601e95a..80c62dfa68a3 100644 --- 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 @@ -8,7 +8,8 @@ "tableHeaderPrivacy": "Confidentialité ", "networkSecureTitle": "Privé", "networkPublicTitle": "Public", - "tableHeaderDuration": "Durée de fonctionnement", + "tableHeaderDuration": "Durée", + "durationHelper": "Durée de fonctionnement du notebook", "tableHeaderStatus": "Statut", "tableActionManage": "Gérer", "tableActionStart": "Démarrer", 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 index 61c1938c535c..991232f9ec85 100644 --- 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 @@ -53,6 +53,6 @@ "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", + "cliEquivalentModalDescription": "Commande 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 index afeff6751635..36d7b5cbc248 100644 --- 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 @@ -1,4 +1,8 @@ { + "dashboardTab": "Dashboard", + "dataTab": "Données attachées", + "backupTab": "Backup", + "logsTab": "Logs", "deleteNotebookTitle": "Supprimer le notebook", "deleteNotebookDescription": "Etes-vous sur de vouloir supprimer le notebook {{name}} ?", "notebookButtonCancel": "Annuler", diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/dashboard/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/dashboard/Messages_fr_FR.json new file mode 100644 index 000000000000..ff3838a7d758 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/dashboard/Messages_fr_FR.json @@ -0,0 +1,29 @@ +{ + "dashboardTitle": "Dashboard", + "resourcesTitle": "Ressources", + "powerTitleSection": "Power", + "computeTitleSection": "Compute", + "storageTitleSection": "Stockage", + "gpuMemoryField": "{{ gpu }} x {{ memory }} RAM", + "gcuComputeField": "CPU: {{ cpu }} vCores ", + "memoryField": "RAM: {{ memory }}", + "publicNetworkField": "Réseau public: {{ network }}/s", + "temporaryLocalStorageField": "Stockage local éphémère: {{ storage }} SSD", + "temporaryLocalStorageHelper": "Stockage local performant, mais non sauvegardé", + "workspaceStorage": "Espace de travail {{ storage }} SSD inclus", + "workspaceStorageHelper": "Stockage distant sauvegardé. Au-delà de 30 jours consécutifs de stockage ou/et du dépassement de ce quota de 10 Gio, les tarifs appliqués seront ceux de Public Cloud Object Storage.", + "sliderInfo": "{{ usedStorage }} / {{ totalStorage }}", + "notebookIdLabel": "Id du notebook", + "notebookIdCopyToast": "L'identifiant du notebook a été copié", + "billingLink": "Gérer la facturation", + "supportLink": "Contacter le support", + "deleteNotebookButton": "Supprimer", + "addLabelTitle": "Ajouter un label", + "keyFieldLabel": "Clé", + "valueFieldLabel": "Valeur", + "addLabelButtonCancel": "Annuler", + "addLabelNameSubmit": "Ajouter", + "notebookToastErrorTitle": "Erreur", + "notebookToastSuccessTitle": "Succés", + "deleteNotebookSuccess": "Le label a été correctement ajouté à votre notebook" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/label.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/label.ts new file mode 100644 index 000000000000..4aad2e74b23b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/label.ts @@ -0,0 +1,6 @@ +import * as ai from '@/types/cloud/project/ai'; + +export const mockedLabel: ai.Label = { + name: 'labelName', + value: 'labelValue', +}; 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 index a38aceed6c35..5ffb8326e28a 100644 --- 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 @@ -8,9 +8,15 @@ interface CliCodeBlockProps { title: string; code: string; toastMessage?: string; + size?: string; } -const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => { +const CliCodeBlock = ({ + title, + code, + toastMessage, + size, +}: CliCodeBlockProps) => { const { t } = useTranslation('common'); const toast = useToast(); const handleCopyPass = (valueToCopy: string) => { @@ -22,7 +28,7 @@ const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => { return (
-
+

{title}

- +
       
     
-    
-  
+ 
 ))
 Slider.displayName = SliderPrimitive.Root.displayName
 
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.api.ts
new file mode 100644
index 000000000000..cd10779e8a16
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.api.ts
@@ -0,0 +1,16 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { NotebookData } from '@/data/api';
+import * as ai from '@/types/cloud/project/ai';
+
+export interface EditLabelProps extends NotebookData {
+  label: ai.Label;
+}
+
+export const editLabel = async ({
+  projectId,
+  notebookId,
+  label,
+}: EditLabelProps) =>
+  apiClient.v6
+    .put(`/cloud/project/${projectId}/ai/notebook/${notebookId}/label`, label)
+    .then((res) => res.data);
diff --git a/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.spec.tsx
new file mode 100644
index 000000000000..92ebaf77445b
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/label/label.spec.tsx
@@ -0,0 +1,36 @@
+import { apiClient } from '@ovh-ux/manager-core-api';
+import { describe, expect, vi } from 'vitest';
+import { editLabel } from './label.api';
+import { mockedLabel } from '@/__tests__/helpers/mocks/label';
+
+vi.mock('@ovh-ux/manager-core-api', () => {
+  const put = vi.fn(() => {
+    return Promise.resolve({ data: null });
+  });
+  return {
+    apiClient: {
+      v6: {
+        put,
+      },
+    },
+  };
+});
+
+describe('label functions', () => {
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('should call editLabel with labelInput', async () => {
+    expect(apiClient.v6.put).not.toHaveBeenCalled();
+    await editLabel({
+      projectId: 'projectId',
+      notebookId: 'notebookId',
+      label: mockedLabel,
+    });
+    expect(apiClient.v6.put).toHaveBeenCalledWith(
+      '/cloud/project/projectId/ai/notebook/notebookId/label',
+      mockedLabel,
+    );
+  });
+});
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.hook.tsx
new file mode 100644
index 000000000000..c7ad2f1213f7
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.hook.tsx
@@ -0,0 +1,28 @@
+import { useMutation } from '@tanstack/react-query';
+import { AIError } from '@/data/api';
+import {
+  EditLabelProps,
+  editLabel,
+} from '@/data/api/ai/notebook/label/label.api';
+
+export interface MutateLabelProps {
+  onError: (cause: AIError) => void;
+  onSuccess: () => void;
+}
+
+export function useEditLabel({ onError, onSuccess }: MutateLabelProps) {
+  const mutation = useMutation({
+    mutationFn: (labelInfo: EditLabelProps) => {
+      return editLabel(labelInfo);
+    },
+    onError,
+    onSuccess,
+  });
+
+  return {
+    editLabel: (labelInfo: EditLabelProps) => {
+      return mutation.mutate(labelInfo);
+    },
+    ...mutation,
+  };
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.spec.tsx
new file mode 100644
index 000000000000..55a3b5bde15f
--- /dev/null
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/label/useEditLabel.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 labelApi from '@/data/api/ai/notebook/label/label.api';
+import { useEditLabel } from './useEditLabel.hook';
+import { mockedLabel } from '@/__tests__/helpers/mocks/label';
+
+vi.mock('@/data/api/ai/notebook/label/label.api', () => ({
+  editLabel: vi.fn(),
+}));
+
+describe('useEditLabel', () => {
+  it('should call useEditLabel on mutation with data', async () => {
+    const projectId = 'projectId';
+    const notebookId = 'notebookId';
+    const onSuccess = vi.fn();
+    const onError = vi.fn();
+
+    vi.mocked(labelApi.editLabel).mockResolvedValue(mockedLabel);
+    const { result } = renderHook(() => useEditLabel({ onError, onSuccess }), {
+      wrapper: QueryClientWrapper,
+    });
+
+    const editLabelProps = {
+      projectId,
+      notebookId,
+      label: mockedLabel,
+    };
+    result.current.editLabel(editLabelProps);
+
+    await waitFor(() => {
+      expect(labelApi.editLabel).toHaveBeenCalledWith(editLabelProps);
+      expect(onSuccess).toHaveBeenCalledWith(
+        mockedLabel,
+        editLabelProps,
+        undefined,
+      );
+    });
+  });
+});
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
index f5ffed2cf769..18f495617aa6 100644
--- 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
@@ -8,7 +8,7 @@ export function useGetNotebooks(
   projectId: string,
   options: Omit = {},
 ) {
-  const queryKey = [projectId, 'ai', 'notebook'];
+  const queryKey = [projectId, 'ai/notebook'];
   return useQueryImmediateRefetch({
     queryKey,
     queryFn: () => getNotebooks({ projectId }),
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStartNotebook.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStartNotebook.hook.tsx
index 1155f03ff555..70b83ff31935 100644
--- a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStartNotebook.hook.tsx
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStartNotebook.hook.tsx
@@ -20,11 +20,11 @@ export function useStartNotebook({
     },
     onError,
     onSuccess: () => {
-      onStartSuccess();
       // Invalidate service list query to get the latest data
       queryClient.invalidateQueries({
         queryKey: [projectId, 'ai/notebook'],
       });
+      onStartSuccess();
     },
   });
 
diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStopNotebook.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStopNotebook.hook.tsx
index f88429cf370e..4e93859e7c60 100644
--- a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStopNotebook.hook.tsx
+++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/useStopNotebook.hook.tsx
@@ -17,11 +17,11 @@ export function useStopNotebook({ onError, onStopSuccess }: UseStopNotebook) {
     },
     onError,
     onSuccess: () => {
-      onStopSuccess();
       // Invalidate service list query to get the latest data
       queryClient.invalidateQueries({
         queryKey: [projectId, 'ai/notebook'],
       });
+      onStopSuccess();
     },
   });
 
diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/statusHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts
similarity index 72%
rename from packages/manager/apps/pci-ai-notebooks/src/lib/statusHelper.ts
rename to packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts
index 8f58386e1e08..fbd97ae518d0 100644
--- a/packages/manager/apps/pci-ai-notebooks/src/lib/statusHelper.ts
+++ b/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts
@@ -13,3 +13,12 @@ export function isDeletingNotebook(
 ) {
   return currentState === ai.notebook.NotebookStateEnum.DELETING;
 }
+
+export const OVH_TAGS_CONFIG = {
+  id: 'ovh/id',
+  type: 'ovh/type',
+};
+
+export function isOvhTags(key: string) {
+  return key === OVH_TAGS_CONFIG.id || key === OVH_TAGS_CONFIG.type;
+}
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.layout.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.layout.tsx
index 28be211174d1..bd120e297903 100644
--- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.layout.tsx
+++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.layout.tsx
@@ -7,6 +7,8 @@ import { useUserActivityContext } from '@/contexts/UserActivityContext';
 import { getNotebook } from '@/data/api/ai/notebook/notebook.api';
 import { useGetNotebook } from '@/hooks/api/ai/notebook/useGetNotebook.hook';
 import { NotebookLayoutContext } from './Notebook.context';
+import { NotebookHeader } from './_components/NotebookHeader.component';
+import NotebookTabs from './_components/NotebookTabs.component';
 
 interface NotebookLayoutProps {
   params: {
@@ -55,6 +57,7 @@ export default function NotebookLayout() {
   if (!notebook) {
     return (
       <>
+        
         
         Loading your notebook data
       
@@ -67,7 +70,8 @@ export default function NotebookLayout() {
 
   return (
     <>
-      
{notebook.spec.name}
+ +
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookHeader.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookHeader.component.tsx new file mode 100644 index 000000000000..00989378f677 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookHeader.component.tsx @@ -0,0 +1,114 @@ +import { useTranslation } from 'react-i18next'; +import { + NotebookText, + Play, + PlayCircle, + PlayIcon, + Square, + Trash2, +} from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import * as ai from '@/types/cloud/project/ai'; +import NotebookStatusBadge from '../../_components/NotebookStatusBadge.component'; +import { Button } from '@/components/ui/button'; +import StartNotebook from './StartNotebook.component'; +import { useModale } from '@/hooks/useModale'; +import StopNotebook from './StopNotebook.component'; +import { isDeletingNotebook, isRunningNotebook } from '@/lib/notebookHelper'; + +export const NotebookHeader = ({ + notebook, +}: { + notebook: ai.notebook.Notebook; +}) => { + const { t } = useTranslation('regions'); + const startModale = useModale('start'); + const stopModale = useModale('stop'); + return ( +
+
+ +
+
+
+

{notebook.spec.name ?? 'Dashboard'}

+
+ + +
+
+
+ + + {notebook.spec.env.frameworkId} + + + {notebook.spec.env.frameworkVersion} + + + {t(`region_${notebook.spec.region}`)} + + + {notebook.spec.env.editorId} + +
+
+ { + startModale.close(); + }} + /> + { + stopModale.close(); + }} + /> +
+ ); +}; + +NotebookHeader.Skeleton = function NotebookHeaderSkeleton() { + return ( +
+ +
+

Dashboard

+
+ + + + +
+
+
+ ); +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookTabs.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookTabs.component.tsx new file mode 100644 index 000000000000..bf977ff641a6 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/NotebookTabs.component.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from 'react-i18next'; +import TabsMenu from '@/components/tabs-menu/TabsMenu.component'; +import * as ai from '@/types/cloud/project/ai'; + +interface NotebookTabsProps { + notebook: ai.notebook.Notebook; +} + +const NotebookTabs = ({ notebook }: NotebookTabsProps) => { + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); + + const tabs = [ + { href: '', label: t('dashboardTab'), end: true }, + { href: 'attach-data', label: t('dataTab') }, + { href: 'backup', label: t('backupTab') }, + { href: 'logs', label: t('logsTab') }, + ].filter((tab) => tab); + + return ; +}; + +export default NotebookTabs; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StartNotebook.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StartNotebook.component.tsx index be8b5c726b76..9f7b98f882ca 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StartNotebook.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StartNotebook.component.tsx @@ -18,7 +18,7 @@ import { useStartNotebook } from '@/hooks/api/ai/notebook/useStartNotebook.hook' interface StartNotebookModalProps { notebook: ai.notebook.Notebook; controller: ModalController; - onSuccess?: (notebook: ai.notebook.Notebook) => void; + onSuccess?: () => void; onError?: (service: Error) => void; } @@ -52,7 +52,7 @@ const StartNotebook = ({ }), }); if (onSuccess) { - onSuccess(notebook); + onSuccess(); } }, }); diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StopNotebook.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StopNotebook.component.tsx index 599f871adf14..4084dcbdc9d1 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StopNotebook.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/StopNotebook.component.tsx @@ -18,7 +18,7 @@ import { useStopNotebook } from '@/hooks/api/ai/notebook/useStopNotebook.hook'; interface StopNotebookModalProps { notebook: ai.notebook.Notebook; controller: ModalController; - onSuccess?: (notebook: ai.notebook.Notebook) => void; + onSuccess?: () => void; onError?: (service: Error) => void; } @@ -52,7 +52,7 @@ const StopNotebook = ({ }), }); if (onSuccess) { - onSuccess(notebook); + onSuccess(); } }, }); diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/Dashboard.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/Dashboard.page.tsx new file mode 100644 index 000000000000..cb3f9f45c128 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/Dashboard.page.tsx @@ -0,0 +1,141 @@ +import { useTranslation } from 'react-i18next'; +import { + ArrowRight, + Atom, + Globe2, + Settings2, + TerminalSquare, + UserCheck, +} from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { useNotebookData } from '../Notebook.context'; +import Resources from './_components/Resources.component'; +import Configurations from './_components/Configuration.component'; +import Privacy from './_components/Privacy.component'; +import { useGetCommand } from '@/hooks/api/ai/notebook/useGetCommand.hook'; +import { getAIApiErrorMessage } from '@/lib/apiHelper'; +import { useToast } from '@/components/ui/use-toast'; +import * as ai from '@/types/cloud/project/ai'; +import CliCodeBlock from '@/components/cli-code-block/CliCodeBlock.component'; +import OvhLink from '@/components/links/OvhLink.component'; + +const Dashboard = () => { + const { notebook, projectId } = useNotebookData(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/dashboard'); + const { toast } = useToast(); + const [command, setCommand] = useState({}); + + const { getCommand, isPending: isPendingCommand } = useGetCommand({ + onError: (err) => { + toast({ + title: t('errorGetCommandCli'), + variant: 'destructive', + description: getAIApiErrorMessage(err), + }); + }, + onSuccess: (cliCommand) => { + setCommand(cliCommand); + }, + }); + + useEffect(() => { + const filteredVolume: ai.volume.Volume[] = notebook.spec.volumes.filter( + (vol) => vol.dataStore.internal === false, + ); + getCommand({ ...notebook.spec, volumes: filteredVolume }); + }, [notebook]); + + return ( + <> +

{t('dashboardTitle')}

+
+ + +

+ + {t('resourcesTitle')} +

+
+ + + +
+ Cycle de vie + + +

+ + Privacy +

+
+ + + +
+
+
+ + +

+ + Support & Facturation +

+
+ +
+ + {t('billingLink')} + + +
+
+ + {t('supportLink')} + + +
+
+
+ + +

+ + Configuration +

+
+ + + +
+
+ + +

+ + CLI +

+
+ + {command && ( + + )} + +
+ + ); +}; + +export default Dashboard; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AddLabel.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AddLabel.component.tsx new file mode 100644 index 000000000000..2c7762ee5655 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AddLabel.component.tsx @@ -0,0 +1,173 @@ +import { useParams } from 'react-router-dom'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { ModalController } from '@/hooks/useModale'; +import { useToast } from '@/components/ui/use-toast'; +import * as ai from '@/types/cloud/project/ai'; +import { useEditLabel } from '@/hooks/api/ai/notebook/label/useEditLabel.hook'; +import { getAIApiErrorMessage } from '@/lib/apiHelper'; + +interface AddLabelProps { + notebook: ai.notebook.Notebook; + controller: ModalController; + onSuccess?: () => void; + onError?: (error: Error) => void; +} + +const AddLabel = ({ + notebook, + controller, + onError, + onSuccess, +}: AddLabelProps) => { + // import translations + const { projectId } = useParams(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/dashboard'); + const toast = useToast(); + + const { editLabel, isPending } = useEditLabel({ + onError: (err) => { + toast.toast({ + title: t('notebookToastErrorTitle'), + variant: 'destructive', + description: getAIApiErrorMessage(err), + }); + if (onError) { + onError(err); + } + }, + onSuccess: () => { + toast.toast({ + title: t('notebookToastSuccessTitle'), + description: t('deleteNotebookSuccess'), + }); + if (onSuccess) { + onSuccess(); + } + }, + }); + // define the schema for the form + const labelSchema = z.object({ + name: z + .string() + .min(1) + .max(15) + .refine( + (newKey) => notebook.spec.labels && !(newKey in notebook.spec.labels), + { + message: t('existingKeyError'), + }, + ), + value: z + .string() + .min(1) + .max(15), + }); + // generate a form controller + const form = useForm>({ + resolver: zodResolver(labelSchema), + defaultValues: { + name: '', + value: '', + }, + }); + + const onSubmit = form.handleSubmit((formValues) => { + editLabel({ + projectId: projectId, + notebookId: notebook.id, + label: { + name: formValues.name, + value: formValues.value, + }, + }); + }); + + return ( + + + + + {t('addLabelTitle')} + + + +
+ + ( + + + {t('keyFieldLabel')} + + + + + + + )} + /> + ( + + {t('valueFieldLabel')} + + + + + + )} + /> + + + + + + + + +
+
+ ); +}; + +export default AddLabel; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Configuration.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Configuration.component.tsx new file mode 100644 index 000000000000..b13ea0c1d621 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Configuration.component.tsx @@ -0,0 +1,70 @@ +import { Files } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useNotebookData } from '../../Notebook.context'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import DeleteNotebook from '../../_components/DeleteNotebook.component'; +import { useGetNotebooks } from '@/hooks/api/ai/notebook/useGetNotebooks.hook'; +import { useModale } from '@/hooks/useModale'; +import { isRunningNotebook } from '@/lib/notebookHelper'; + +const Configurations = () => { + const { notebook, notebookQuery, projectId } = useNotebookData(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/dashboard'); + const navigate = useNavigate(); + const toast = useToast(); + const deleteModale = useModale('delete'); + + const getNotebooksQuery = useGetNotebooks(projectId, { + enabled: false, + }); + + return ( +
+
+
+

{t('notebookIdLabel')}

+

{notebook.id}

+
+ +
+ + { + deleteModale.close(); + notebookQuery.refetch(); + getNotebooksQuery.refetch(); + navigate(`../../../`); + }} + /> +
+ ); +}; + +export default Configurations; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Privacy.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Privacy.component.tsx new file mode 100644 index 000000000000..28058246e3da --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Privacy.component.tsx @@ -0,0 +1,111 @@ +import { Plus, ShieldAlert, ShieldCheck, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useNotebookData } from '../../Notebook.context'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { isOvhTags } from '@/lib/notebookHelper'; +import { useEditLabel } from '@/hooks/api/ai/notebook/label/useEditLabel.hook'; +import { useToast } from '@/components/ui/use-toast'; +import { getAIApiErrorMessage } from '@/lib/apiHelper'; +import AddLabel from './AddLabel.component'; +import { useModale } from '@/hooks/useModale'; +import { CONFIGURATION_CONFIG } from '@/components/order/configuration/configuration.constants'; + +const Privacy = () => { + const { notebook, notebookQuery, projectId } = useNotebookData(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/dashboard'); + const toast = useToast(); + const addModale = useModale('add'); + + const { editLabel, isPending } = useEditLabel({ + onError: (err) => { + toast.toast({ + title: t('notebookToastErrorTitle'), + variant: 'destructive', + description: getAIApiErrorMessage(err), + }); + }, + onSuccess: () => { + toast.toast({ + title: t('notebookToastSuccessTitle'), + description: t('deleteNotebookSuccess'), + }); + notebookQuery.refetch(); + }, + }); + + const handleDeleteLabel = (key: string) => { + editLabel({ + projectId, + notebookId: notebook.id, + label: { + name: key, + }, + }); + }; + + return ( + <> +
+
+
Tags
+ +
+
+ {notebook.spec.labels && + Object.entries(notebook.spec.labels).map(([key, value]) => ( + +
+ + {key} = {value} + + {!isOvhTags(key) && ( + + )} +
+
+ ))} +
+
+ {notebook.spec.unsecureHttp ? ( +
+
Accès publique
+ +
+ ) : ( +
+
Accès privé
+ +
+ )} +
+
+ { + addModale.close(); + notebookQuery.refetch(); + }} + /> + + ); +}; + +export default Privacy; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Resources.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Resources.component.tsx new file mode 100644 index 000000000000..ebb72f05e129 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/Resources.component.tsx @@ -0,0 +1,151 @@ +import { Cpu, HardDrive, HelpCircle, MemoryStick, Zap } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useNotebookData } from '../../Notebook.context'; +import { bytesConverter } from '@/lib/bytesHelper'; +import { Slider } from '@/components/ui/slider'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; + +const Resources = () => { + const { notebook } = useNotebookData(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/dashboard'); + return ( +
+ {notebook.spec.resources.gpu > 0 ? ( +
+
+
Power
+ +
+ + {`${notebook.spec.resources.gpu} x ${notebook.spec.resources.flavor}`} + + + {`${notebook.spec.resources.gpu} x ${notebook.spec.resources.gpuModel}`} + + + {t('gpuMemoryField', { + gpu: notebook.spec.resources.gpu, + memory: bytesConverter( + notebook.spec.resources.gpuMemory, + false, + 0, + ), + })} + +
+ ) : ( +
+
+
{t('powerTitleSection')}
+ +
+ + {`${notebook.spec.resources.cpu} x ${notebook.spec.resources.flavor}`} + + {`${notebook.spec.resources.cpu} x INTEL CPU VCORES`} +
+ )} +
+
+
{t('computeTitleSection')}
+ +
+ {notebook.spec.resources.gpu > 0 && ( + + {t('gcuComputeField', { + cpu: notebook.spec.resources.cpu, + })} + + )} + + {t('memoryField', { + memory: bytesConverter(notebook.spec.resources.memory, false, 0), + })} + + + {t('publicNetworkField', { + network: bytesConverter( + notebook.spec.resources.publicNetwork, + true, + 2, + ), + })} + + +
+
{t('storageTitleSection')}
+ +
+
+ + {t('temporaryLocalStorageField', { + storage: bytesConverter( + notebook.spec.resources.ephemeralStorage, + false, + 0, + ), + })} + + + + + + +

{t('temporaryLocalStorageHelper')}

+
+
+
+
+
+ + {t('workspaceStorage', { + storage: bytesConverter( + notebook.status.workspace.storageFree, + false, + 1, + ), + })} + + + + + + +

{t('workspaceStorageHelper')}

+
+
+
+
+ + + {t('sliderInfo', { + usedStorage: bytesConverter( + notebook.status.workspace.storageUsed, + false, + 1, + ), + totalStorage: bytesConverter( + notebook.status.workspace.storageFree, + false, + 1, + ), + })} + +
+
+ ); +}; + +export default Resources; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListColumns.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListColumns.component.tsx index fbb0b68c22e9..dbc7851f3edf 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListColumns.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListColumns.component.tsx @@ -1,6 +1,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { Cpu, + HelpCircle, MoreHorizontal, ShieldAlert, ShieldCheck, @@ -23,7 +24,12 @@ import { SortableHeader } from '@/components/ui/data-table'; import Link from '@/components/links/Link.component'; import { convertSecondsToTimeString } from '@/lib/durationHelper'; import NotebookStatusBadge from './NotebookStatusBadge.component'; -import { isDeletingNotebook, isRunningNotebook } from '@/lib/statusHelper'; +import { isDeletingNotebook, isRunningNotebook } from '@/lib/notebookHelper'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; interface NotebooksListColumnsProps { onStartClicked: (notebook: ai.notebook.Notebook) => void; @@ -120,10 +126,18 @@ export const getColumns = ({ { id: 'Operating time', accessorFn: (row) => convertSecondsToTimeString(row.status.duration), - header: ({ column }) => ( - - {t('tableHeaderDuration')} - + header: () => ( +
+ {t('tableHeaderDuration')} + + + + + +

{t('durationHelper')}

+
+
+
), }, { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/CliEquivalent.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/CliEquivalent.component.tsx index 03ce10313f1b..b9e8ccd930f4 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/CliEquivalent.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/CliEquivalent.component.tsx @@ -28,6 +28,7 @@ const CliEquivalent = ({ command, controller }: CliEquivalentModalProps) => { title={t('cliEquivalentModalDescription')} code={command.command} toastMessage={t('cliEquivalentModalToastMessage')} + size="max-h-[60vh] px-6" /> diff --git a/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx b/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx index 8164e37b43f1..fdd624492653 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx @@ -42,6 +42,15 @@ export default [ ...lazyRouteConfig(() => import('@/pages/notebooks/[notebookId]/Notebook.layout'), ), + children: [ + { + path: '', + id: 'notebook.dashboard', + ...lazyRouteConfig(() => + import('@/pages/notebooks/[notebookId]/dashboard/Dashboard.page'), + ), + }, + ], }, ], }, diff --git a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/Label.ts b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/Label.ts index d05f8a7a9f85..23ec8bb75de8 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/Label.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/Label.ts @@ -3,5 +3,5 @@ export interface Label { /** Name of the label to update/add */ name: string; /** Value of the label to update/add, is there is no value the label is deleted */ - value: string; + value?: string; }