From 288f3abf3c79456ad1d2cfe8526d50b0284f92e7 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:58:34 +0100 Subject: [PATCH] feat: adding icon for non downloaded models (#1866) * feat: adding new remote model icon Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix: prettier Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * test: ensuring expected behaviour Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * feat: sharing ModelStatusIcon between model details and model table Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix(ui): model tests Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * Update packages/frontend/src/lib/icons/ModelStatusIcon.svelte Co-authored-by: Florent BENOIT Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix: prettier Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --------- Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> Co-authored-by: Florent BENOIT --- .../ModelStatusIcon.spec.ts} | 45 +++++++++++-------- .../src/lib/icons/ModelStatusIcon.svelte | 30 +++++++++++++ .../frontend/src/lib/icons/RemoteModel.svelte | 24 ++++++++++ .../lib/table/model/ModelColumnIcon.svelte | 25 ----------- packages/frontend/src/pages/Model.spec.ts | 32 ++++++++++++- packages/frontend/src/pages/Model.svelte | 8 ++++ packages/frontend/src/pages/Models.svelte | 14 +++--- 7 files changed, 126 insertions(+), 52 deletions(-) rename packages/frontend/src/lib/{table/model/ModelColumnIcon.spec.ts => icons/ModelStatusIcon.spec.ts} (78%) create mode 100644 packages/frontend/src/lib/icons/ModelStatusIcon.svelte create mode 100644 packages/frontend/src/lib/icons/RemoteModel.svelte delete mode 100644 packages/frontend/src/lib/table/model/ModelColumnIcon.svelte diff --git a/packages/frontend/src/lib/table/model/ModelColumnIcon.spec.ts b/packages/frontend/src/lib/icons/ModelStatusIcon.spec.ts similarity index 78% rename from packages/frontend/src/lib/table/model/ModelColumnIcon.spec.ts rename to packages/frontend/src/lib/icons/ModelStatusIcon.spec.ts index 9692bbad8..4159f4306 100644 --- a/packages/frontend/src/lib/table/model/ModelColumnIcon.spec.ts +++ b/packages/frontend/src/lib/icons/ModelStatusIcon.spec.ts @@ -20,26 +20,18 @@ import '@testing-library/jest-dom/vitest'; import { expect, test, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import type { ModelInfo } from '@shared/src/models/IModelInfo'; -import ModelColumnIcon from './ModelColumnIcon.svelte'; +import ModelColumnIcon from './ModelStatusIcon.svelte'; import { type InferenceServer, InferenceType } from '@shared/src/models/IInference'; +import { readable } from 'svelte/store'; +import * as inferenceStore from '/@/stores/inferenceServers'; -const mocks = vi.hoisted(() => { - return { - getInferenceServersMock: vi.fn<() => InferenceServer[]>(), - }; -}); - -vi.mock('../../../stores/inferenceServers', () => ({ - inferenceServers: { - subscribe: (f: (msg: InferenceServer[]) => void) => { - f(mocks.getInferenceServersMock()); - return (): void => {}; - }, - }, +vi.mock('/@/stores/inferenceServers', () => ({ + inferenceServers: vi.fn(), })); beforeEach(() => { vi.resetAllMocks(); + (inferenceStore.inferenceServers as unknown) = readable([]); }); test('Expect remote model to have NONE title', async () => { @@ -53,8 +45,6 @@ test('Expect remote model to have NONE title', async () => { memory: 1000, }; - mocks.getInferenceServersMock.mockReturnValue([]); - render(ModelColumnIcon, { object }); const role = screen.getByRole('status'); @@ -79,8 +69,6 @@ test('Expect downloaded model to have DOWNLOADED title', async () => { memory: 1000, }; - mocks.getInferenceServersMock.mockReturnValue([]); - render(ModelColumnIcon, { object }); const role = screen.getByRole('status'); @@ -105,7 +93,7 @@ test('Expect in used model to have USED title', async () => { memory: 1000, }; - mocks.getInferenceServersMock.mockReturnValue([ + (inferenceStore.inferenceServers as unknown) = readable([ { models: [object], type: InferenceType.LLAMA_CPP, @@ -121,9 +109,28 @@ test('Expect in used model to have USED title', async () => { labels: {}, }, ]); + render(ModelColumnIcon, { object }); const role = screen.getByRole('status'); expect(role).toBeDefined(); expect(role.title).toBe('USED'); }); + +test('Expect non-downloaded model to have NONE title', async () => { + const object: ModelInfo = { + id: 'model-downloaded-id', + description: '', + license: '', + name: '', + registry: '', + url: '', + memory: 1000, + }; + + render(ModelColumnIcon, { object }); + + const role = screen.getByRole('status'); + expect(role).toBeDefined(); + expect(role.title).toBe('NONE'); +}); diff --git a/packages/frontend/src/lib/icons/ModelStatusIcon.svelte b/packages/frontend/src/lib/icons/ModelStatusIcon.svelte new file mode 100644 index 000000000..3043834f2 --- /dev/null +++ b/packages/frontend/src/lib/icons/ModelStatusIcon.svelte @@ -0,0 +1,30 @@ + + +{#if status === 'NONE'} +
+ +
+{:else} + +{/if} diff --git a/packages/frontend/src/lib/icons/RemoteModel.svelte b/packages/frontend/src/lib/icons/RemoteModel.svelte new file mode 100644 index 000000000..de76c74b2 --- /dev/null +++ b/packages/frontend/src/lib/icons/RemoteModel.svelte @@ -0,0 +1,24 @@ + + +
+ + + + + + +
diff --git a/packages/frontend/src/lib/table/model/ModelColumnIcon.svelte b/packages/frontend/src/lib/table/model/ModelColumnIcon.svelte deleted file mode 100644 index 52ee53049..000000000 --- a/packages/frontend/src/lib/table/model/ModelColumnIcon.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/packages/frontend/src/pages/Model.spec.ts b/packages/frontend/src/pages/Model.spec.ts index 7fde3adec..5c2620dc7 100644 --- a/packages/frontend/src/pages/Model.spec.ts +++ b/packages/frontend/src/pages/Model.spec.ts @@ -16,11 +16,14 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import { vi, test, expect } from 'vitest'; +import { vi, test, expect, beforeEach } from 'vitest'; import { screen, render } from '@testing-library/svelte'; import Model from './Model.svelte'; import { studioClient } from '../utils/client'; import type { ModelInfo } from '@shared/src/models/IModelInfo'; +import * as inferenceStore from '/@/stores/inferenceServers'; +import { readable } from 'svelte/store'; +import type { InferenceServer } from '@shared/src/models/IInference'; vi.mock('../utils/client', async () => { return { @@ -37,6 +40,10 @@ vi.mock('../utils/client', async () => { }; }); +vi.mock('/@/stores/inferenceServers', () => ({ + inferenceServers: vi.fn(), +})); + const model: ModelInfo = { id: 'model1', name: 'Model 1', @@ -44,6 +51,29 @@ const model: ModelInfo = { description: '', }; +beforeEach(() => { + (inferenceStore.inferenceServers as unknown) = readable([]); +}); + +test('model status should be visible', async () => { + vi.mocked(studioClient.getCatalog).mockResolvedValue({ + models: [model], + categories: [], + recipes: [], + version: 'v1', + }); + + const { getByRole } = render(Model, { + modelId: model.id, + }); + + await vi.waitFor(() => { + const role = getByRole('status'); + expect(role).toBeDefined(); + expect(role.title).toBe('NONE'); + }); +}); + test('should display model information', async () => { vi.mocked(studioClient.getCatalog).mockResolvedValue({ models: [model], diff --git a/packages/frontend/src/pages/Model.svelte b/packages/frontend/src/pages/Model.svelte index 91ca9b216..9938d7a27 100644 --- a/packages/frontend/src/pages/Model.svelte +++ b/packages/frontend/src/pages/Model.svelte @@ -3,6 +3,7 @@ import MarkdownRenderer from '/@/lib/markdown/MarkdownRenderer.svelte'; import { catalog } from '/@/stores/catalog'; import { DetailsPage } from '@podman-desktop/ui-svelte'; import { router } from 'tinro'; +import ModelStatusIcon from '/@/lib/icons/ModelStatusIcon.svelte'; export let modelId: string; @@ -20,6 +21,13 @@ export function goToUpPage(): void { breadcrumbTitle="Go back to Models" onclose={goToUpPage} onbreadcrumbClick={goToUpPage}> + + {#if model} +
+ +
+ {/if} +
diff --git a/packages/frontend/src/pages/Models.svelte b/packages/frontend/src/pages/Models.svelte index 5f9d0f77f..1d33e77c4 100644 --- a/packages/frontend/src/pages/Models.svelte +++ b/packages/frontend/src/pages/Models.svelte @@ -13,30 +13,30 @@ import ModelColumnActions from '../lib/table/model/ModelColumnActions.svelte'; import { EmptyScreen, Tab, Button, Table, TableColumn, TableRow, NavPage } from '@podman-desktop/ui-svelte'; import Route from '/@/Route.svelte'; import { tasks } from '/@/stores/tasks'; -import ModelColumnIcon from '../lib/table/model/ModelColumnIcon.svelte'; +import ModelStatusIcon from '../lib/icons/ModelStatusIcon.svelte'; import { router } from 'tinro'; import { faBookOpen, faFileImport } from '@fortawesome/free-solid-svg-icons'; -const columns: TableColumn[] = [ +const columns = [ new TableColumn('Status', { width: '60px', - renderer: ModelColumnIcon, - comparator: (a, b) => (a.file ? 0 : 1) - (b.file ? 0 : 1), + renderer: ModelStatusIcon, + comparator: (a, b): number => (a.file ? 0 : 1) - (b.file ? 0 : 1), }), new TableColumn('Name', { width: '3fr', renderer: ModelColumnName, - comparator: (a, b) => b.name.localeCompare(a.name), + comparator: (a, b): number => b.name.localeCompare(a.name), }), new TableColumn('Size', { width: '50px', renderer: ModelColumnSize, - comparator: (a, b) => (a.file?.size ?? 0) - (b.file?.size ?? 0), + comparator: (a, b): number => (a.file?.size ?? 0) - (b.file?.size ?? 0), }), new TableColumn('Age', { width: '70px', renderer: ModelColumnAge, - comparator: (a, b) => (a.file?.creation?.getTime() ?? 0) - (b.file?.creation?.getTime() ?? 0), + comparator: (a, b): number => (a.file?.creation?.getTime() ?? 0) - (b.file?.creation?.getTime() ?? 0), }), new TableColumn('', { width: '225px', align: 'right', renderer: ModelColumnLabels }), new TableColumn('Actions', { align: 'right', width: '120px', renderer: ModelColumnActions }),