diff --git a/packages/manager/apps/pci-ai-notebooks/package.json b/packages/manager/apps/pci-ai-notebooks/package.json index c7d7fd2d7371..390cee532a50 100644 --- a/packages/manager/apps/pci-ai-notebooks/package.json +++ b/packages/manager/apps/pci-ai-notebooks/package.json @@ -70,7 +70,6 @@ "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", 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 7bc5f777d904..807db18e1d29 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 @@ -3,7 +3,8 @@ "createNewNotebook": "Créer un notebook", "tableHeaderName": "Name", "tableHeaderLocation": "Région", - "tableHeaderEnvironment": "Environnement", + "tableHeaderFramework": "Framework", + "tableHeaderEditor": "Editeur", "tableHeaderResources": "Ressources", "tableHeaderPrivacy": "Confidentialité ", "networkSecureTitle": "Privé", 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 991232f9ec85..c3d4e908e1af 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 @@ -54,5 +54,8 @@ "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": "Commande CLI", - "cliEquivalentModalToastMessage": "Le code a été copié" + "cliEquivalentModalToastMessage": "Le code a été copié", + "errorCreatingNotebook": "Erreur", + "successCreatingNotebookTitle": "Succès", + "successCreatingNotebookDescription": "Votre notebook a été créé avec succès" } 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 bc8ec540304a..07513632c31e 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,7 +1,7 @@ { "dashboardTab": "Dashboard", "dataTab": "Données attachées", - "backupTab": "Backup", + "backupTab": "Backups", "logsTab": "Logs", "publicAccessLabel": "Public", "privateAccessLabel": "Privé", diff --git a/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/backups/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/backups/Messages_fr_FR.json new file mode 100644 index 000000000000..f9d12fdead49 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/public/translations/pci-ai-notebooks/notebooks/notebook/backups/Messages_fr_FR.json @@ -0,0 +1,17 @@ +{ + "breadcrumb": "Backups", + "title": "Sauvegardes", + "description": "Vous trouverez ci-dessous la liste de vos sauvegardes. Vous pouvez créer un nouveau notebook à partir du chacune d'entre elles", + "tableHeaderId": "Id", + "tableHeaderCreationDate": "Date de création", + "tableHeaderUpdateDate": "Date de modification", + "backupDropdownMenuLabel": "Action", + "tableActionFork": "Fork", + "forkBackupTitle": "Commander un notebook depuis un backup", + "forkBackupDescription": "Utiliser le backup {{id}} créé le {{date}} pour commander un nouveau notebook?", + "forkButtonCancel": "Annuler", + "forkBackupButtonConfirm": "Commander", + "forkToastErrorTitle": "Une erreur est survenue", + "forkToastSuccessTitle": "Succés", + "forkToastSuccessDescription": "votre notebook a été créé avec succès" +} 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 index fc7dcebe7821..e455f64ce9a9 100644 --- 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 @@ -7,6 +7,7 @@ "billingSupportTitle": "Support & Facturation", "configurationTitle": "Configuration", "cliTitle": "CLI", + "cliCodeTitle": "Vous pouvez créer le même notebook en utilisant ces lignes de commande dans votre ovhai CLI.", "powerTitleSection": "Power", "computeTitleSection": "Compute", "storageTitleSection": "Stockage", diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/route-modal/RouteModal.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/route-modal/RouteModal.tsx new file mode 100644 index 000000000000..15f4449b264f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/route-modal/RouteModal.tsx @@ -0,0 +1,61 @@ +import { useNavigate } from 'react-router-dom'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, +} from '../ui/dialog'; +import { Skeleton } from '../ui/skeleton'; + +interface RouteModalProps { + backUrl?: string; + isLoading?: boolean; + children: React.ReactNode | React.ReactNode[]; + onClose?: () => void; +} +const RouteModal = ({ + backUrl, + isLoading = false, + children, + onClose, +}: RouteModalProps) => { + const navigate = useNavigate(); + const onOpenChange = (open: boolean) => { + if (!open) { + if (onClose) { + onClose(); + return; + } + if (backUrl) navigate(backUrl); + } + }; + + return ( + + {isLoading ? ( + + + + + + + + + + + + + + + + + + ) : ( + children + )} + + ); +}; + +export default RouteModal; 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 index a71a1c699516..b3577ed56e1c 100644 --- 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 @@ -250,7 +250,8 @@ DataTable.Skeleton = function DataTableSkeleton({ {Array.from({ length: columns }).map((col, iCol) => ( ))} 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 index 01b8b6d4f716..7fec2f335e3e 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/components/ui/skeleton.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/components/ui/skeleton.tsx @@ -5,7 +5,7 @@ function Skeleton({ ...props }: React.HTMLAttributes) { return ( -
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 index 496bf59ee946..4930ea38f61d 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/configuration/polling.constants.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/polling.constants.ts @@ -2,6 +2,7 @@ export const POLLING = { NOTEBOOKS: 30_000, NOTEBOOK: 30_000, LOGS: 30_000, + BACKUPS: 30_000, }; export const USER_INACTIVITY_TIMEOUT = 5 * 60_000; // inactivity after 5 minutes 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 index e422f6a30b60..06784b3275c8 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/configuration/tracking.constants.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/configuration/tracking.constants.ts @@ -1,11 +1,2 @@ -const APP_TRACKING_PREFIX = 'PublicCloud::databases_analytics::databases'; +export const APP_TRACKING_PREFIX = 'PublicCloud::ai::notebooks'; 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/data/api/ai/notebook/backups/backups.api.ts b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/backups/backups.api.ts new file mode 100644 index 000000000000..4a8be2f412c8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/data/api/ai/notebook/backups/backups.api.ts @@ -0,0 +1,34 @@ +import { apiClient } from '@ovh-ux/manager-core-api'; +import * as ai from '@/types/cloud/project/ai'; +import { NotebookData } from '@/data/api'; + +export const getBackups = async ({ projectId, notebookId }: NotebookData) => + apiClient.v6 + .get(`/cloud/project/${projectId}/ai/notebook/${notebookId}/backup`) + .then((res) => res.data as ai.notebook.Backup[]); + +export interface BackupData extends NotebookData { + backupId: string; +} + +export const getBackup = async ({ + projectId, + notebookId, + backupId, +}: BackupData) => + apiClient.v6 + .get( + `/cloud/project/${projectId}/ai/notebook/${notebookId}/backup/${backupId}`, + ) + .then((res) => res.data as ai.notebook.Backup); + +export const forkBackup = async ({ + projectId, + notebookId, + backupId, +}: BackupData) => + apiClient.v6 + .post( + `/cloud/project/${projectId}/ai/notebook/${notebookId}/backup/${backupId}/fork`, + ) + .then((res) => res.data as ai.notebook.Notebook); diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useForkBackup.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useForkBackup.hook.tsx new file mode 100644 index 000000000000..f3c60ac0d527 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useForkBackup.hook.tsx @@ -0,0 +1,40 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; +import { AIError } from '@/data/api'; +import * as ai from '@/types/cloud/project/ai'; +import { + BackupData, + forkBackup, +} from '@/data/api/ai/notebook/backups/backups.api'; + +interface UseForkBackup { + onError: (cause: AIError) => void; + onSuccess: (notebook: ai.notebook.Notebook) => void; +} + +export function useForkBackup({ onError, onSuccess }: UseForkBackup) { + const queryClient = useQueryClient(); + const { projectId } = useParams(); + const mutation = useMutation({ + mutationFn: (forkInfo: BackupData) => { + return forkBackup(forkInfo); + }, + onError, + onSuccess: (data) => { + // invalidate notebooks list to avoid displaying + // old list + queryClient.invalidateQueries({ + queryKey: [projectId, 'ai/notebook'], + refetchType: 'none', + }); + onSuccess(data); + }, + }); + + return { + forkBackup: (forkInfo: BackupData) => { + return mutation.mutate(forkInfo); + }, + ...mutation, + }; +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackup.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackup.hook.tsx new file mode 100644 index 000000000000..c589803e8e8f --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackup.hook.tsx @@ -0,0 +1,26 @@ +import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query'; +import * as ai from '@/types/cloud/project/ai'; +import { useQueryImmediateRefetch } from '@/hooks/api/useImmediateRefetch'; +import { AIError } from '@/data/api'; +import { getBackup } from '@/data/api/ai/notebook/backups/backups.api'; + +export function useGetBackup( + projectId: string, + notebookId: string, + backupId: string, + options: Omit = {}, +) { + const queryKey = [ + projectId, + 'ai', + 'notebook', + notebookId, + 'backup', + backupId, + ]; + return useQueryImmediateRefetch({ + queryKey, + queryFn: () => getBackup({ projectId, notebookId, backupId }), + ...options, + }) as UseQueryResult; +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackups.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackups.hook.tsx new file mode 100644 index 000000000000..fcf6d07c9d16 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/api/ai/notebook/backups/useGetBackups.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 { AIError } from '@/data/api'; +import { getBackups } from '@/data/api/ai/notebook/backups/backups.api'; + +export function useGetBackups( + projectId: string, + notebookId: string, + options: Omit = {}, +) { + const queryKey = [projectId, 'ai', 'notebook', notebookId, 'backup']; + return useQueryImmediateRefetch({ + queryKey, + queryFn: () => getBackups({ projectId, notebookId }), + ...options, + }) as UseQueryResult; +} 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 index 05449699ca7d..d2a4348e2259 100644 --- 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 @@ -19,7 +19,7 @@ export function useAddNotebook({ onError, onSuccess }: AddNotebookProps) { }, onError, onSuccess: (data) => { - // invalidate services list to avoid displaying + // invalidate notebook list to avoid displaying // old list queryClient.invalidateQueries({ queryKey: [projectId, 'ai/notebook'], 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 index 0be47edbd121..0cc4a714f5e5 100644 --- 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 @@ -5,19 +5,27 @@ import { deleteNotebook } from '@/data/api/ai/notebook/notebook.api'; interface UseDeleteNotebook { onError: (cause: AIError) => void; - onSuccess: () => void; + onDeleteSuccess: () => void; } export function useDeleteNotebook({ onError, - onSuccess, + onDeleteSuccess, }: UseDeleteNotebook) { + const queryClient = useQueryClient(); + const { projectId } = useParams(); const mutation = useMutation({ mutationFn: (notebookInfo: NotebookData) => { return deleteNotebook(notebookInfo); }, onError, - onSuccess, + onSuccess: () => { + // Invalidate notebooks list query to get the latest data + queryClient.invalidateQueries({ + queryKey: [projectId, 'ai/notebook', { exact: true }], + }); + onDeleteSuccess(); + }, }); return { @@ -26,4 +34,4 @@ export function useDeleteNotebook({ }, ...mutation, }; -} \ No newline at end of file +} 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 index 82804cc240bb..e320a255e559 100644 --- 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 @@ -12,13 +12,13 @@ describe('useDeleteNotebooks', () => { it('should delete a Notebook', async () => { const projectId = 'projectId'; const notebookId = 'notebookId'; - const onSuccess = vi.fn(); + const onDeleteSuccess = vi.fn(); const onError = vi.fn(); vi.mocked(notebookApi.deleteNotebook).mockResolvedValue(undefined); const { result } = renderHook( - () => useDeleteNotebook({ onError, onSuccess }), + () => useDeleteNotebook({ onError, onDeleteSuccess }), { wrapper: QueryClientWrapper, }, 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 index 151a97d9c074..4b420f00422c 100644 --- 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 @@ -1,4 +1,4 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { useParams } from 'react-router-dom'; import * as ai from '@/types/cloud/project/ai'; @@ -11,22 +11,13 @@ interface GetCommandProps { } export function useGetCommand({ onError, onSuccess }: GetCommandProps) { - const queryClient = useQueryClient(); const { projectId } = useParams(); const mutation = useMutation({ mutationFn: (notebookInfo: ai.notebook.NotebookSpecInput) => { 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); - }, + onSuccess, }); return { 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 70b83ff31935..91405ccf75f6 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,7 +20,7 @@ export function useStartNotebook({ }, onError, onSuccess: () => { - // Invalidate service list query to get the latest data + // Invalidate notebooks list query to get the latest data queryClient.invalidateQueries({ queryKey: [projectId, 'ai/notebook'], }); 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 4e93859e7c60..af753c2d4cee 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,7 +17,7 @@ export function useStopNotebook({ onError, onStopSuccess }: UseStopNotebook) { }, onError, onSuccess: () => { - // Invalidate service list query to get the latest data + // Invalidate notebooks list query to get the latest data queryClient.invalidateQueries({ queryKey: [projectId, 'ai/notebook'], }); diff --git a/packages/manager/apps/pci-ai-notebooks/src/hooks/useTracking.ts b/packages/manager/apps/pci-ai-notebooks/src/hooks/useTracking.ts index 1f6da3c23344..2c3c9b3099e0 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/hooks/useTracking.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/hooks/useTracking.ts @@ -1,8 +1,12 @@ -import { useContext, useEffect } from 'react'; +import { useLocation, useMatches, useParams } from 'react-router-dom'; +import { useContext, useEffect, useRef } from 'react'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import usePciProject from './api/project/usePciProject.hook'; -import { PCI_LEVEL2 } from '@/configuration/tracking.constants'; -import { PlanCode } from '@/configuration/project'; +import { + APP_TRACKING_PREFIX, + PCI_LEVEL2, +} from '@/configuration/tracking.constants'; +import { PlanCode } from '@/types/cloud/Project'; // Set the project mode, needed to track discovery actions function useProjectModeTracking() { @@ -47,3 +51,38 @@ export function useTrackPage(pageTracking: string) { }); }, []); } + +export function useTrackPageAuto() { + useProjectModeTracking(); + const { shell } = useContext(ShellContext); + const { trackPage } = shell.tracking; + const matches = useMatches(); + const location = useLocation(); + const params = useParams(); + // Last match is the current route, we need it + // to get the tracking key associated with the route + const match = matches[matches.length - 1]; + const hasTrackedRef = useRef(false); + + useEffect(() => { + if (hasTrackedRef.current) return; + const prefix = APP_TRACKING_PREFIX; + const { id } = match; + const routerTrackingKey = (match?.handle as { tracking: string })?.tracking; + const suffix = + routerTrackingKey || id || location.pathname.split('/').pop(); + let injectedTrackingKey = `${prefix}::${suffix}`; + + // replace . by :: + injectedTrackingKey = injectedTrackingKey.replaceAll('.', '::'); + trackPage({ + name: injectedTrackingKey, + level2: PCI_LEVEL2, + }); + hasTrackedRef.current = true; + }, [location.pathname, params.serviceId]); + + useEffect(() => { + hasTrackedRef.current = false; + }, [location.pathname]); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts index 17fb43a6aab3..2dd3ff9768d5 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/lib/notebookHelper.ts @@ -8,6 +8,10 @@ export function isRunningNotebook(currentState: ai.notebook.NotebookStateEnum) { ); } +export function isStoppedNotebook(currentState: ai.notebook.NotebookStateEnum) { + return currentState === ai.notebook.NotebookStateEnum.STOPPED; +} + export function isDeletingNotebook( currentState: ai.notebook.NotebookStateEnum, ) { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/Root.layout.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/Root.layout.tsx index 04254744315e..2b1fadcb7c5a 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/Root.layout.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/Root.layout.tsx @@ -1,20 +1,23 @@ -import { Outlet, redirect, useLocation, useParams } from 'react-router-dom'; +import { + Outlet, + redirect, + useLocation, + useMatches, + useParams, +} from 'react-router-dom'; import { useRouting, useShell } from '@ovh-ux/manager-react-shell-client'; - import { useEffect } from 'react'; +import { defineCurrentPage } from '@ovh-ux/request-tagger'; import queryClient from '@/query.client'; - import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context'; import { getProject } from '@/data/api/project/project.api'; import Breadcrumb from '@/components/breadcrumb/Breadcrumb.component'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; - import { Toaster } from '@/components/ui/toaster'; import PageLayout from '@/components/page-layout/PageLayout.component'; -import Auth from './auth/auth.page'; import { UserActivityProvider } from '@/contexts/UserActivityContext'; import { USER_INACTIVITY_TIMEOUT } from '@/configuration/polling.constants'; -import { useGetAuthorization } from '@/hooks/api/ai/authorization/useGetAuthorization.hook'; +import { useTrackPageAuto } from '@/hooks/useTracking'; export function breadcrumb() { return ( @@ -31,6 +34,8 @@ interface NotebooksLayoutProps { // try to fetch the service data, redirect to service page if it fails export const Loader = async ({ params }: NotebooksLayoutProps) => { const { projectId } = params; + + // check if we have a correct projectId return queryClient .fetchQuery({ queryKey: ['projectId', projectId], @@ -47,6 +52,7 @@ function RoutingSynchronisation() { const location = useLocation(); const routing = useRouting(); const shell = useShell(); + const matches = useMatches(); useEffect(() => { routing.stopListenForHashChange(); @@ -56,6 +62,15 @@ function RoutingSynchronisation() { setLoading(false); routing.onHashChange(); }, [location]); + + useEffect(() => { + const match = matches.slice(-1); + // We cannot type properly useMatches cause it's not support type inference or passing specific type https://github.com/remix-run/react-router/discussions/10902 + defineCurrentPage(`app.pci-databases-analytics.${match[0].id}`); + }, [location]); + + useTrackPageAuto(); + return <>; } @@ -65,29 +80,12 @@ export function useNotebooksData() { } export default function Layout() { - const { projectId } = useParams(); - const authorizationQuery = useGetAuthorization(projectId); - if (authorizationQuery.isSuccess && authorizationQuery.data.authorized) { - return ( - - - - - - - - - ); - } return ( + - { - authorizationQuery.refetch(); - }} - /> + diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/Root.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/Root.page.tsx index ab25676cc6b8..3db813298c09 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/Root.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/Root.page.tsx @@ -1,4 +1,43 @@ +import { redirect } from 'react-router-dom'; +import queryClient from '@/query.client'; import Notebooks from './notebooks/Notebooks.page'; +import { getNotebooks } from '@/data/api/ai/notebook/notebook.api'; +import { getAuthorization } from '@/data/api/ai/authorization.api'; + +interface NotebooksProps { + params: { + projectId: string; + }; + request: Request; +} + +export const Loader = ({ params }: NotebooksProps) => { + // check if we have a correct category + const { projectId } = params; + return queryClient + .fetchQuery({ + queryKey: [projectId, 'auth'], + queryFn: () => getAuthorization({ projectId }), + }) + .then((auth) => { + if (!auth.authorized) { + return redirect(`/pci/projects/${projectId}/ai/notebooks/auth`); + } + return queryClient + .fetchQuery({ + queryKey: [projectId, 'ai/notebooks'], + queryFn: () => getNotebooks({ projectId }), + }) + .then((notebooks) => { + if (notebooks.length === 0) { + return redirect( + `/pci/projects/${projectId}/ai/notebooks/onboarding`, + ); + } + return null; + }); + }); +}; export default function Root() { return ; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/_components/AddSSHKey.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/_components/sshkey/AddSSHKey.modal.tsx similarity index 88% rename from packages/manager/apps/pci-ai-notebooks/src/pages/_components/AddSSHKey.component.tsx rename to packages/manager/apps/pci-ai-notebooks/src/pages/_components/sshkey/AddSSHKey.modal.tsx index da29e50d66e3..755f19670fea 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/_components/AddSSHKey.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/_components/sshkey/AddSSHKey.modal.tsx @@ -1,11 +1,10 @@ -import { useEffect } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import * as sshkey from '@/types/cloud/sshkey'; -import { ModalController } from '@/hooks/useModale'; import { UseAddSshKey, useAddSshKey, @@ -13,7 +12,6 @@ import { import { getAIApiErrorMessage } from '@/lib/apiHelper'; import { useToast } from '@/components/ui/use-toast'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -30,22 +28,20 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import RouteModal from '@/components/route-modal/RouteModal'; +import { useGetSshkey } from '@/hooks/api/sshkey/useGetSshkey.hook'; -interface AddSSHKeyProps { - configuredSshKeys: sshkey.SshKey[]; - controller: ModalController; - onSuccess?: (sshKey: sshkey.SshKey) => void; - onError?: (error: Error) => void; -} - -const AddSSHKey = ({ - configuredSshKeys, - controller, - onError, - onSuccess, -}: AddSSHKeyProps) => { +const AddSSHKey = () => { const { projectId } = useParams(); const { t } = useTranslation('pci-ai-notebooks/components/configuration'); + const navigate = useNavigate(); + + const sshKeyQuery = useGetSshkey(projectId); + + const configuredSshKeys: sshkey.SshKey[] = useMemo(() => { + return sshKeyQuery.data; + }, [sshKeyQuery.isSuccess]); + const sshKeySchema = z.object({ name: z .string() @@ -68,10 +64,6 @@ const AddSSHKey = ({ resolver: zodResolver(sshKeySchema), }); - useEffect(() => { - if (!controller.open) form.reset(); - }, [controller.open]); - const toast = useToast(); const sshKeyMutationConfig: UseAddSshKey = { @@ -81,9 +73,6 @@ const AddSSHKey = ({ variant: 'destructive', description: getAIApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onAddKeySuccess(sshKey) { form.reset(); @@ -93,9 +82,7 @@ const AddSSHKey = ({ name: sshKey.name, }), }); - if (onSuccess) { - onSuccess(sshKey); - } + navigate('../'); }, }; @@ -110,7 +97,7 @@ const AddSSHKey = ({ }); return ( - + @@ -179,7 +166,7 @@ const AddSSHKey = ({ - + ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.spec.tsx deleted file mode 100644 index 9864a917f540..000000000000 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.spec.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; - -import Layout from '@/pages/Root.layout'; -import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; -import * as authAPI from '@/data/api/ai/authorization.api'; -import * as ai from '@/types/cloud/project/ai'; -import { mockedAuthorization } from '@/__tests__/helpers/mocks/authorization'; - -describe('Dashboard Layout', () => { - beforeEach(() => { - vi.restoreAllMocks(); - - vi.mock('react-router-dom', async () => { - const mod = await vi.importActual('react-router-dom'); - return { - ...mod, - useParams: () => ({ - projectId: 'projectId', - }), - }; - }); - - vi.mock('@/data/api/project/project.api', () => { - return { - getProject: vi.fn(() => ({ - project_id: '123456', - projectName: 'projectName', - description: 'description', - })), - }; - }); - vi.mock('@/data/api/ai/authorization.api', () => ({ - getAuthorization: vi.fn(() => mockedAuthorization), - postAuthorization: vi.fn(() => mockedAuthorization), - })); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - - it('renders the Layout component and display auth page', async () => { - const noAut: ai.AuthorizationStatus = { - authorized: false, - }; - vi.mocked(authAPI.getAuthorization).mockResolvedValueOnce(noAut); - render(, { - wrapper: RouterWithQueryClientWrapper, - }); - await waitFor(() => { - expect(screen.getByTestId('activate-project-button')).toBeInTheDocument(); - expect(screen.getByTestId('auth-page-container')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('activate-project-button')); - }); - await waitFor(() => { - expect(authAPI.postAuthorization).toHaveBeenCalled(); - expect(authAPI.getAuthorization).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx index 2a1beb6af327..c33ce3920c2c 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx @@ -1,6 +1,6 @@ import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useParams, Outlet } from 'react-router-dom'; import Link from '@/components/links/Link.component'; import { Button } from '@/components/ui/button'; import { POLLING } from '@/configuration/polling.constants'; @@ -8,7 +8,6 @@ import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { useGetNotebooks } from '@/hooks/api/ai/notebook/useGetNotebooks.hook'; import Guides from '@/components/guides/Guides.component'; import NotebooksList from './_components/NotebooksListTable.component'; -import Onboarding from './_components/Onboarding.component'; const Notebooks = () => { const { t } = useTranslation('pci-ai-notebooks/notebooks'); @@ -19,9 +18,6 @@ const Notebooks = () => { }); if (notebooksQuery.isLoading) return ; - if (notebooksQuery.isSuccess && notebooksQuery.data.length === 0) { - return ; - } return ( <>
{ {t('createNewNotebook')} - + + ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.context.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.context.tsx index 301426096e49..4decbdafd87e 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.context.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/Notebook.context.tsx @@ -1,6 +1,7 @@ import { UseQueryResult } from '@tanstack/react-query'; -import { useOutletContext, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import * as ai from '@/types/cloud/project/ai'; +import { useGetNotebook } from '@/hooks/api/ai/notebook/useGetNotebook.hook'; // Share data with the child routes export type NotebookLayoutContext = { @@ -8,10 +9,7 @@ export type NotebookLayoutContext = { notebookQuery: UseQueryResult; }; export const useNotebookData = () => { - const { projectId } = useParams(); - const { - notebook, - notebookQuery, - } = useOutletContext() as NotebookLayoutContext; - return { projectId, notebook, notebookQuery }; + const { projectId, notebookId } = useParams(); + const notebookQuery = useGetNotebook(projectId, notebookId); + return { projectId, notebook: notebookQuery.data, notebookQuery }; }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/DeleteNotebook.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/DeleteNotebook.component.tsx index 90ff70416e33..915c37247c4d 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/DeleteNotebook.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/_components/DeleteNotebook.component.tsx @@ -2,7 +2,6 @@ import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -11,25 +10,22 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import * as ai from '@/types/cloud/project/ai'; -import { ModalController } from '@/hooks/useModale'; import { useDeleteNotebook } from '@/hooks/api/ai/notebook/useDeleteNotebook.hook'; import { getAIApiErrorMessage } from '@/lib/apiHelper'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteNotebookModalProps { +interface DeleteNotebookProps { notebook: ai.notebook.Notebook; - controller: ModalController; onSuccess?: (notebook: ai.notebook.Notebook) => void; onError?: (notebook: Error) => void; } const DeleteNotebook = ({ notebook, - controller, onError, onSuccess, -}: DeleteNotebookModalProps) => { +}: DeleteNotebookProps) => { const { projectId } = useParams(); - const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); const toast = useToast(); @@ -44,7 +40,7 @@ const DeleteNotebook = ({ onError(err); } }, - onSuccess: () => { + onDeleteSuccess: () => { toast.toast({ title: t('notebookToastSuccessTitle'), description: t('deleteNotebookToastSuccessDescription', { @@ -64,7 +60,7 @@ const DeleteNotebook = ({ }); }; return ( - + @@ -96,7 +92,7 @@ const DeleteNotebook = ({ - + ); }; 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 index 6f550b504984..43f82b957698 100644 --- 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 @@ -7,16 +7,16 @@ import { PlayIcon, Square, } from 'lucide-react'; +import { useState } from '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'; import A from '@/components/links/A.component'; +import StartNotebook from './StartNotebook.component'; +import StopNotebook from './StopNotebook.component'; export const NotebookHeader = ({ notebook, @@ -24,107 +24,105 @@ export const NotebookHeader = ({ notebook: ai.notebook.Notebook; }) => { const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); - const { t: tRegions } = useTranslation('regions'); - const startModale = useModale('start'); - const stopModale = useModale('stop'); - + const [isStartOpen, setIsStartOpen] = useState(false); + const [isStopOpen, setIsStopOpen] = useState(false); return ( -
-
- -
-
-
-

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

-
- {isRunningNotebook(notebook.status.state) || - isDeletingNotebook(notebook.status.state) ? ( - - ) : ( - - )} -
+ <> +
+
+
-
- +
+
+

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

+
+ {isRunningNotebook(notebook.status.state) || + isDeletingNotebook(notebook.status.state) ? ( + + ) : ( + + )} +
+
+
+ - - - {notebook.spec.env.frameworkId} - - - {notebook.spec.env.frameworkVersion} - - - {tRegions(`region_${notebook.spec.region}`)} - - - {notebook.spec.unsecureHttp ? ( -
- {t('publicAccessLabel')} - -
- ) : ( -
- {t('privateAccessLabel')} - -
- )} -
+ +
+ {notebook.spec.env.editorId} + +
+
+ + + {notebook.spec.env.frameworkId} + + + {notebook.spec.env.frameworkVersion} + + + {tRegions(`region_${notebook.spec.region}`)} + + + {notebook.spec.unsecureHttp ? ( +
+ {t('publicAccessLabel')} + +
+ ) : ( +
+ {t('privateAccessLabel')} + +
+ )} +
+
- { - startModale.close(); - }} - /> - { - stopModale.close(); - }} - /> -
+ {isStartOpen && ( + setIsStartOpen(false)} + onClose={() => setIsStartOpen(false)} + /> + )} + {isStopOpen && ( + setIsStopOpen(false)} + onClose={() => setIsStopOpen(false)} + /> + )} + ); }; 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 index bf977ff641a6..bdaffedd5b22 100644 --- 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 @@ -1,6 +1,10 @@ import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import TabsMenu from '@/components/tabs-menu/TabsMenu.component'; import * as ai from '@/types/cloud/project/ai'; +import { useGetBackups } from '@/hooks/api/ai/notebook/backups/useGetBackups.hook'; +import { POLLING } from '@/configuration/polling.constants'; +import { useUserActivityContext } from '@/contexts/UserActivityContext'; interface NotebookTabsProps { notebook: ai.notebook.Notebook; @@ -8,14 +12,31 @@ interface NotebookTabsProps { const NotebookTabs = ({ notebook }: NotebookTabsProps) => { const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); + const { projectId } = useParams(); + const { isUserActive } = useUserActivityContext(); + const { data: backups } = useGetBackups(projectId, notebook.id, { + refetchInterval: isUserActive && POLLING.BACKUPS, + }); + + const attachedData: ai.volume.Volume[] = notebook.spec.volumes.filter( + (vol: ai.volume.Volume) => vol.volumeSource.dataStore.internal === false, + ); const tabs = [ { href: '', label: t('dashboardTab'), end: true }, - { href: 'attach-data', label: t('dataTab') }, - { href: 'backup', label: t('backupTab') }, + attachedData.length > 0 && { + href: 'attach-data', + label: t('dataTab'), + count: attachedData.length, + }, + backups && + backups?.length > 0 && { + href: 'backups', + label: t('backupTab'), + count: backups?.length, + }, { href: 'logs', label: t('logsTab') }, ].filter((tab) => tab); - return ; }; 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 99051db0e781..2e8575351927 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 @@ -2,7 +2,6 @@ import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -11,23 +10,23 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import * as ai from '@/types/cloud/project/ai'; -import { ModalController } from '@/hooks/useModale'; import { getAIApiErrorMessage } from '@/lib/apiHelper'; import { useStartNotebook } from '@/hooks/api/ai/notebook/useStartNotebook.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface StartNotebookModalProps { +interface StartNotebookProps { notebook: ai.notebook.Notebook; - controller: ModalController; onSuccess?: () => void; onError?: (notebook: Error) => void; + onClose?: () => void; } const StartNotebook = ({ notebook, - controller, onError, onSuccess, -}: StartNotebookModalProps) => { + onClose, +}: StartNotebookProps) => { const { projectId } = useParams(); const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); @@ -63,8 +62,9 @@ const StartNotebook = ({ notebookId: notebook.id, }); }; + return ( - + @@ -96,7 +96,7 @@ const StartNotebook = ({ - + ); }; 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 e92f5bdaa96c..46ba3e953794 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 @@ -2,7 +2,6 @@ import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -11,23 +10,23 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import * as ai from '@/types/cloud/project/ai'; -import { ModalController } from '@/hooks/useModale'; import { getAIApiErrorMessage } from '@/lib/apiHelper'; import { useStopNotebook } from '@/hooks/api/ai/notebook/useStopNotebook.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface StopNotebookModalProps { +interface StopNotebookProps { notebook: ai.notebook.Notebook; - controller: ModalController; onSuccess?: () => void; onError?: (notebook: Error) => void; + onClose?: () => void; } const StopNotebook = ({ notebook, - controller, onError, onSuccess, -}: StopNotebookModalProps) => { + onClose, +}: StopNotebookProps) => { const { projectId } = useParams(); const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook'); @@ -64,7 +63,7 @@ const StopNotebook = ({ }); }; return ( - + @@ -115,7 +114,7 @@ const StopNotebook = ({ - + ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/AttachedData.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/AttachedData.page.tsx index a4ee0bf25915..b52c26587c65 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/AttachedData.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/AttachedData.page.tsx @@ -1,12 +1,11 @@ import { useTranslation } from 'react-i18next'; import { ArrowUpRightFromSquare, Info } from 'lucide-react'; +import { Outlet, useNavigate } from 'react-router-dom'; import { useNotebookData } from '../Notebook.context'; import A from '@/components/links/A.component'; import { Button } from '@/components/ui/button'; import VolumesList from './_components/VolumesListTable.component'; import * as ai from '@/types/cloud/project/ai'; -import DataSync from './_components/DataSync.component'; -import { useModale } from '@/hooks/useModale'; import { isDataSyncNotebook } from '@/lib/notebookHelper'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; @@ -20,12 +19,12 @@ export function breadcrumb() { } const AttachedData = () => { - const { notebook, notebookQuery } = useNotebookData(); + const { notebook } = useNotebookData(); + const navigate = useNavigate(); const { t } = useTranslation( 'pci-ai-notebooks/notebooks/notebook/attached-data', ); const volumeInfoLink = 'https://docs.ovh.com/gb/en/publiccloud/ai/data/'; - const dataSyncModale = useModale('datasyncglobal'); return ( <>

{t('attachedDataTitle')}

@@ -48,7 +47,7 @@ const AttachedData = () => { size="sm" type="button" className="text-base" - onClick={() => dataSyncModale.open()} + onClick={() => navigate('./data-sync')} disabled={!isDataSyncNotebook(notebook.status.state)} > {t('synchroniseDataButton')} @@ -56,16 +55,11 @@ const AttachedData = () => {
vol.dataStore.internal === false, + (vol: ai.volume.Volume) => + vol.volumeSource.dataStore.internal === false, )} /> - { - dataSyncModale.close(); - notebookQuery.refetch(); - }} - /> + ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListColumns.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListColumns.component.tsx index e5fd8458d33b..e86e26afa664 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListColumns.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListColumns.component.tsx @@ -28,14 +28,14 @@ export const getColumns = ({ onDataSyncClicked }: VolumesListColumnsProps) => { const columns: ColumnDef[] = [ { id: 'Alias', - accessorFn: (row) => row.dataStore.alias, + accessorFn: (row) => row.volumeSource.dataStore.alias, header: ({ column }) => ( {t('tableHeaderAlias')} ), }, { id: 'Container', - accessorFn: (row) => row.dataStore.container, + accessorFn: (row) => row.volumeSource.dataStore.container, header: ({ column }) => ( {t('tableHeaderContainer')} diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListTable.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListTable.component.tsx index e122509b6499..dba927b7d149 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListTable.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/VolumesListTable.component.tsx @@ -1,45 +1,28 @@ import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import { useModale } from '@/hooks/useModale'; +import { useNavigate } from 'react-router-dom'; import * as ai from '@/types/cloud/project/ai'; import { getColumns } from './VolumesListColumns.component'; import { DataTable } from '@/components/ui/data-table'; import { Skeleton } from '@/components/ui/skeleton'; -import DataSync from './DataSync.component'; +import { useNotebookData } from '../../Notebook.context'; interface VolumesListProps { volumes: ai.volume.Volume[]; } export default function VolumesList({ volumes }: Readonly) { - const dataSyncModale = useModale('datasync'); - - const dataSyncVolume: ai.volume.Volume = useMemo( - () => - volumes.find((vol) => vol.dataStore.container === dataSyncModale.value), - [dataSyncModale.value, volumes], - ); - + const { notebook } = useNotebookData(); + const navigate = useNavigate(); const columns: ColumnDef[] = getColumns({ onDataSyncClicked: (volume: ai.volume.Volume) => { - dataSyncModale.open(volume.dataStore.container); + const volumeId = notebook.status.volumes.find( + (vol) => vol.mountPath === volume.mountPath, + ).id; + navigate(`./data-sync/${volumeId}`); }, }); - return ( - <> - - {dataSyncVolume && ( - { - dataSyncModale.close(); - }} - /> - )} - - ); + return ; } VolumesList.Skeleton = function VolumesListSkeleton() { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/formDataSync/useDataSyncForm.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/formDataSync/useDataSyncForm.hook.tsx deleted file mode 100644 index 229281b266e0..000000000000 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/formDataSync/useDataSyncForm.hook.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import * as ai from '@/types/cloud/project/ai'; - -export const useDataSyncForm = () => { - const dataSyncTypeRules = z.nativeEnum(ai.volume.DataSyncEnum); - - const schema = z.object({ - type: dataSyncTypeRules, - }); - - type ValidationSchema = z.infer; - - const defaultValues: ValidationSchema = { - type: ai.volume.DataSyncEnum.pull, - }; - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues, - }); - - return { form, schema }; -}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/DataSync.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/dataSync/DataSync.modal.tsx similarity index 73% rename from packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/DataSync.component.tsx rename to packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/dataSync/DataSync.modal.tsx index 964d9e4ab123..769afcbc18c4 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/_components/DataSync.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/attached-data/dataSync/DataSync.modal.tsx @@ -1,8 +1,12 @@ import { useTranslation } from 'react-i18next'; -import { HelpCircle, Info } from 'lucide-react'; +import { HelpCircle } from 'lucide-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -11,9 +15,7 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import * as ai from '@/types/cloud/project/ai'; -import { ModalController } from '@/hooks/useModale'; import { getAIApiErrorMessage } from '@/lib/apiHelper'; -import { useDataSyncForm } from './formDataSync/useDataSyncForm.hook'; import { useDataSync } from '@/hooks/api/ai/notebook/datasync/useDataSync.hook'; import { useNotebookData } from '../../Notebook.context'; import { @@ -36,28 +38,26 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DataSyncModalProps { - volume?: ai.volume.Volume; - controller: ModalController; - onSuccess?: () => void; - onError?: (service: Error) => void; -} - -const DataSync = ({ - volume, - controller, - onError, - onSuccess, -}: DataSyncModalProps) => { +const DataSync = () => { + const { volumeId } = useParams(); + const [volume, setVolume] = useState(); const { notebook, projectId } = useNotebookData(); const { t } = useTranslation( 'pci-ai-notebooks/notebooks/notebook/attached-data', ); + const navigate = useNavigate(); const toast = useToast(); - const { form } = useDataSyncForm(); + useEffect(() => { + if (!volumeId) return; + const volumeToSync: ai.volume.VolumeStatus = notebook.status.volumes.find( + (vol: ai.volume.VolumeStatus) => vol.id === volumeId, + ); + if (volumeId && !volumeToSync) navigate('../'); + setVolume(volumeToSync); + }, [volumeId]); const { dataSync, isPending } = useDataSync({ onError: (err) => { @@ -66,30 +66,42 @@ const DataSync = ({ variant: 'destructive', description: getAIApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { const toastdesc: string = volume ? t('dataSyncMountPathToastSuccessDescription', { name: volume.mountPath, + interpolation: { escapeValue: false }, }) : t('dataSyncGlobalToastSuccessDescription'); toast.toast({ title: t('dataSyncToastSuccessTitle'), description: toastdesc, }); - if (onSuccess) { - onSuccess(); - } + navigate('../'); }, }); + const dataSyncTypeRules = z.nativeEnum(ai.volume.DataSyncEnum); + + const schema = z.object({ + type: dataSyncTypeRules, + }); + + type ValidationSchema = z.infer; + + const defaultValues: ValidationSchema = { + type: ai.volume.DataSyncEnum.pull, + }; + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues, + }); + const onSubmit = form.handleSubmit((formValues) => { const dataSyncFormValues: ai.volume.DataSyncSpec = { direction: formValues.type, - manual: true, }; if (volume) dataSyncFormValues.volume = notebook.status.volumes.find( @@ -104,7 +116,7 @@ const DataSync = ({ }); return ( - + @@ -148,29 +160,21 @@ const DataSync = ({ )} - - -
- - {volume ? ( -

- {t('dataSyncMountPathAlertDescription', { - name: volume.mountPath, - interpolation: { escapeValue: false }, - })} -

- ) : ( -

{t('dataSyncGlobalAlertDescription')}

- )} -
-
-
- - + )} /> + {volume ? ( +

+ {t('dataSyncMountPathAlertDescription', { + name: volume.mountPath, + interpolation: { escapeValue: false }, + })} +

+ ) : ( +

{t('dataSyncGlobalAlertDescription')}

+ )}
+ ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/Backups.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/Backups.page.tsx new file mode 100644 index 000000000000..219f8e046dea --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/Backups.page.tsx @@ -0,0 +1,38 @@ +import { useTranslation } from 'react-i18next'; +import { Outlet } from 'react-router-dom'; +import { useNotebookData } from '../Notebook.context'; +import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; +import { useGetBackups } from '@/hooks/api/ai/notebook/backups/useGetBackups.hook'; +import { useUserActivityContext } from '@/contexts/UserActivityContext'; +import { POLLING } from '@/configuration/polling.constants'; +import BackupsList from './_components/BackupsListTable.component'; + +export function breadcrumb() { + return ( + + ); +} + +const Backups = () => { + const { projectId, notebook } = useNotebookData(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/backups'); + const { isUserActive } = useUserActivityContext(); + + const backupsQuery = useGetBackups(projectId, notebook.id, { + refetchInterval: isUserActive && POLLING.BACKUPS, + }); + + return ( + <> +

{t('title')}

+

{t('description')}

+ {backupsQuery.isSuccess && } + + + ); +}; + +export default Backups; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx new file mode 100644 index 000000000000..4cd7e421b417 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx @@ -0,0 +1,85 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { MoreHorizontal } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { SortableHeader } from '@/components/ui/data-table'; +import * as ai from '@/types/cloud/project/ai'; + +interface BackupsListColumnsProps { + onForkClicked: (backup: ai.notebook.Backup) => void; +} + +export const getColumns = ({ onForkClicked }: BackupsListColumnsProps) => { + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/backups'); + const columns: ColumnDef[] = [ + { + id: 'Id', + accessorFn: (row) => row.id, + header: ({ column }) => ( + {t('tableHeaderId')} + ), + }, + { + id: 'CreationDate', + accessorFn: (row) => row.createdAt, + header: ({ column }) => ( + + {t('tableHeaderCreationDate')} + + ), + }, + { + id: 'UpdateDate', + accessorFn: (row) => row.updatedAt, + header: ({ column }) => ( + + {t('tableHeaderUpdateDate')} + + ), + }, + { + id: 'actions', + cell: ({ row }) => { + return ( + + + + + + + {t('backupDropdownMenuLabel')} + + { + onForkClicked(row.original); + }} + > + {t('tableActionFork')} + + + + ); + }, + }, + ]; + return columns; +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx new file mode 100644 index 000000000000..71f77b561cca --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx @@ -0,0 +1,40 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { useNavigate } from 'react-router-dom'; +import * as ai from '@/types/cloud/project/ai'; +import { getColumns } from './BackupsListColumns.component'; +import { DataTable } from '@/components/ui/data-table'; +import { Skeleton } from '@/components/ui/skeleton'; + +interface BackupsListProps { + backups: ai.notebook.Backup[]; +} + +export default function BackupsList({ backups }: Readonly) { + const navigate = useNavigate(); + + const columns: ColumnDef[] = getColumns({ + onForkClicked: (backup: ai.notebook.Backup) => { + navigate(`./fork/${backup.id}`); + }, + }); + + return ; +} + +BackupsList.Skeleton = function BackupsListSkeleton() { + return ( + <> +
+ +
+ + +
+
+ + + ); +}; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/fork/Fork.modal.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/fork/Fork.modal.tsx new file mode 100644 index 000000000000..cba3d758e01b --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/fork/Fork.modal.tsx @@ -0,0 +1,97 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useToast } from '@/components/ui/use-toast'; +import { useForkBackup } from '@/hooks/api/ai/notebook/backups/useForkBackup.hook'; +import { getAIApiErrorMessage } from '@/lib/apiHelper'; +import * as ai from '@/types/cloud/project/ai'; +import { Button } from '@/components/ui/button'; +import RouteModal from '@/components/route-modal/RouteModal'; +import { useGetBackup } from '@/hooks/api/ai/notebook/backups/useGetBackup.hook'; +import { useNotebookData } from '../../Notebook.context'; + +const Fork = () => { + const { notebook, projectId } = useNotebookData(); + const { backupId } = useParams(); + const navigate = useNavigate(); + const { t } = useTranslation('pci-ai-notebooks/notebooks/notebook/backups'); + const backupQuery = useGetBackup(projectId, notebook.id, backupId); + const toast = useToast(); + + const backup: ai.notebook.Backup = useMemo(() => { + return backupQuery.data; + }, [backupId, backupQuery.isSuccess]); + + const { forkBackup, isPending } = useForkBackup({ + onError: (err) => { + toast.toast({ + title: t('forkToastErrorTitle'), + variant: 'destructive', + description: getAIApiErrorMessage(err), + }); + }, + onSuccess: (newNotebook) => { + toast.toast({ + title: t('forkToastSuccessTitle'), + description: t('forkToastSuccessDescription'), + }); + navigate(`../../../${newNotebook.id}`); + }, + }); + + const handleFork = () => { + forkBackup({ + projectId, + notebookId: notebook.id, + backupId: backup.id, + }); + }; + + return ( + + + + + {t('forkBackupTitle')} + + + {backup && ( +

+ {t('forkBackupDescription', { + id: backup.id, + date: backup.createdAt, + })} +

+ )} + + + + + + +
+
+ ); +}; + +export default Fork; 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 index 6de51acee7a8..1ab4aa7727ad 100644 --- 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 @@ -10,6 +10,7 @@ import { UserCheck, } from 'lucide-react'; import { useEffect, useState } from 'react'; +import { Outlet } from 'react-router-dom'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { useNotebookData } from '../Notebook.context'; import Resources from './_components/Resources.component'; @@ -45,7 +46,7 @@ const Dashboard = () => { useEffect(() => { const filteredVolume: ai.volume.Volume[] = notebook.spec.volumes.filter( - (vol) => vol.dataStore.internal === false, + (vol) => vol.volumeSource.dataStore.internal === false, ); getCommand({ ...notebook.spec, volumes: filteredVolume }); }, [notebook]); @@ -146,14 +147,12 @@ const Dashboard = () => { {command && ( - + )}
+ ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AccessLink.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AccessLink.component.tsx index d9641615aa98..e7cfac2dee26 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AccessLink.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/_components/AccessLink.component.tsx @@ -6,9 +6,8 @@ import { import { useTranslation } from 'react-i18next'; import { useNotebookData } from '../../Notebook.context'; import { Button } from '@/components/ui/button'; - +import * as ai from '@/types/cloud/project/ai'; import A from '@/components/links/A.component'; -import { isRunningNotebook } from '@/lib/notebookHelper'; const AccessLink = () => { const { notebook } = useNotebookData(); @@ -24,7 +23,9 @@ const AccessLink = () => { type="button" variant="default" className="w-full" - disabled={!isRunningNotebook(notebook.status.state)} + disabled={ + notebook.status.state !== ai.notebook.NotebookStateEnum.RUNNING + } >
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 index 3be287ba8dba..ae21f53f1e81 100644 --- 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 @@ -4,21 +4,13 @@ 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'; +import { isStoppedNotebook } from '@/lib/notebookHelper'; const Configurations = () => { - const { notebook, projectId } = useNotebookData(); + const { notebook } = 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 (
@@ -46,20 +38,11 @@ const Configurations = () => { data-testid="service-confi-delete-button" variant="destructive" className="w-full bg-background border-2 hover:bg-destructive/10 font-semibold border-destructive text-destructive mt-4" - onClick={() => deleteModale.open()} - disabled={isRunningNotebook(notebook.status.state)} + onClick={() => navigate('./delete')} + disabled={!isStoppedNotebook(notebook.status.state)} > {t('deleteNotebookButton')} - { - navigate(`../../../`); - deleteModale.close(); - getNotebooksQuery.refetch(); - }} - />
); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/delete/Delete.modal.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/delete/Delete.modal.tsx new file mode 100644 index 000000000000..406b97e2b709 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/dashboard/delete/Delete.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetNotebook } from '@/hooks/api/ai/notebook/useGetNotebook.hook'; +import DeleteNotebook from '../../_components/DeleteNotebook.component'; + +const DeleteNotebookModal = () => { + const { projectId, notebookId } = useParams(); + const navigate = useNavigate(); + const notebookQuery = useGetNotebook(projectId, notebookId); + return ( + navigate('../..')} + notebook={notebookQuery.data} + /> + ); +}; + +export default DeleteNotebookModal; 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 48bfa350dd48..d758528926e3 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,5 +1,12 @@ import { ColumnDef } from '@tanstack/react-table'; -import { Cpu, Globe, LockKeyhole, MoreHorizontal, Zap } from 'lucide-react'; +import { + ArrowUpRightFromSquare, + Cpu, + Globe, + LockKeyhole, + MoreHorizontal, + Zap, +} from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import * as ai from '@/types/cloud/project/ai'; @@ -17,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/notebookHelper'; +import { + isDeletingNotebook, + isRunningNotebook, + isStoppedNotebook, +} from '@/lib/notebookHelper'; +import A from '@/components/links/A.component'; interface NotebooksListColumnsProps { onStartClicked: (notebook: ai.notebook.Notebook) => void; @@ -75,14 +87,46 @@ export const getColumns = ({ ), }, { - id: 'Environment', - accessorFn: (row) => - `${row.spec.env.frameworkId} - ${row.spec.env.frameworkVersion}`, + id: 'Framework', + accessorFn: (row) => row.spec.env.frameworkId, + header: ({ column }) => ( + + {t('tableHeaderFramework')} + + ), + cell: ({ row }) => ( + {row.original.spec.env.frameworkId} + ), + }, + { + id: 'Editor', + accessorFn: (row) => row.spec.env.editorId, header: ({ column }) => ( - {t('tableHeaderEnvironment')} + {t('tableHeaderEditor')} ), + cell: ({ row }) => ( +
+ ), }, { id: 'Resources', @@ -226,7 +270,7 @@ export const getColumns = ({ { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListTable.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListTable.component.tsx index 46c98d08c738..e67d8cb7ca92 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListTable.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/NotebooksListTable.component.tsx @@ -1,93 +1,30 @@ import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { DataTable } from '@/components/ui/data-table'; import { Skeleton } from '@/components/ui/skeleton'; import * as ai from '@/types/cloud/project/ai'; import { getColumns } from './NotebooksListColumns.component'; -import { useModale } from '@/hooks/useModale'; -import DeleteNotebook from '../[notebookId]/_components/DeleteNotebook.component'; -import StartNotebook from '../[notebookId]/_components/StartNotebook.component'; -import StopNotebook from '../[notebookId]/_components/StopNotebook.component'; interface NotebooksListProps { notebooks: ai.notebook.Notebook[]; - refetchFn: () => void; } -export default function NotebooksList({ - notebooks, - refetchFn, -}: NotebooksListProps) { - const startModale = useModale('start'); - const stopModale = useModale('stop'); - const deleteModale = useModale('delete'); - - const startingNotebook = useMemo( - () => notebooks.find((s) => s.id === startModale.value), - [startModale.value, notebooks], - ); - const stoppingNotebook = useMemo( - () => notebooks.find((s) => s.id === stopModale.value), - [stopModale.value, notebooks], - ); - - const deletingNotebook = useMemo( - () => notebooks.find((s) => s.id === deleteModale.value), - [deleteModale.value, notebooks], - ); +export default function NotebooksList({ notebooks }: NotebooksListProps) { + const navigate = useNavigate(); const columns: ColumnDef[] = getColumns({ onStartClicked: (notebook: ai.notebook.Notebook) => { - startModale.open(notebook.id); + navigate(`./start/${notebook.id}`); }, onStopClicked: (notebook: ai.notebook.Notebook) => { - stopModale.open(notebook.id); + navigate(`./stop/${notebook.id}`); }, onDeleteClicked: (notebook: ai.notebook.Notebook) => { - deleteModale.open(notebook.id); + navigate(`./delete/${notebook.id}`); }, }); - return ( - <> - - {deletingNotebook && ( - { - deleteModale.close(); - refetchFn(); - }} - /> - )} - {startingNotebook && ( - { - startModale.close(); - refetchFn(); - }} - /> - )} - {stoppingNotebook && ( - { - stopModale.close(); - refetchFn(); - }} - /> - )} - - ); + return ; } NotebooksList.Skeleton = function NotebooksListSkeleton() { @@ -97,10 +34,10 @@ NotebooksList.Skeleton = function NotebooksListSkeleton() { data-testid="notebook-list-table-skeleton" className="flex justify-between w-100 mb-2 items-end" > - +
- - + +
diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/auth/Auth.page.tsx similarity index 93% rename from packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.page.tsx rename to packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/auth/Auth.page.tsx index 5d3cec440579..b26d2fbf8b81 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/auth/auth.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/auth/Auth.page.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { AlertCircle, ArrowRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; @@ -12,13 +12,10 @@ import OvhLink from '@/components/links/OvhLink.component'; import usePciProject from '@/hooks/api/project/usePciProject.hook'; import { PlanCode } from '@/configuration/project'; -interface AuthProps { - onSuccess?: () => void; -} - -export default function Auth({ onSuccess }: AuthProps) { +export default function Auth() { const { t } = useTranslation('pci-ai-notebooks/auth'); const toast = useToast(); + const navigate = useNavigate(); const { projectId } = useParams(); const projectData = usePciProject(); @@ -38,9 +35,7 @@ export default function Auth({ onSuccess }: AuthProps) { title: t('formActiveUserToastSuccessTitle'), description: t(`formActiveUserToastSuccessDescription`), }); - if (onSuccess) { - onSuccess(); - } + navigate('../'); }, }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx index 4b150afb7454..9f39d5b38592 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { Outlet, useParams } from 'react-router-dom'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; import Guides from '@/components/guides/Guides.component'; import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; @@ -62,6 +62,7 @@ const Notebook = () => { suggestions={suggestionsQuery.data} /> )} + ); }; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx index 9d2445e3582e..b0b754ef34a3 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx @@ -49,11 +49,10 @@ import { useAddNotebook } from '@/hooks/api/ai/notebook/useAddNotebook.hook'; import { useToast } from '@/components/ui/use-toast'; import { getAIApiErrorMessage } from '@/lib/apiHelper'; import ErrorList from '@/components/order/error-list/ErrorList.component'; -import { OrderSshKey, PrivacyEnum, Suggestions } from '@/types/orderFunnel'; +import { PrivacyEnum, Suggestions } from '@/types/orderFunnel'; import { useModale } from '@/hooks/useModale'; import { useGetCommand } from '@/hooks/api/ai/notebook/useGetCommand.hook'; import CliEquivalent from './CliEquivalent.component'; -import AddSSHKey from '@/pages/_components/AddSSHKey.component'; import { getNotebookSpec } from '@/lib/orderFunnelHelper'; interface OrderFunnelProps { @@ -91,7 +90,6 @@ const OrderFunnel = ({ const { toast } = useToast(); const [command, setCommand] = useState({ command: '' }); - const addSshKeyModale = useModale('addSshKey'); const { addNotebook, isPending: isPendingAddNotebook } = useAddNotebook({ onError: (err) => { toast({ @@ -102,7 +100,8 @@ const OrderFunnel = ({ }, onSuccess: (notebook) => { toast({ - title: t('successCreatingNotebook'), + title: t('successCreatingNotebookTitle'), + description: t('successCreatingNotebookDescription'), }); navigate(`../${notebook.id}`); }, @@ -495,7 +494,7 @@ const OrderFunnel = ({ size="sm" className="text-base" type="button" - onClick={() => addSshKeyModale.open()} + onClick={() => navigate('./add-sshkey')} > {t('sshkeyAddButtonLabel')} @@ -576,16 +575,6 @@ const OrderFunnel = ({ - { - addSshKeyModale.close(); - const newSshKeyList: OrderSshKey[] = model.form.getValues('sshKey'); - newSshKeyList.push({ name: sshKey.name, sshKey: sshKey.publicKey }); - model.form.setValue('sshKey', newSshKeyList); - }} - /> { + const { projectId, notebookId } = useParams(); + const navigate = useNavigate(); + const notebookQuery = useGetNotebook(projectId, notebookId); + return ( + navigate('../')} + notebook={notebookQuery.data} + /> + ); +}; + +export default DeleteNotebookModal; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/Onboarding.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/onboarding/Onboarding.page.tsx similarity index 100% rename from packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/_components/Onboarding.component.tsx rename to packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/onboarding/Onboarding.page.tsx diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/start/Start.modal.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/start/Start.modal.tsx new file mode 100644 index 000000000000..3431a369082d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/start/Start.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetNotebook } from '@/hooks/api/ai/notebook/useGetNotebook.hook'; +import StartNotebook from '../[notebookId]/_components/StartNotebook.component'; + +const StartNotebookModal = () => { + const { projectId, notebookId } = useParams(); + const navigate = useNavigate(); + const notebookQuery = useGetNotebook(projectId, notebookId); + return ( + navigate('../')} + notebook={notebookQuery.data} + /> + ); +}; + +export default StartNotebookModal; diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/stop/Stop.modal.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/stop/Stop.modal.tsx new file mode 100644 index 000000000000..630ce97b88b4 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/stop/Stop.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetNotebook } from '@/hooks/api/ai/notebook/useGetNotebook.hook'; +import StopNotebook from '../[notebookId]/_components/StopNotebook.component'; + +const StopNotebookModal = () => { + const { projectId, notebookId } = useParams(); + const navigate = useNavigate(); + const notebookQuery = useGetNotebook(projectId, notebookId); + return ( + navigate('../')} + notebook={notebookQuery.data} + /> + ); +}; + +export default StopNotebookModal; 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 7f4e0381518a..078fb4d0d4ac 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/routes/routes.tsx @@ -29,6 +29,41 @@ export default [ path: '', id: 'notebooks', ...lazyRouteConfig(() => import('@/pages/Root.page')), + children: [ + { + path: 'start/:notebookId', + id: 'notebooks.start', + ...lazyRouteConfig(() => + import('@/pages/notebooks/start/Start.modal'), + ), + }, + { + path: 'stop/:notebookId', + id: 'notebooks.stop', + ...lazyRouteConfig(() => + import('@/pages/notebooks/stop/Stop.modal'), + ), + }, + { + path: 'delete/:notebookId', + id: 'notebooks.delete', + ...lazyRouteConfig(() => + import('@/pages/notebooks/delete/Delete.modal'), + ), + }, + ], + }, + { + path: 'auth', + id: 'auth', + ...lazyRouteConfig(() => import('@/pages/notebooks/auth/Auth.page')), + }, + { + path: 'onboarding', + id: 'onboarding', + ...lazyRouteConfig(() => + import('@/pages/notebooks/onboarding/Onboarding.page'), + ), }, { path: 'new', @@ -36,6 +71,15 @@ export default [ ...lazyRouteConfig(() => import('@/pages/notebooks/create/Create.page'), ), + children: [ + { + path: 'add-sshkey', + id: 'create.add-sshkey', + ...lazyRouteConfig(() => + import('@/pages/_components/sshkey/AddSSHKey.modal'), + ), + }, + ], }, { path: ':notebookId', @@ -49,6 +93,17 @@ export default [ ...lazyRouteConfig(() => import('@/pages/notebooks/[notebookId]/dashboard/Dashboard.page'), ), + children: [ + { + path: 'delete', + id: 'notebook.dashboard.delete', + ...lazyRouteConfig(() => + import( + '@/pages/notebooks/[notebookId]/dashboard/delete/Delete.modal' + ), + ), + }, + ], }, { path: 'attach-data', @@ -58,6 +113,26 @@ export default [ '@/pages/notebooks/[notebookId]/attached-data/AttachedData.page' ), ), + children: [ + { + path: 'data-sync', + id: 'notebook.attach-data.data-sync', + ...lazyRouteConfig(() => + import( + '@/pages/notebooks/[notebookId]/attached-data/dataSync/DataSync.modal' + ), + ), + }, + { + path: 'data-sync/:volumeId?', + id: 'notebook.attach-data.data-sync.volume', + ...lazyRouteConfig(() => + import( + '@/pages/notebooks/[notebookId]/attached-data/dataSync/DataSync.modal' + ), + ), + }, + ], }, { path: 'logs', @@ -66,6 +141,24 @@ export default [ import('@/pages/notebooks/[notebookId]/logs/Logs.page'), ), }, + { + path: 'backups', + id: 'notebook.backups', + ...lazyRouteConfig(() => + import('@/pages/notebooks/[notebookId]/backups/Backups.page'), + ), + children: [ + { + path: 'fork/:backupId?', + id: 'notebook.notebook.backups.fork', + ...lazyRouteConfig(() => + import( + '@/pages/notebooks/[notebookId]/backups/fork/Fork.modal' + ), + ), + }, + ], + }, ], }, ], diff --git a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/Project.ts b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/Project.ts index 7ce76596e848..638fd476b39c 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/Project.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/Project.ts @@ -26,3 +26,8 @@ export interface Project { /** Project unleashed */ unleash: boolean; } + +export enum PlanCode { + DISCOVERY = 'project.discovery', + STANDARD = 'project.2018', +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/volume/DataSyncSpec.ts b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/volume/DataSyncSpec.ts index fcbcb9f1915a..5b9b47b3088b 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/volume/DataSyncSpec.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/types/cloud/project/ai/volume/DataSyncSpec.ts @@ -5,7 +5,7 @@ export interface DataSyncSpec { /** Direction of the sync */ direction: DataSyncEnum; /** True if the user has created the object */ - manual: boolean; + manual?: boolean; /** Only sync this volume */ volume?: string; }