Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

send application tasks to frontend when updated #102

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,15 @@ export class ApplicationManager {
}
}

let previousProgressValue = -1;
resp.on('data', chunk => {
progress += chunk.length;
const progressValue = (progress * 100) / totalFileSize;

taskUtil.setTaskProgress(modelId, progressValue);
if (progressValue === 100 || progressValue - previousProgressValue > 1) {
previousProgressValue = progressValue;
taskUtil.setTaskProgress(modelId, progressValue);
}

// send progress in percentage (ex. 1.2%, 2.6%, 80.1%) to frontend
//this.sendProgress(progressValue);
Expand Down
21 changes: 20 additions & 1 deletion packages/backend/src/registries/RecipeStatusRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,40 @@

import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import type { TaskRegistry } from './TaskRegistry';
import type { Webview } from '@podman-desktop/api';
import { MSG_NEW_RECIPE_STATE } from '@shared/Messages';

export class RecipeStatusRegistry {
private statuses: Map<string, RecipeStatus> = new Map<string, RecipeStatus>();

constructor(private taskRegistry: TaskRegistry) {}
constructor(
private taskRegistry: TaskRegistry,
private webview: Webview,
) {}

setStatus(recipeId: string, status: RecipeStatus) {
// Update the TaskRegistry
if (status.tasks && status.tasks.length > 0) {
status.tasks.map(task => this.taskRegistry.set(task));
}
this.statuses.set(recipeId, status);
this.dispatchState().catch((err: unknown) => {
console.error('error dispatching recipe statuses', err);
}); // we don't want to wait
}

getStatus(recipeId: string): RecipeStatus | undefined {
return this.statuses.get(recipeId);
}

getStatuses(): Map<string, RecipeStatus> {
return this.statuses;
}

private async dispatchState() {
await this.webview.postMessage({
id: MSG_NEW_RECIPE_STATE,
body: this.statuses,
});
}
}
4 changes: 0 additions & 4 deletions packages/backend/src/registries/TaskRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ import type { Task } from '@shared/src/models/ITask';
export class TaskRegistry {
private tasks: Map<string, Task> = new Map<string, Task>();

getTasksByLabel(label: string): Task[] {
return Array.from(this.tasks.values()).filter(task => label in (task.labels || {}));
}

set(task: Task) {
this.tasks.set(task.id, task);
}
Expand Down
2 changes: 0 additions & 2 deletions packages/backend/src/studio-api-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import type { ApplicationManager } from './managers/applicationManager';
import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import { StudioApiImpl } from './studio-api-impl';
import type { PlayGroundManager } from './managers/playground';
import type { TaskRegistry } from './registries/TaskRegistry';
import type { Webview } from '@podman-desktop/api';

import * as fs from 'node:fs';
Expand Down Expand Up @@ -75,7 +74,6 @@ beforeEach(async () => {
appUserDirectory,
} as unknown as ApplicationManager,
{} as unknown as RecipeStatusRegistry,
{} as unknown as TaskRegistry,
{} as unknown as PlayGroundManager,
catalogManager,
);
Expand Down
11 changes: 4 additions & 7 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import type { ApplicationManager } from './managers/applicationManager';
import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import type { TaskRegistry } from './registries/TaskRegistry';
import type { Task } from '@shared/src/models/ITask';
import type { PlayGroundManager } from './managers/playground';
import * as podmanDesktopApi from '@podman-desktop/api';
import type { QueryState } from '@shared/src/models/IPlaygroundQueryState';
Expand All @@ -36,7 +34,6 @@ export class StudioApiImpl implements StudioAPI {
constructor(
private applicationManager: ApplicationManager,
private recipeStatusRegistry: RecipeStatusRegistry,
private taskRegistry: TaskRegistry,
private playgroundManager: PlayGroundManager,
private catalogManager: CatalogManager,
) {}
Expand All @@ -53,6 +50,10 @@ export class StudioApiImpl implements StudioAPI {
return this.recipeStatusRegistry.getStatus(recipeId);
}

async getPullingStatuses(): Promise<Map<string, RecipeStatus>> {
return this.recipeStatusRegistry.getStatuses();
}

async getModelById(modelId: string): Promise<ModelInfo> {
// TODO: move logic to catalog manager
const model = this.catalogManager.getModels().find(m => modelId === m.id);
Expand Down Expand Up @@ -83,10 +84,6 @@ export class StudioApiImpl implements StudioAPI {
return this.catalogManager.getModels().filter(m => localIds.includes(m.id));
}

async getTasksByLabel(label: string): Promise<Task[]> {
return this.taskRegistry.getTasksByLabel(label);
}

async startPlayground(modelId: string): Promise<void> {
// TODO: improve the following
const localModelInfo = this.applicationManager.getLocalModels().filter(m => m.id === modelId);
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class Studio {
this.rpcExtension = new RpcExtension(this.#panel.webview);
const gitManager = new GitManager();
const taskRegistry = new TaskRegistry();
const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry);
const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry, this.#panel.webview);
const applicationManager = new ApplicationManager(gitManager, recipeStatusRegistry, this.#extensionContext);
this.playgroundManager = new PlayGroundManager(this.#panel.webview);
// Create catalog manager, responsible for loading the catalog files and watching for changes
Expand All @@ -102,7 +102,6 @@ export class Studio {
this.studioApi = new StudioApiImpl(
applicationManager,
recipeStatusRegistry,
taskRegistry,
this.playgroundManager,
this.catalogManager,
);
Expand Down
30 changes: 11 additions & 19 deletions packages/frontend/src/pages/Models.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ import ModelColumnRegistry from '../lib/table/model/ModelColumnRegistry.svelte';
import ModelColumnPopularity from '../lib/table/model/ModelColumnPopularity.svelte';
import ModelColumnLicense from '../lib/table/model/ModelColumnLicense.svelte';
import ModelColumnHw from '../lib/table/model/ModelColumnHW.svelte';
import { onDestroy, onMount } from 'svelte';
import { studioClient } from '/@/utils/client';
import type { Category } from '@shared/models/ICategory';
import type { Task } from '@shared/models/ITask';
import type { Task } from '@shared/src/models/ITask';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import { faRefresh } from '@fortawesome/free-solid-svg-icons';
import Card from '/@/lib/Card.svelte';
import Button from '/@/lib/button/Button.svelte';
import LinearProgress from '/@/lib/progress/LinearProgress.svelte';
import { modelsPulling } from '../stores/recipe';
import { onMount } from 'svelte';

const columns: Column<ModelInfo>[] = [
new Column<ModelInfo>('Name', { width: '4fr', renderer: ModelColumnName }),
Expand All @@ -29,7 +25,6 @@ const columns: Column<ModelInfo>[] = [
const row = new Row<ModelInfo>({});

let loading: boolean = true;
let intervalId: ReturnType<typeof setInterval> | undefined = undefined;

let tasks: Task[] = [];
let models: ModelInfo[] = [];
Expand All @@ -46,31 +41,28 @@ function filterModels(): void {
}
return previousValue;
}, [] as string[]);
filteredModels = models.filter((model) => !(model.id in modelsId));
filteredModels = models.filter((model) => !modelsId.includes(model.id));
}

onMount(() => {
// Pulling update
intervalId = setInterval(async () => {
tasks = await studioClient.getTasksByLabel("model-pulling");
const modelsPullingUnsubscribe = modelsPulling.subscribe(runningTasks => {
tasks = runningTasks;
loading = false;
filterModels();
}, 1000);
});

// Subscribe to the models store
return localModels.subscribe((value) => {
const localModelsUnsubscribe = localModels.subscribe((value) => {
models = value;
filterModels();
})
});

onDestroy(() => {
if(intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
return () => {
modelsPullingUnsubscribe();
localModelsUnsubscribe();
}
});

</script>

<NavPage title="Models on disk" searchEnabled="{false}" loading="{loading}">
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/pages/Recipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import Recipe from './Recipe.svelte';
const mocks = vi.hoisted(() => {
return {
getCatalogMock: vi.fn(),
getPullingStatusesMock: vi.fn(),
};
});

vi.mock('../utils/client', async () => {
return {
studioClient: {
getCatalog: mocks.getCatalogMock,
getPullingStatuses: mocks.getPullingStatusesMock,
},
rpcBrowser: {
subscribe: () => {
Expand All @@ -29,6 +31,7 @@ test('should display recipe information', async () => {
expect(recipe).not.toBeUndefined();

mocks.getCatalogMock.mockResolvedValue(catalog);
mocks.getPullingStatusesMock.mockResolvedValue(new Map());
render(Recipe, {
recipeId: 'recipe 1',
});
Expand Down
28 changes: 3 additions & 25 deletions packages/frontend/src/pages/Recipe.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import NavPage from '/@/lib/NavPage.svelte';
import { onDestroy, onMount } from 'svelte';
import { studioClient } from '/@/utils/client';
import Tab from '/@/lib/Tab.svelte';
import Route from '/@/Route.svelte';
Expand All @@ -12,45 +11,24 @@ import { faDownload, faRefresh } from '@fortawesome/free-solid-svg-icons';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import Button from '/@/lib/button/Button.svelte';
import { getDisplayName } from '/@/utils/versionControlUtils';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import { getIcon } from '/@/utils/categoriesUtils';
import RecipeModels from './RecipeModels.svelte';
import { catalog } from '/@/stores/catalog';
import { recipes } from '/@/stores/recipe';

export let recipeId: string;

// The recipe model provided
$: recipe = $catalog.recipes.find(r => r.id === recipeId);
$: categories = $catalog.categories;
$: recipeStatus = $recipes.get(recipeId);

// By default, we are loading the recipe information
let loading: boolean = true;

// The pulling tasks
let recipeStatus: RecipeStatus | undefined = undefined;

let intervalId: ReturnType<typeof setInterval> | undefined = undefined;

onMount(async () => {
// Pulling update
intervalId = setInterval(async () => {
recipeStatus = await studioClient.getPullingStatus(recipeId);
loading = false;
}, 1000);
})

let loading: boolean = false;
const onPullingRequest = async () => {
loading = true;
await studioClient.pullApplication(recipeId);
}

onDestroy(() => {
if(intervalId !== undefined) {
clearInterval(intervalId);
intervalId = undefined;
}
});

const onClickRepository = () => {
if (recipe) {
studioClient.openURL(recipe.repository);
Expand Down
27 changes: 27 additions & 0 deletions packages/frontend/src/stores/recipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Readable } from 'svelte/store';
import { derived, readable } from 'svelte/store';
import { MSG_NEW_RECIPE_STATE } from '@shared/Messages';
import { rpcBrowser, studioClient } from '/@/utils/client';
import type { RecipeStatus } from '@shared/src/models/IRecipeStatus';

export const recipes: Readable<Map<string, RecipeStatus>> = readable<Map<string, RecipeStatus>>(
new Map<string, RecipeStatus>(),
set => {
const sub = rpcBrowser.subscribe(MSG_NEW_RECIPE_STATE, msg => {
set(msg);
});
// Initialize the store manually
studioClient.getPullingStatuses().then(state => {
set(state);
});
return () => {
sub.unsubscribe();
};
},
);

export const modelsPulling = derived(recipes, $recipes => {
return Array.from($recipes.values())
.flatMap(recipe => recipe.tasks)
.filter(task => 'model-pulling' in (task.labels || {}));
});
1 change: 1 addition & 0 deletions packages/shared/Messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const MSG_PLAYGROUNDS_STATE_UPDATE = 'playgrounds-state-update';
export const MSG_NEW_PLAYGROUND_QUERIES_STATE = 'new-playground-queries-state';
export const MSG_NEW_CATALOG_STATE = 'new-catalog-state';
export const MSG_NEW_RECIPE_STATE = 'new-recipe-state';
10 changes: 1 addition & 9 deletions packages/shared/src/StudioAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { RecipeStatus } from './models/IRecipeStatus';
import type { ModelInfo } from './models/IModelInfo';
import type { Task } from './models/ITask';
import type { QueryState } from './models/IPlaygroundQueryState';
import type { Catalog } from './models/ICatalog';
import type { PlaygroundState } from './models/IPlaygroundState';
Expand All @@ -9,6 +8,7 @@ export abstract class StudioAPI {
abstract ping(): Promise<string>;
abstract getCatalog(): Promise<Catalog>;
abstract getPullingStatus(recipeId: string): Promise<RecipeStatus>;
abstract getPullingStatuses(): Promise<Map<string, RecipeStatus>>;
abstract pullApplication(recipeId: string): Promise<void>;
abstract openURL(url: string): Promise<boolean>;
/**
Expand All @@ -19,14 +19,6 @@ export abstract class StudioAPI {
abstract startPlayground(modelId: string): Promise<void>;
abstract stopPlayground(modelId: string): Promise<void>;
abstract askPlayground(modelId: string, prompt: string): Promise<number>;

/**
* Get task by label
* @param label
*/
abstract getTasksByLabel(label: string): Promise<Task[]>;

abstract getPlaygroundQueriesState(): Promise<QueryState[]>;

abstract getPlaygroundsState(): Promise<PlaygroundState[]>;
}