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

feat: open webview from image action #235

Merged
merged 4 commits into from
Mar 28, 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
24 changes: 22 additions & 2 deletions packages/backend/src/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type * as podmanDesktopApi from '@podman-desktop/api';
import { activate, deactivate } from './extension';
import * as podmanDesktopApi from '@podman-desktop/api';
import { activate, deactivate, openBuildPage } from './extension';
import * as fs from 'node:fs';
import os from 'node:os';

Expand Down Expand Up @@ -48,6 +48,10 @@ vi.mock('@podman-desktop/api', async () => {
},
onDidChangeViewState: vi.fn(),
}),
listWebviews: vi.fn().mockReturnValue([{ viewType: 'a' }, { id: 'test', viewType: 'bootc' }, { viewType: 'b' }]),
},
navigation: {
navigateToWebview: vi.fn(),
},
fs: {
createFileSystemWatcher: () => ({
Expand Down Expand Up @@ -89,3 +93,19 @@ test('check deactivate', async () => {

expect(consoleLogMock).toBeCalledWith('stopping bootc extension');
});

test('check command triggers webview and redirects', async () => {
const postMessageMock = vi.fn();
const panel = {
webview: {
postMessage: postMessageMock,
},
} as unknown as podmanDesktopApi.WebviewPanel;

const image = { name: 'build', tag: 'latest' };

await openBuildPage(panel, image);

expect(podmanDesktopApi.navigation.navigateToWebview).toHaveBeenCalled();
expect(postMessageMock).toHaveBeenCalledWith({ body: 'build/latest', id: 'navigate-build' });
});
60 changes: 33 additions & 27 deletions packages/backend/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,20 @@

import type { ExtensionContext } from '@podman-desktop/api';
import * as extensionApi from '@podman-desktop/api';
import { buildDiskImage } from './build-disk-image';
import { History } from './history';
import fs from 'node:fs';
import { bootcBuildOptionSelection } from './quickpicks';
import { RpcExtension } from '/@shared/src/messages/MessageProxy';
import { BootcApiImpl } from './api-impl';
import { HistoryNotifier } from './history/historyNotifier';
import type { BuildType } from '/@shared/src/models/bootc';
import { Messages } from '/@shared/src/messages/Messages';

export async function activate(extensionContext: ExtensionContext): Promise<void> {
console.log('starting bootc extension');

const history = new History(extensionContext.storagePath);
await history.loadFile();

extensionContext.subscriptions.push(
extensionApi.commands.registerCommand('bootc.image.build', async image => {
const selections = await bootcBuildOptionSelection(history);
if (selections) {
// Get a unique name for the build
const name = await history.getUnusedHistoryName(image.name);

await buildDiskImage(
{
id: name,
image: image.name,
tag: image.tag,
engineId: image.engineId,
type: [selections.type as BuildType],
folder: selections.folder,
arch: selections.arch,
},
history,
);
}
}),
);

const panel = extensionApi.window.createWebviewPanel('bootc', 'Bootc', {
const panel = extensionApi.window.createWebviewPanel('bootc', 'Bootable Containers', {
localResourceRoots: [extensionApi.Uri.joinPath(extensionContext.extensionUri, 'media')],
});
extensionContext.subscriptions.push(panel);
Expand Down Expand Up @@ -106,6 +81,37 @@ export async function activate(extensionContext: ExtensionContext): Promise<void
// so the frontend can be notified when the history changes and so we can update the UI / call listHistoryInfo
const historyNotifier = new HistoryNotifier(panel.webview, extensionContext.storagePath);
extensionContext.subscriptions.push(historyNotifier);

extensionContext.subscriptions.push(
extensionApi.commands.registerCommand('bootc.image.build', async image => {
await openBuildPage(panel, image);
}),
);
}

export async function openBuildPage(
panel: extensionApi.WebviewPanel,
image: { name: string; tag: string },
): Promise<void> {
// this should use webview reveal function in the future
const webviews = extensionApi.window.listWebviews();
const bootcWebView = (await webviews).find(webview => webview.viewType === 'bootc');

if (!bootcWebView) {
console.error('Could not find bootc webview');
return;
}

await extensionApi.navigation.navigateToWebview(bootcWebView.id);

// if we trigger immediately, the webview hasn't loaded yet and can't redirect
// if we trigger too slow, there's a visible flash as the homepage appears first
await new Promise(r => setTimeout(r, 100));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunate that we're unable to figure out if the extension has loaded yet before switching to the web view.

Has there been a solution for this within ai studio or podman desktop / they've encountered the same thing when redirecting after loading the web view?

@feloy @axel7083 @lstocchi


await panel.webview.postMessage({
id: Messages.MSG_NAVIGATE_BUILD,
body: encodeURIComponent(image.name) + '/' + encodeURIComponent(image.tag),
});
}

export async function deactivate(): Promise<void> {
Expand Down
100 changes: 0 additions & 100 deletions packages/backend/src/quickpicks.spec.ts

This file was deleted.

74 changes: 0 additions & 74 deletions packages/backend/src/quickpicks.ts

This file was deleted.

9 changes: 9 additions & 0 deletions packages/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Build from './Build.svelte';
import { onMount } from 'svelte';
import { getRouterState } from './api/client';
import Homepage from './Homepage.svelte';
import { rpcBrowser } from '/@/api/client';
import { Messages } from '/@shared/src/messages/Messages';

router.mode.hash();

Expand All @@ -17,6 +19,10 @@ onMount(() => {
const state = getRouterState();
router.goto(state.url);
isMounted = true;

return rpcBrowser.subscribe(Messages.MSG_NAVIGATE_BUILD, (x: string) => {
router.goto(`/build/${x}`);
});
});
</script>

Expand All @@ -29,6 +35,9 @@ onMount(() => {
<Route path="/build" breadcrumb="Build">
<Build />
</Route>
<Route path="/build/:name/:tag" breadcrumb="Build" let:meta>
<Build imageName="{decodeURIComponent(meta.params.name)}" imageTag="{decodeURIComponent(meta.params.tag)}" />
</Route>
</div>
</main>
</Route>
29 changes: 26 additions & 3 deletions packages/frontend/src/Build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ vi.mock('./api/client', async () => {
};
});

async function waitRender(customProperties: object): Promise<void> {
async function waitRender(customProperties?: object): Promise<void> {
const result = render(Build, { ...customProperties });
// wait that result.component.$$.ctx[2] is set
while (result.component.$$.ctx[2] === undefined) {
Expand All @@ -105,7 +105,7 @@ async function waitRender(customProperties: object): Promise<void> {
test('Render shows correct images and history', async () => {
vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo);
vi.mocked(bootcClient.listBootcImages).mockResolvedValue(mockBootcImages);
await waitRender(Build);
await waitRender();

// Wait until children length is 2 meaning it's fully rendered / propagated the changes
while (screen.getByLabelText('image-select')?.children.length !== 2) {
Expand Down Expand Up @@ -139,7 +139,30 @@ test('Render shows correct images and history', async () => {
});

test('Check that VMDK option is there', async () => {
await waitRender(Build);
await waitRender();
const vmdk = screen.getByLabelText('vmdk-select');
expect(vmdk).toBeDefined();
});

test('Check that preselecting an image works', async () => {
vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo);
vi.mocked(bootcClient.listBootcImages).mockResolvedValue(mockBootcImages);
await waitRender({ imageName: 'image2', imageTag: 'latest' });

// Wait until children length is 2 meaning it's fully rendered / propagated the changes
while (screen.getByLabelText('image-select')?.children.length !== 2) {
await new Promise(resolve => setTimeout(resolve, 100));
}
const select = screen.getByLabelText('image-select') as HTMLSelectElement;
expect(select).toBeDefined();
expect(select.children.length).toEqual(2);

// Expect image:1 to be first since it's the last one in the history
expect(select.children[0].textContent).toEqual('image1:latest');
expect(select.children[1].textContent).toEqual('image2:latest');

// Expect the one we passed in to be selected
const selectedImage = select.value as unknown as any[];
expect(selectedImage).toBeDefined();
expect(selectedImage).toEqual('image2:latest');
});
Loading