Skip to content

Commit

Permalink
feat: support task redirection (#1745)
Browse files Browse the repository at this point in the history
* feat: support task redirection

Signed-off-by: axel7083 <[email protected]>

* fix: linter&prettier

Signed-off-by: axel7083 <[email protected]>

* fix: typecheck

Signed-off-by: axel7083 <[email protected]>

---------

Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 authored Sep 30, 2024
1 parent 46f4759 commit 4d420cb
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 19 deletions.
14 changes: 13 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
},
"main": "./dist/extension.cjs",
"contributes": {
"commands": [
{
"command": "ai-lab.navigation.inference.start",
"title": "AI Lab: navigate to inference start page",
"hidden": true
},
{
"command": "ai-lab.navigation.recipe.start",
"title": "AI Lab: navigate to recipe start page",
"hidden": true
}
],
"configuration": {
"title": "AI Lab",
"properties": {
Expand Down Expand Up @@ -96,7 +108,7 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@podman-desktop/api": "1.12.0",
"@podman-desktop/api": "1.13.0-202409181313-78725a6565",
"@rollup/plugin-replace": "^6.0.1",
"@types/express": "^4.17.21",
"@types/js-yaml": "^4.0.9",
Expand Down
13 changes: 11 additions & 2 deletions packages/backend/src/managers/application/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
POD_LABEL_RECIPE_ID,
} from '../../utils/RecipeConstants';
import { VMType } from '@shared/src/models/IPodman';
import { RECIPE_START_ROUTE } from '../../registries/NavigationRegistry';

export class ApplicationManager extends Publisher<ApplicationState[]> implements Disposable {
#applications: ApplicationRegistry<ApplicationState>;
Expand Down Expand Up @@ -91,8 +92,16 @@ export class ApplicationManager extends Publisher<ApplicationState[]> implements
});

window
.withProgress({ location: ProgressLocation.TASK_WIDGET, title: `Pulling ${recipe.name}.` }, () =>
this.pullApplication(connection, recipe, model, labels),
.withProgress(
{
location: ProgressLocation.TASK_WIDGET,
title: `Pulling ${recipe.name}.`,
details: {
routeId: RECIPE_START_ROUTE,
routeArgs: [recipe.id, trackingId],
},
},
() => this.pullApplication(connection, recipe, model, labels),
)
.then(() => {
task.state = 'success';
Expand Down
136 changes: 136 additions & 0 deletions packages/backend/src/registries/NavigationRegistry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { beforeAll, afterAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { commands, navigation, type WebviewPanel, type Disposable } from '@podman-desktop/api';
import { NavigationRegistry } from './NavigationRegistry';
import { Messages } from '@shared/Messages';

vi.mock('@podman-desktop/api', async () => ({
commands: {
registerCommand: vi.fn(),
},
navigation: {
register: vi.fn(),
},
}));

const panelMock: WebviewPanel = {
reveal: vi.fn(),
webview: {
postMessage: vi.fn(),
},
} as unknown as WebviewPanel;

beforeEach(() => {
vi.resetAllMocks();
vi.restoreAllMocks();
});

describe('incompatible podman-desktop', () => {
let register: typeof navigation.register | undefined;
beforeAll(() => {
register = navigation.register;
(navigation.register as unknown as undefined) = undefined;
});

afterAll(() => {
if (!register) return;
navigation.register = register;
});

test('init should not register command and navigation when using old version of podman', () => {
(navigation.register as unknown as undefined) = undefined;
const registry = new NavigationRegistry(panelMock);
registry.init();

expect(commands.registerCommand).not.toHaveBeenCalled();
});
});

test('init should register command and navigation', () => {
const registry = new NavigationRegistry(panelMock);
registry.init();

expect(commands.registerCommand).toHaveBeenCalled();
expect(navigation.register).toHaveBeenCalled();
});

test('dispose should dispose all command and navigation registered', () => {
const registry = new NavigationRegistry(panelMock);
const disposables: Disposable[] = [];
vi.mocked(commands.registerCommand).mockImplementation(() => {
const disposable: Disposable = {
dispose: vi.fn(),
};
disposables.push(disposable);
return disposable;
});
vi.mocked(navigation.register).mockImplementation(() => {
const disposable: Disposable = {
dispose: vi.fn(),
};
disposables.push(disposable);
return disposable;
});

registry.dispose();

disposables.forEach((disposable: Disposable) => {
expect(disposable.dispose).toHaveBeenCalledOnce();
});
});

test('navigateToInferenceCreate should reveal and postMessage to webview', async () => {
const registry = new NavigationRegistry(panelMock);

await registry.navigateToInferenceCreate('dummyTrackingId');

await vi.waitFor(() => {
expect(panelMock.reveal).toHaveBeenCalledOnce();
});

expect(panelMock.webview.postMessage).toHaveBeenCalledWith({
id: Messages.MSG_NAVIGATION_ROUTE_UPDATE,
body: '/service/create?trackingId=dummyTrackingId',
});
});

test('navigateToRecipeStart should reveal and postMessage to webview', async () => {
const registry = new NavigationRegistry(panelMock);

await registry.navigateToRecipeStart('dummyRecipeId', 'dummyTrackingId');

await vi.waitFor(() => {
expect(panelMock.reveal).toHaveBeenCalledOnce();
});

expect(panelMock.webview.postMessage).toHaveBeenCalledWith({
id: Messages.MSG_NAVIGATION_ROUTE_UPDATE,
body: '/recipe/dummyRecipeId/start?trackingId=dummyTrackingId',
});
});

test('reading the route has side-effect', async () => {
const registry = new NavigationRegistry(panelMock);

await registry.navigateToRecipeStart('dummyRecipeId', 'dummyTrackingId');

expect(registry.readRoute()).toBeDefined();
expect(registry.readRoute()).toBeUndefined();
});
82 changes: 82 additions & 0 deletions packages/backend/src/registries/NavigationRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { type Disposable, navigation, type WebviewPanel, commands } from '@podman-desktop/api';
import { Messages } from '@shared/Messages';

export const RECIPE_START_ROUTE = 'recipe.start';
export const RECIPE_START_NAVIGATE_COMMAND = 'ai-lab.navigation.recipe.start';

export const INFERENCE_CREATE_ROUTE = 'inference.create';
export const INFERENCE_CREATE_NAVIGATE_COMMAND = 'ai-lab.navigation.inference.create';

export class NavigationRegistry implements Disposable {
#disposables: Disposable[] = [];
#route: string | undefined = undefined;

constructor(private panel: WebviewPanel) {}

init(): void {
if (!navigation.register) {
console.warn('this version of podman-desktop do not support task actions: some feature will not be available.');
return;
}

// register the recipes start navigation and command
this.#disposables.push(
commands.registerCommand(RECIPE_START_NAVIGATE_COMMAND, this.navigateToRecipeStart.bind(this)),
);
this.#disposables.push(navigation.register(RECIPE_START_ROUTE, RECIPE_START_NAVIGATE_COMMAND));

// register the inference create navigation and command
this.#disposables.push(
commands.registerCommand(INFERENCE_CREATE_NAVIGATE_COMMAND, this.navigateToInferenceCreate.bind(this)),
);
this.#disposables.push(navigation.register(INFERENCE_CREATE_ROUTE, INFERENCE_CREATE_NAVIGATE_COMMAND));
}

/**
* This function return the route, and reset it.
* Meaning after read the route is undefined
*/
public readRoute(): string | undefined {
const result: string | undefined = this.#route;
this.#route = undefined;
return result;
}

dispose(): void {
this.#disposables.forEach(disposable => disposable.dispose());
}

protected async updateRoute(route: string): Promise<void> {
await this.panel.webview.postMessage({
id: Messages.MSG_NAVIGATION_ROUTE_UPDATE,
body: route,
});
this.#route = route;
this.panel.reveal();
}

public async navigateToRecipeStart(recipeId: string, trackingId: string): Promise<void> {
return this.updateRoute(`/recipe/${recipeId}/start?trackingId=${trackingId}`);
}

public async navigateToInferenceCreate(trackingId: string): Promise<void> {
return this.updateRoute(`/service/create?trackingId=${trackingId}`);
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/studio-api-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import * as podman from './utils/podman';
import type { ConfigurationRegistry } from './registries/ConfigurationRegistry';
import type { RecipeManager } from './managers/recipes/RecipeManager';
import type { PodmanConnection } from './managers/podmanConnection';
import type { NavigationRegistry } from './registries/NavigationRegistry';

vi.mock('./ai.json', () => {
return {
Expand Down Expand Up @@ -158,6 +159,7 @@ beforeEach(async () => {
{} as unknown as ConfigurationRegistry,
{} as unknown as RecipeManager,
podmanConnectionMock,
{} as unknown as NavigationRegistry,
);
vi.mock('node:fs');

Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import type { RecipeManager } from './managers/recipes/RecipeManager';
import type { PodmanConnection } from './managers/podmanConnection';
import type { RecipePullOptions } from '@shared/src/models/IRecipe';
import type { ContainerProviderConnection } from '@podman-desktop/api';
import type { NavigationRegistry } from './registries/NavigationRegistry';

interface PortQuickPickItem extends podmanDesktopApi.QuickPickItem {
port: number;
Expand All @@ -75,8 +76,13 @@ export class StudioApiImpl implements StudioAPI {
private configurationRegistry: ConfigurationRegistry,
private recipeManager: RecipeManager,
private podmanConnection: PodmanConnection,
private navigationRegistry: NavigationRegistry,
) {}

async readRoute(): Promise<string | undefined> {
return this.navigationRegistry.readRoute();
}

async requestDeleteConversation(conversationId: string): Promise<void> {
// Do not wait on the promise as the api would probably timeout before the user answer.
podmanDesktopApi.window
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/studio.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ vi.mock('@podman-desktop/api', async () => {
onEvent: vi.fn(),
listContainers: mocks.listContainers,
},
navigation: {
register: vi.fn(),
},
provider: {
onDidRegisterContainerConnection: vi.fn(),
onDidUpdateContainerConnection: vi.fn(),
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { WhisperCpp } from './workers/provider/WhisperCpp';
import { ApiServer } from './managers/apiServer';
import { InstructlabManager } from './managers/instructlab/instructlabManager';
import { InstructlabApiImpl } from './instructlab-api-impl';
import { NavigationRegistry } from './registries/NavigationRegistry';

export class Studio {
readonly #extensionContext: ExtensionContext;
Expand Down Expand Up @@ -85,6 +86,7 @@ export class Studio {
#inferenceProviderRegistry: InferenceProviderRegistry | undefined;
#configurationRegistry: ConfigurationRegistry | undefined;
#gpuManager: GPUManager | undefined;
#navigationRegistry: NavigationRegistry | undefined;
#instructlabManager: InstructlabManager | undefined;

constructor(readonly extensionContext: ExtensionContext) {
Expand Down Expand Up @@ -137,6 +139,14 @@ export class Studio {
this.#telemetry?.logUsage(e.webviewPanel.visible ? 'opened' : 'closed');
});

/**
* The navigation registry is used
* to register and managed the routes of the extension
*/
this.#navigationRegistry = new NavigationRegistry(this.#panel);
this.#navigationRegistry.init();
this.#extensionContext.subscriptions.push(this.#navigationRegistry);

/**
* Cancellation token registry store the tokens used to cancel a task
*/
Expand Down Expand Up @@ -333,6 +343,7 @@ export class Studio {
this.#configurationRegistry,
this.#recipeManager,
this.#podmanConnection,
this.#navigationRegistry,
);
// Register the instance
this.#rpcExtension.registerInstance<StudioApiImpl>(StudioApiImpl, this.#studioApi);
Expand Down
Loading

0 comments on commit 4d420cb

Please sign in to comment.