Skip to content

Commit

Permalink
feat(ai.notebooks): header tabs and dashboard page
Browse files Browse the repository at this point in the history
ref:DATATR-1636, DATATR-1638
Signed-off-by: Arthur Bullet <[email protected]>
  • Loading branch information
abullet33 committed Nov 15, 2024
1 parent 802e674 commit a2ca427
Show file tree
Hide file tree
Showing 24 changed files with 743 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"tableHeaderPrivacy": "Confidentialité ",
"networkSecureTitle": "Privé",
"networkPublicTitle": "Public",
"tableHeaderDuration": "Durée de fonctionnement",
"tableHeaderDuration": "Durée",
"tableHeaderStatus": "Statut",
"tableActionManage": "Gérer",
"tableActionStart": "Démarrer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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é"
}
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"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": "Not that much for now",
"workspaceStorage": "Espace de travail {{ storage }} SSD inclus",
"workspaceStorageHelper": "Nothing for now",
"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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as ai from '@/types/cloud/project/ai';

export const mockedLabel: ai.Label = {
name: 'labelName',
value: 'labelValue',
};
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -22,7 +28,7 @@ const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => {

return (
<div className="flex flex-col">
<div className="flex flex-row items-center justify-between p-2 px-8">
<div className="flex flex-row items-center justify-between p-2 px-6">
<p>{title}</p>
<Button
data-testid="code-block-copy-button"
Expand All @@ -34,7 +40,7 @@ const CliCodeBlock = ({ title, code, toastMessage }: CliCodeBlockProps) => {
<span className="sr-only">copy</span>
</Button>
</div>
<ScrollArea className="max-h-[80vh] px-6">
<ScrollArea className={size}>
<pre
style={{ wordBreak: 'break-word' }}
className="p-4 whitespace-pre-wrap rounded-md bg-[#122844] text-white"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const buttonVariants = cva(
link: "text-base",
table: "h-4 w-4 my-auto",
menu: 'size-8 p-0',
input: "h-10 w-full rounded-md px-3 py-2 text-sm"
input: "h-10 w-full rounded-md px-3 py-2 text-sm",
roundedIcon: "h-8 w-8 rounded-full",
},
},
defaultVariants: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ const Slider = React.forwardRef<
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-gray-100">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -55,6 +57,7 @@ export default function NotebookLayout() {
if (!notebook) {
return (
<>
<NotebookHeader.Skeleton />
<TabsMenu.Skeleton />
Loading your notebook data
</>
Expand All @@ -67,7 +70,8 @@ export default function NotebookLayout() {

return (
<>
<div>{notebook.spec.name}</div>
<NotebookHeader notebook={notebook} />
<NotebookTabs notebook={notebook} />
<div className="space-y-2">
<Outlet context={notebookLayoutContext} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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';

export const NotebookHeader = ({
notebook,
}: {
notebook: ai.notebook.Notebook;
}) => {
const { t } = useTranslation('regions');
return (
<div
data-testid="notebook-header-container"
className="flex gap-2 items-center mt-4 mb-6"
>
<div className="rounded-full bg-gradient-to-tr from-primary to-slate-50 text-white p-2">
<NotebookText width={40} height={40} />
</div>
<div className="w-full">
<div className="flex flex-row items-center gap-8">
<h2>{notebook.spec.name ?? 'Dashboard'}</h2>
<div className="flex flex-row gap-2">
<Button type="button" size="roundedIcon">
<PlayIcon className="size-4 ml-1 fill-white" />
</Button>
<Button type="button" size="roundedIcon" className="bg-red-400">
<Square className="size-4 fill-white" />
</Button>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<NotebookStatusBadge status={notebook.status.state} />
<Badge variant={'outline'} className="capitalize">
{notebook.spec.env.frameworkId}
</Badge>
<Badge variant={'outline'}>
{notebook.spec.env.frameworkVersion}
</Badge>
<Badge variant={'outline'} className="capitalize">
{t(`region_${notebook.spec.region}`)}
</Badge>
<Badge variant={'outline'} className="capitalize">
{notebook.spec.env.editorId}
</Badge>
</div>
</div>
</div>
);
};

NotebookHeader.Skeleton = function NotebookHeaderSkeleton() {
return (
<div className="flex gap-2 items-center mt-4 mb-6">
<Skeleton className="rounded-full h-14 w-14" />
<div>
<h2>Dashboard</h2>
<div className="flex gap-2">
<Skeleton className="h-4 w-10" />
<Skeleton className="h-4 w-10" />
<Skeleton className="h-4 w-10" />
<Skeleton className="h-4 w-10" />
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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 <TabsMenu tabs={tabs} />;
};

export default NotebookTabs;
Loading

0 comments on commit a2ca427

Please sign in to comment.