diff --git a/packages/backend/src/managers/catalogManager.ts b/packages/backend/src/managers/catalogManager.ts index b92aa85a8..83037692f 100644 --- a/packages/backend/src/managers/catalogManager.ts +++ b/packages/backend/src/managers/catalogManager.ts @@ -5,11 +5,17 @@ import defaultCatalog from '../ai.json'; import type { Category } from '@shared/src/models/ICategory'; import type { Recipe } from '@shared/src/models/IRecipe'; import type { ModelInfo } from '@shared/src/models/IModelInfo'; +import { MSG_NEW_CATALOG_STATE } from '@shared/Messages'; +import { fs } from '@podman-desktop/api'; +import type { Webview } from '@podman-desktop/api'; export class CatalogManager { private catalog: Catalog; - constructor(private appUserDirectory: string) { + constructor( + private appUserDirectory: string, + private webview: Webview, + ) { // We start with an empty catalog, for the methods to work before the catalog is loaded this.catalog = { categories: [], @@ -18,6 +24,10 @@ export class CatalogManager { }; } + public getCatalog(): Catalog { + return this.catalog; + } + public getCategories(): Category[] { return this.catalog.categories; } @@ -31,23 +41,61 @@ export class CatalogManager { async loadCatalog() { const catalogPath = path.resolve(this.appUserDirectory, 'catalog.json'); + if (!existsSync(catalogPath)) { + return this.setCatalog(defaultCatalog); + } + try { - if (!existsSync(catalogPath)) { - this.setCatalog(defaultCatalog); - return; - } - // TODO(feloy): watch catalog file and update catalog with new content - const data = await promises.readFile(catalogPath, 'utf-8'); - const cat = JSON.parse(data) as Catalog; - this.setCatalog(cat); + this.watchCatalogFile(catalogPath); // do not await, we want to do this async + } catch (err: unknown) { + console.error('unable to watch catalog file, changes to the catalog file won\'t be reflected to the UI', err); + } + + try { + const cat = await this.readAndAnalyzeCatalog(catalogPath); + return this.setCatalog(cat); } catch (err: unknown) { console.error('unable to read catalog file, reverting to default catalog', err); - this.setCatalog(defaultCatalog); } + // If something went wrong we load the default catalog + return this.setCatalog(defaultCatalog); + } + + watchCatalogFile(path: string) { + const watcher = fs.createFileSystemWatcher(path); + watcher.onDidCreate(async () => { + try { + const cat = await this.readAndAnalyzeCatalog(path); + await this.setCatalog(cat); + } catch (err: unknown) { + console.error('unable to read created catalog file, continue using default catalog', err); + } + }); + watcher.onDidDelete(async () => { + console.log('user catalog file deleted, reverting to default catalog'); + await this.setCatalog(defaultCatalog); + }); + watcher.onDidChange(async () => { + try { + const cat = await this.readAndAnalyzeCatalog(path); + await this.setCatalog(cat); + } catch (err: unknown) { + console.error('unable to read modified catalog file, reverting to default catalog', err); + } + }); + } + + async readAndAnalyzeCatalog(path: string): Promise { + const data = await promises.readFile(path, 'utf-8'); + return JSON.parse(data) as Catalog; + // TODO(feloy): check version, ... } - setCatalog(newCatalog: Catalog) { - // TODO(feloy): send message to frontend with new catalog + async setCatalog(newCatalog: Catalog) { this.catalog = newCatalog; + await this.webview.postMessage({ + id: MSG_NEW_CATALOG_STATE, + body: this.catalog, + }); } } diff --git a/packages/backend/src/studio-api-impl.spec.ts b/packages/backend/src/studio-api-impl.spec.ts index fefb9a2d4..71c31bbd6 100644 --- a/packages/backend/src/studio-api-impl.spec.ts +++ b/packages/backend/src/studio-api-impl.spec.ts @@ -29,6 +29,7 @@ import type { TaskRegistry } from './registries/TaskRegistry'; import type { Webview } from '@podman-desktop/api'; import * as fs from 'node:fs'; +import { CatalogManager } from './managers/catalogManager'; vi.mock('./ai.json', () => { return { @@ -41,17 +42,34 @@ vi.mock('node:fs', () => { existsSync: vi.fn(), promises: { readFile: vi.fn(), - } + }, }; }); +vi.mock('@podman-desktop/api', () => { + return { + fs: { + createFileSystemWatcher: () => ({ + onDidCreate: vi.fn(), + onDidDelete: vi.fn(), + onDidChange: vi.fn(), + }), + }, + }; +}); let studioApiImpl: StudioApiImpl; let catalogManager; beforeEach(async () => { const appUserDirectory = '.'; - catalogManager = new CatalogManager(appUserDirectory); + + // Creating CatalogManager + catalogManager = new CatalogManager(appUserDirectory, { + postMessage: vi.fn(), + } as unknown as Webview); + + // Creating StudioApiImpl studioApiImpl = new StudioApiImpl( { appUserDirectory, @@ -60,9 +78,6 @@ beforeEach(async () => { {} as unknown as TaskRegistry, {} as unknown as PlayGroundManager, catalogManager, - { - postMessage: vi.fn(), - } as unknown as Webview, ); vi.resetAllMocks(); vi.mock('node:fs'); @@ -84,8 +99,8 @@ describe('invalid user catalog', () => { ); }); - test('expect error if id does not correspond to any model', () => { - expect(() => studioApiImpl.getModelById('unknown')).toThrowError('No model found having id unknown'); + test('expect error if id does not correspond to any model', async () => { + await expect(() => studioApiImpl.getModelById('unknown')).rejects.toThrowError('No model found having id unknown'); }); }); diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts index 57b1e7b3a..e8b1b0be1 100644 --- a/packages/backend/src/studio-api-impl.ts +++ b/packages/backend/src/studio-api-impl.ts @@ -10,11 +10,10 @@ 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'; -import type { Catalog } from '@shared/src/models/ICatalog'; -import { MSG_NEW_CATALOG_STATE } from '@shared/Messages'; import * as path from 'node:path'; import type { CatalogManager } from './managers/catalogManager'; +import type { Catalog } from '@shared/src/models/ICatalog'; export const RECENT_CATEGORY_ID = 'recent-category'; @@ -25,67 +24,10 @@ export class StudioApiImpl implements StudioAPI { private taskRegistry: TaskRegistry, private playgroundManager: PlayGroundManager, private catalogManager: CatalogManager, - private webview: podmanDesktopApi.Webview, ) {} - async loadCatalog() { - const catalogPath = path.resolve(this.applicationManager.appUserDirectory, 'catalog.json'); - - try { - this.watchCatalogFile(catalogPath); // do not await, we want to do this async - } catch (err: unknown) { - console.error("unable to watch catalog file, changes to the catalog file won't be reflected to the UI", err); - } - - try { - if (!fs.existsSync(catalogPath)) { - await this.setCatalog(defaultCatalog); - return; - } - const cat = await this.readAndAnalyzeCatalog(catalogPath); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read catalog file, reverting to default catalog', err); - await this.setCatalog(defaultCatalog); - } - } - - watchCatalogFile(path: string) { - const watcher = podmanDesktopApi.fs.createFileSystemWatcher(path); - watcher.onDidCreate(async () => { - try { - const cat = await this.readAndAnalyzeCatalog(path); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read created catalog file, continue using default catalog', err); - } - }); - watcher.onDidDelete(async () => { - console.log('user catalog file deleted, reverting to default catalog'); - await this.setCatalog(defaultCatalog); - }); - watcher.onDidChange(async () => { - try { - const cat = await this.readAndAnalyzeCatalog(path); - await this.setCatalog(cat); - } catch (err: unknown) { - console.error('unable to read modified catalog file, reverting to default catalog', err); - } - }); - } - - async readAndAnalyzeCatalog(path: string): Promise { - const data = await fs.promises.readFile(path, 'utf-8'); - return JSON.parse(data) as Catalog; - // TODO(feloy): check version, ... - } - - async setCatalog(newCatalog: Catalog) { - this.catalog = newCatalog; - await this.webview.postMessage({ - id: MSG_NEW_CATALOG_STATE, - body: this.catalog, - }); + async ping(): Promise { + return 'pong'; } async openURL(url: string): Promise { @@ -96,10 +38,6 @@ export class StudioApiImpl implements StudioAPI { return this.recipeStatusRegistry.getStatus(recipeId); } - async ping(): Promise { - return 'pong'; - } - async getRecentRecipes(): Promise { return []; // no recent implementation for now } @@ -187,4 +125,8 @@ export class StudioApiImpl implements StudioAPI { async getPlaygroundStates(): Promise { return this.playgroundManager.getState(); } + + async getCatalog(): Promise { + return this.catalogManager.getCatalog(); + } } diff --git a/packages/backend/src/studio.ts b/packages/backend/src/studio.ts index f80118b4f..a81f3fa22 100644 --- a/packages/backend/src/studio.ts +++ b/packages/backend/src/studio.ts @@ -95,7 +95,11 @@ export class Studio { const recipeStatusRegistry = new RecipeStatusRegistry(taskRegistry); const applicationManager = new ApplicationManager(gitManager, recipeStatusRegistry, this.#extensionContext); this.playgroundManager = new PlayGroundManager(this.#panel.webview); - this.catalogManager = new CatalogManager(applicationManager.appUserDirectory); + // Create catalog manager, responsible for loading the catalog files and watching for changes + this.catalogManager = new CatalogManager( + applicationManager.appUserDirectory, + this.#panel.webview, + ); // Creating StudioApiImpl this.studioApi = new StudioApiImpl( @@ -104,7 +108,6 @@ export class Studio { taskRegistry, this.playgroundManager, this.catalogManager, - this.#panel.webview, ); await this.catalogManager.loadCatalog();