Skip to content

Commit

Permalink
feat: adds webview build page (#180)
Browse files Browse the repository at this point in the history
### What does this PR do?

* Adds a webview build page
* Uses the last deployed bootc build as the "previous selection" for the
  page
* Added tests for the webview
* Fixed current tests identified when adding new tsconfig

### Screenshot / video of UI

<!-- If this PR is changing UI, please include
screenshots or screencasts showing the difference -->

### What issues does this PR fix or reference?

<!-- Include any related issues from Podman Desktop
repository (or from another issue tracker). -->

Closes containers#141

### How to test this PR?

<!-- Please explain steps to reproduce -->

1. `yarn watch` in this extension directory
2. `cd podman-desktop`
3. `yarn watch --extension-folder ../bootc/packages/backend`
4. Press the bootc icon on the left that should be appear, and start a
   build via the shown webview.

Signed-off-by: Charlie Drage <[email protected]>
  • Loading branch information
cdrage authored Mar 14, 2024
1 parent 3c0e449 commit c1c2a01
Show file tree
Hide file tree
Showing 50 changed files with 3,892 additions and 527 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ build
__mocks__
coverage
packages/backend/media/**
tests/
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
FROM scratch as builder
COPY packages/backend/dist/ /extension/dist
COPY packages/backend/package.json /extension/
COPY packages/backend/media/ /extension/media
COPY LICENSE /extension/
COPY packages/backend/icon-dark.png /extension/
COPY packages/backend/icon-light.png /extension/
Expand Down
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@
"npm": ">=8.19.2"
},
"scripts": {
"build": "yarn --cwd packages/backend build",
"watch": "yarn --cwd packages/backend watch",
"build": "concurrently \"yarn --cwd packages/frontend build\" \"yarn --cwd packages/backend build\"",
"watch": "concurrently \"yarn --cwd packages/frontend watch\" \"yarn --cwd packages/backend watch\"",
"format:check": "prettier --check \"**/src/**/*.{ts,svelte}\"",
"format:fix": "prettier --write \"**/src/**/*.{ts,svelte}\"",
"lint:check": "yarn --cwd packages/backend lint:check",
"lint:fix": "yarn --cwd packages/backend lint:fix",
"lint:check": "eslint . --ext js,ts,tsx",
"lint:fix": "eslint . --fix --ext js,ts,tsx",
"svelte:check": "svelte-check",
"test:backend": "vitest run -r packages/backend --passWithNoTests --coverage",
"test:unit": "npm run test:backend",
"test:frontend": "vitest run -r packages/frontend --passWithNoTests --coverage",
"test:shared": "vitest run -r packages/shared --passWithNoTests --coverage",
"test:unit": "npm run test:backend && npm run test:shared && npm run test:frontend",
"typecheck:shared": "tsc --noEmit --project packages/shared",
"typecheck:frontend": "tsc --noEmit --project packages/frontend",
"typecheck:backend": "tsc --noEmit --project packages/backend",
"typecheck": "npm run typecheck:backend"
"typecheck": "npm run typecheck:shared && npm run typecheck:frontend && npm run typecheck:backend"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
Expand Down
91 changes: 0 additions & 91 deletions packages/backend/.eslintrc.json

This file was deleted.

8 changes: 0 additions & 8 deletions packages/backend/.prettierrc

This file was deleted.

1 change: 0 additions & 1 deletion packages/backend/.yarnrc

This file was deleted.

76 changes: 76 additions & 0 deletions packages/backend/src/api-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**********************************************************************
* 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 * as podmanDesktopApi from '@podman-desktop/api';
import type { ImageInfo } from '@podman-desktop/api';
import type { BootcApi } from '/@shared/src/BootcAPI';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import { buildDiskImage } from './build-disk-image';
import { History } from './history';

export class BootcApiImpl implements BootcApi {
private history: History;

constructor(private readonly extensionContext: podmanDesktopApi.ExtensionContext) {
this.history = new History(extensionContext.storagePath);
}

async buildImage(build: BootcBuildInfo): Promise<void> {
return buildDiskImage(build, this.history);
}

async selectOutputFolder(): Promise<string> {
const path = await podmanDesktopApi.window.showOpenDialog({
title: 'Select output folder',
selectors: ['openDirectory'],
});
if (path && path.length > 0) {
return path[0].fsPath;
}
return '';
}

async listBootcImages(): Promise<ImageInfo[]> {
let images: ImageInfo[] = [];
try {
const retrieveImages = await podmanDesktopApi.containerEngine.listImages();
images = retrieveImages.filter(image => {
if (image.Labels) {
return image.Labels['bootc'] ?? image.Labels['containers.bootc'];
}
});
} catch (err) {
await podmanDesktopApi.window.showErrorMessage(`Error listing images: ${err}`);
console.error('Error listing images: ', err);
}
return images;
}

async listHistoryInfo(): Promise<BootcBuildInfo[]> {
try {
// Load the file so it retrieves the latest information.
await this.history.loadFile();
} catch (err) {
await podmanDesktopApi.window.showErrorMessage(
`Error loading history from ${this.extensionContext.storagePath}, error: ${err}`,
);
console.error('Error loading history: ', err);
}
return this.history.getHistory();
}
}
5 changes: 4 additions & 1 deletion packages/backend/src/build-disk-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ test('check image builder options', async () => {
expect(options).toBeDefined();
expect(options.name).toEqual(name);
expect(options.Image).toEqual(bootcImageBuilderName);
expect(options.HostConfig.Binds[0]).toEqual(outputFolder + ':/output/');
expect(options.HostConfig).toBeDefined();
if (options.HostConfig?.Binds) {
expect(options.HostConfig.Binds[0]).toEqual(outputFolder + ':/output/');
}
expect(options.Cmd).toEqual([image, '--type', type, '--target-arch', arch, '--output', '/output/']);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/build-disk-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import * as fs from 'node:fs';
import { resolve } from 'node:path';
import * as containerUtils from './container-utils';
import { bootcImageBuilderContainerName, bootcImageBuilderName } from './constants';
import type { BootcBuildInfo } from '@shared/src/models/bootc';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import type { History } from './history';
import * as machineUtils from './machine-utils';

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/container-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async function getVolumesMatchingContainer(engineId: string, container: string):
// Go through each volume and only retrieve the ones that match our container
// "Names" in the API weirdly has `/` appended to the beginning of the name due to how it models the podman API
// so we have to make sure / is appended to the container name for comparison..
let volumeNames = [];
let volumeNames: string[] = [];
volumes.Volumes.forEach(v => {
v.containersUsage.forEach(c => {
c.names.forEach(n => {
Expand Down Expand Up @@ -227,7 +227,7 @@ export async function removeContainerAndVolumes(engineId: string, container: str

// If we are unable to get the containers, we should still try to delete the container, so we ignore the error
// and just log the error.
let volumeNames = [];
let volumeNames: string[] = [];
try {
volumeNames = await getVolumesMatchingContainer(engineId, container);
console.log('Matching volumes: ', volumeNames);
Expand Down
18 changes: 17 additions & 1 deletion packages/backend/src/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type * as podmanDesktopApi from '@podman-desktop/api';
import { activate, deactivate } from './extension';
import * as fs from 'node:fs';

/// mock console.log
const originalConsoleLog = console.log;
Expand All @@ -34,6 +35,19 @@ vi.mock('@podman-desktop/api', async () => {
commands: {
registerCommand: vi.fn(),
},
Uri: class {
static joinPath = () => ({ fsPath: '.' });
},
window: {
createWebviewPanel: () => ({
webview: {
html: '',
onDidReceiveMessage: vi.fn(),
postMessage: vi.fn(),
},
onDidChangeViewState: vi.fn(),
}),
},
};
});

Expand All @@ -52,7 +66,9 @@ test('check activate', async () => {
push: vi.fn(),
},
} as unknown as podmanDesktopApi.ExtensionContext;

vi.spyOn(fs.promises, 'readFile').mockImplementation(() => {
return Promise.resolve('<html></html>');
});
await activate(fakeContext);

expect(consoleLogMock).toBeCalledWith('starting bootc extension');
Expand Down
72 changes: 61 additions & 11 deletions packages/backend/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import * as extensionApi from '@podman-desktop/api';
import { launchVFKit } from './launch-vfkit';
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';

export async function activate(extensionContext: ExtensionContext): Promise<void> {
console.log('starting bootc extension');
Expand All @@ -35,19 +38,66 @@ export async function activate(extensionContext: ExtensionContext): Promise<void
}),
extensionApi.commands.registerCommand('bootc.image.build', async image => {
const selections = await bootcBuildOptionSelection(history);
await buildDiskImage(
{
name: image.name,
tag: image.tag,
engineId: image.engineId,
type: selections.type,
folder: selections.folder,
arch: selections.arch,
},
history,
);
if (selections) {
await buildDiskImage(
{
name: image.name,
tag: image.tag,
engineId: image.engineId,
type: selections.type,
folder: selections.folder,
arch: selections.arch,
},
history,
);
}
}),
);

const panel = extensionApi.window.createWebviewPanel('bootc', 'Bootc', {
localResourceRoots: [extensionApi.Uri.joinPath(extensionContext.extensionUri, 'media')],
});

const indexHtmlUri = extensionApi.Uri.joinPath(extensionContext.extensionUri, 'media', 'index.html');
const indexHtmlPath = indexHtmlUri.fsPath;
let indexHtml = await fs.promises.readFile(indexHtmlPath, 'utf8');

// replace links with webView Uri links
// in the content <script type="module" crossorigin src="./index-RKnfBG18.js"></script> replace src with webview.asWebviewUri
const scriptLink = indexHtml.match(/<script.*?src="(.*?)".*?>/g);
if (scriptLink) {
scriptLink.forEach(link => {
const src = link.match(/src="(.*?)"/);
if (src) {
const webviewSrc = panel.webview.asWebviewUri(
extensionApi.Uri.joinPath(extensionContext.extensionUri, 'media', src[1]),
);
indexHtml = indexHtml.replace(src[1], webviewSrc.toString());
}
});
}

// and now replace for css file as well
const cssLink = indexHtml.match(/<link.*?href="(.*?)".*?>/g);
if (cssLink) {
cssLink.forEach(link => {
const href = link.match(/href="(.*?)"/);
if (href) {
const webviewHref = panel.webview.asWebviewUri(
extensionApi.Uri.joinPath(extensionContext.extensionUri, 'media', href[1]),
);
indexHtml = indexHtml.replace(href[1], webviewHref.toString());
}
});
}

// Update the html
panel.webview.html = indexHtml;

// Register the 'api' for the webview to communicate to the backend
const rpcExtension = new RpcExtension(panel.webview);
const bootcApi = new BootcApiImpl(extensionContext);
rpcExtension.registerInstance<BootcApiImpl>(BootcApiImpl, bootcApi);
}

export async function deactivate(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { existsSync } from 'node:fs';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import * as path from 'node:path';
import type { BootcBuildInfo } from '@shared/src/models/bootc';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';

const filename = 'history.json';

Expand Down
Loading

0 comments on commit c1c2a01

Please sign in to comment.