From 1f4508a6cc0feec3234fd811df721efd2222b76a Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Thu, 28 Mar 2024 16:04:31 -0400 Subject: [PATCH] feat: adds welcome page & pull / build buttons ### What does this PR do? * Updates the welcome / empty screen page so there is no more empty descriptions * Adds a build / pull button for a test iamge * Added API for opening an eternal link * If pulling for more than 5 seconds, show a warning ### Screenshot / video of UI ### What issues does this PR fix or reference? Closes https://github.com/containers/podman-desktop-extension-bootc/issues/150 ### How to test this PR? Go to the welcome page with no builds, click on the pull / build buttons. Signed-off-by: Charlie Drage --- packages/backend/src/api-impl.ts | 4 + packages/frontend/src/Homepage.spec.ts | 12 +- .../frontend/src/lib/BootcEmptyScreen.spec.ts | 114 ++++++++++++++++- .../frontend/src/lib/BootcEmptyScreen.svelte | 117 +++++++++++++++++- packages/frontend/src/lib/BootcSelkie.svelte | 20 +++ .../frontend/src/lib/upstream/Link.svelte | 4 +- packages/shared/src/BootcAPI.ts | 1 + 7 files changed, 254 insertions(+), 18 deletions(-) create mode 100644 packages/frontend/src/lib/BootcSelkie.svelte diff --git a/packages/backend/src/api-impl.ts b/packages/backend/src/api-impl.ts index a09573e3..23309aab 100644 --- a/packages/backend/src/api-impl.ts +++ b/packages/backend/src/api-impl.ts @@ -120,6 +120,10 @@ export class BootcApiImpl implements BootcApi { return await podmanDesktopApi.env.openExternal(podmanDesktopApi.Uri.file(folder)); } + async openLink(link: string): Promise { + await podmanDesktopApi.env.openExternal(podmanDesktopApi.Uri.parse(link)); + } + async generateUniqueBuildID(name: string): Promise { return this.history.getUnusedHistoryName(name); } diff --git a/packages/frontend/src/Homepage.spec.ts b/packages/frontend/src/Homepage.spec.ts index 85fb0235..04a45527 100644 --- a/packages/frontend/src/Homepage.spec.ts +++ b/packages/frontend/src/Homepage.spec.ts @@ -47,6 +47,7 @@ vi.mock('./api/client', async () => { return { bootcClient: { listHistoryInfo: vi.fn(), + listBootcImages: vi.fn(), deleteBuilds: vi.fn(), }, rpcBrowser: { @@ -77,7 +78,8 @@ test('Homepage renders correctly with no past builds', async () => { await waitRender(Homepage); // No bootable container builds found should be present - expect(screen.queryByText('No bootable container builds found')).not.toBeNull(); + // so expect the welcome page + expect(screen.queryByText('Welcome to Bootable Containers')).not.toBeNull(); }); test('Homepage renders correctly with multiple rows', async () => { @@ -85,9 +87,9 @@ test('Homepage renders correctly with multiple rows', async () => { await waitRender(Homepage); - // Wait until header 'No bootable container builds found' is removed + // Wait until header 'Welcome to Bootable Containers' is removed // as that means it's fully loaded - while (screen.queryByText('No bootable container builds found')) { + while (screen.queryByText('Welcome to Bootable Containers')) { await new Promise(resolve => setTimeout(resolve, 100)); } @@ -104,9 +106,9 @@ test('Test clicking on delete button', async () => { await waitRender(Homepage); - // Wait until header 'No bootable container builds found' is removed + // Wait until header 'Welcome to Bootable Containers' is removed // as that means it's fully loaded - while (screen.queryByText('No bootable container builds found')) { + while (screen.queryByText('Welcome to Bootable Containers')) { await new Promise(resolve => setTimeout(resolve, 100)); } diff --git a/packages/frontend/src/lib/BootcEmptyScreen.spec.ts b/packages/frontend/src/lib/BootcEmptyScreen.spec.ts index 31e4feb4..71399426 100644 --- a/packages/frontend/src/lib/BootcEmptyScreen.spec.ts +++ b/packages/frontend/src/lib/BootcEmptyScreen.spec.ts @@ -21,12 +21,116 @@ import '@testing-library/jest-dom/vitest'; import { render, screen } from '@testing-library/svelte'; -import { expect, test } from 'vitest'; - +import { expect, test, vi } from 'vitest'; +import { bootcClient } from '../api/client'; import BootcEmptyScreen from './BootcEmptyScreen.svelte'; +import type { ImageInfo } from '@podman-desktop/api'; + +const exampleTestImage = `quay.io/bootc-extension/httpd:latest`; + +const mockBootcImages: ImageInfo[] = [ + { + Id: 'quay.io/bootc-extension/httpd', + RepoTags: [exampleTestImage], + Labels: { + bootc: 'true', + }, + engineId: 'engine1', + engineName: 'engine1', + ParentId: 'parent1', + Created: 0, + VirtualSize: 0, + Size: 0, + Containers: 0, + SharedSize: 0, + }, +]; + +vi.mock('../api/client', async () => { + return { + bootcClient: { + listHistoryInfo: vi.fn(), + listBootcImages: vi.fn(), + pullImage: vi.fn(), + }, + rpcBrowser: { + subscribe: () => { + return { + unsubscribe: () => {}, + }; + }, + }, + }; +}); + +async function waitRender(customProperties: object): Promise { + const result = render(BootcEmptyScreen, { ...customProperties }); + // wait that result.component.$$.ctx[2] is set + while (result.component.$$.ctx[2] === undefined) { + await new Promise(resolve => setTimeout(resolve, 100)); + } +} -test('Expect empty screen', async () => { - render(BootcEmptyScreen); - const noDeployments = screen.getByRole('heading', { name: 'No bootable container builds found' }); +test('Expect welcome screen header on empty build page', async () => { + vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); + vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); + await waitRender(BootcEmptyScreen); + const noDeployments = screen.getByRole('heading', { name: 'Welcome to Bootable Containers' }); expect(noDeployments).toBeInTheDocument(); }); + +test('Expect build image button if example image does not exist', async () => { + vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); + vi.mocked(bootcClient.listBootcImages).mockResolvedValue(mockBootcImages); + await waitRender(BootcEmptyScreen); + + // Wait until the "Pull image" button DISSAPEARS + while (screen.queryAllByRole('button', { name: 'Pull image' }).length === 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Build image exists since there is the example image in our mocked mockBootcImages + const buildImage = screen.getByRole('button', { name: 'Build image' }); + expect(buildImage).toBeInTheDocument(); +}); + +test('Expect pull image button if example image does not exist', async () => { + vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); + vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); + await waitRender(BootcEmptyScreen); + + // Wait until the "Build image" button disappears + while (screen.queryAllByRole('button', { name: 'Build image' }).length === 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Pull image exists since there is no image in our mocked mockBootcImages + const pullImage = screen.getByRole('button', { name: 'Pull image' }); + expect(pullImage).toBeInTheDocument(); +}); + +test('Clicking on Pull image button should call bootcClient.pullImage', async () => { + vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); + vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); + await waitRender(BootcEmptyScreen); + + const pullImage = screen.getByRole('button', { name: 'Pull image' }); + pullImage.click(); + expect(bootcClient.pullImage).toHaveBeenCalled(); +}); + +test('Clicking on Build image button should navigate to the build page', async () => { + vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); + vi.mocked(bootcClient.listBootcImages).mockResolvedValue(mockBootcImages); + await waitRender(BootcEmptyScreen); + + // Wait until the "Pull image" button disappears + while (screen.queryAllByRole('button', { name: 'Pull image' }).length === 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + const buildImage = screen.getByRole('button', { name: 'Build image' }); + buildImage.click(); + const [image, tag] = exampleTestImage.split(':'); + expect(window.location.href).toContain(`/build/${encodeURIComponent(image)}/${encodeURIComponent(tag)}`); +}); diff --git a/packages/frontend/src/lib/BootcEmptyScreen.svelte b/packages/frontend/src/lib/BootcEmptyScreen.svelte index 9df13ff4..78cebd58 100644 --- a/packages/frontend/src/lib/BootcEmptyScreen.svelte +++ b/packages/frontend/src/lib/BootcEmptyScreen.svelte @@ -1,9 +1,114 @@ - +
+
+ +
+ +
+ +

Welcome to Bootable Containers

+ +

+ Bootable Containers builds an entire bootable OS from your container image. Utilizing the technology of a + compatible image, bootc-image-builder, and bootc, your container image is transformed into a bootable disk + image. +

+ +

+ Create your first disk image by {imageExists ? 'building' : 'pulling'} the example container image: +

+ + + {#if imageExists} + + {:else} + + {/if} + {#if displayDisclaimer} +

The file size of the image is over 1.5GB and may take a while to download.

+ {/if} + +

+ Want to learn more including building your own Containerfile? Check out the extension documentation. +

+
+
diff --git a/packages/frontend/src/lib/BootcSelkie.svelte b/packages/frontend/src/lib/BootcSelkie.svelte new file mode 100644 index 00000000..a6add4c2 --- /dev/null +++ b/packages/frontend/src/lib/BootcSelkie.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/frontend/src/lib/upstream/Link.svelte b/packages/frontend/src/lib/upstream/Link.svelte index 71b02e93..257acc3d 100644 --- a/packages/frontend/src/lib/upstream/Link.svelte +++ b/packages/frontend/src/lib/upstream/Link.svelte @@ -2,6 +2,7 @@ import { onMount, createEventDispatcher } from 'svelte'; import Fa from 'svelte-fa'; import { router } from 'tinro'; +import { bootcClient } from '/@/api/client'; export let internalRef: string | undefined = undefined; export let externalRef: string | undefined = undefined; @@ -23,8 +24,7 @@ function click() { if (internalRef) { router.goto(internalRef); } else if (externalRef) { - // TODO: Does not work at the moment - //window.openExternal(externalRef); + bootcClient.openLink(externalRef); } else { dispatch('click'); } diff --git a/packages/shared/src/BootcAPI.ts b/packages/shared/src/BootcAPI.ts index 8b6bf58a..9983304e 100644 --- a/packages/shared/src/BootcAPI.ts +++ b/packages/shared/src/BootcAPI.ts @@ -28,4 +28,5 @@ export abstract class BootcApi { abstract listHistoryInfo(): Promise; abstract openFolder(folder: string): Promise; abstract generateUniqueBuildID(name: string): Promise; + abstract openLink(link: string): Promise; }