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: adds welcome page & pull / build buttons #241

Merged
merged 1 commit 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
4 changes: 4 additions & 0 deletions packages/backend/src/api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export class BootcApiImpl implements BootcApi {
return await podmanDesktopApi.env.openExternal(podmanDesktopApi.Uri.file(folder));
}

async openLink(link: string): Promise<void> {
await podmanDesktopApi.env.openExternal(podmanDesktopApi.Uri.parse(link));
}

async generateUniqueBuildID(name: string): Promise<string> {
return this.history.getUnusedHistoryName(name);
}
Expand Down
12 changes: 7 additions & 5 deletions packages/frontend/src/Homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ vi.mock('./api/client', async () => {
return {
bootcClient: {
listHistoryInfo: vi.fn(),
listBootcImages: vi.fn(),
deleteBuilds: vi.fn(),
},
rpcBrowser: {
Expand Down Expand Up @@ -77,17 +78,18 @@ 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 () => {
vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo);

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));
}

Expand All @@ -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));
}

Expand Down
114 changes: 109 additions & 5 deletions packages/frontend/src/lib/BootcEmptyScreen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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)}`);
});
117 changes: 111 additions & 6 deletions packages/frontend/src/lib/BootcEmptyScreen.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,114 @@
<script lang="ts">
import EmptyScreen from './upstream/EmptyScreen.svelte';
import BootcIcon from './BootcIcon.svelte';
import BootcSelkie from './BootcSelkie.svelte';
import Link from './upstream/Link.svelte';
import { faArrowCircleDown, faCube } from '@fortawesome/free-solid-svg-icons';
import Button from './upstream/Button.svelte';
import { onMount, tick } from 'svelte';
import { bootcClient, rpcBrowser } from '../api/client';
import { Messages } from '/@shared/src/messages/Messages';
import { router } from 'tinro';

let pullInProgress = false;
let imageExists = false;
let displayDisclaimer = false;
let bootcAvailableImages: any[] = [];

const exampleImage = 'quay.io/bootc-extension/httpd:latest';
const bootcImageBuilderSite = 'https://github.com/osbuild/bootc-image-builder';
const bootcSite = 'https://containers.github.io/bootc/';
const centosBootcSite = 'https://github.com/CentOS/centos-bootc';
const extensionSite = 'https://github.com/containers/podman-desktop-extension-bootc';

async function gotoBuild(): Promise<void> {
// Split the image name to get the image name and tag
// and go to /build/:image/:tag
// this will pre-select the image and tag in the build screen
const [image, tag] = exampleImage.split(':');
router.goto(`/build/${encodeURIComponent(image)}/${encodeURIComponent(tag)}`);
}

async function pullExampleImage() {
pullInProgress = true;
displayDisclaimer = false;
bootcClient.pullImage(exampleImage);

// After 5 seconds, check if pull is still in progress and display disclaimer if true
setTimeout(async () => {
if (pullInProgress) {
displayDisclaimer = true;
await tick(); // Ensure UI updates to reflect the new state
}
}, 5000);
}

onMount(async () => {
bootcAvailableImages = await bootcClient.listBootcImages();

return rpcBrowser.subscribe(Messages.MSG_IMAGE_PULL_UPDATE, async msg => {
if (msg.image === exampleImage) {
pullInProgress = !msg.success;
if (!pullInProgress) {
displayDisclaimer = false; // Ensure disclaimer is removed when not in progress
}
}
// Update the list of available images after a successful pull
if (msg.success) {
bootcAvailableImages = await bootcClient.listBootcImages();
}
});
});

// Each time bootcAvailableImages updates, check if 'quay.io/bootc-extension/httpd' is in RepoTags
$: {
if (bootcAvailableImages && bootcAvailableImages.some(image => image.RepoTags.includes(exampleImage))) {
imageExists = true;
}
}
</script>

<EmptyScreen
icon="{BootcIcon}"
title="No bootable container builds found"
message="Start your first build by clicking the 'Build' button" />
<div class="flex w-full h-full justify-center items-center">
<div class="flex flex-col h-full items-center text-center space-y-3">
<!-- Bootable Container Icon -->
<div class="text-gray-700 py-2">
<svelte:component this="{BootcSelkie}" size="120" />
</div>

<h1 class="text-xl pb-4">Welcome to Bootable Containers</h1>

<p class="text-gray-700 pb-4 max-w-xl">
Bootable Containers builds an entire bootable OS from your container image. Utilizing the technology of a
<Link externalRef="{centosBootcSite}">compatible image</Link>, <Link externalRef="{bootcImageBuilderSite}"
>bootc-image-builder</Link
>, and <Link externalRef="{bootcSite}">bootc</Link>, your container image is transformed into a bootable disk
image.
</p>

<p class="text-gray-700 pb-1 max-w-xl">
Create your first disk image by {imageExists ? 'building' : 'pulling'} the <Link
externalRef="{`https://${exampleImage}`}">example container image</Link
>:
</p>

<!-- Build / pull buttons -->
{#if imageExists}
<Button on:click="{() => gotoBuild()}" icon="{faCube}" aria-label="Build image" title="Build"
>Build {exampleImage}</Button>
{:else}
<Button
on:click="{() => pullExampleImage()}"
icon="{faArrowCircleDown}"
inProgress="{pullInProgress}"
aria-label="Pull image"
title="Pull image">Pull {exampleImage}</Button>
{/if}
{#if displayDisclaimer}
<p class="text-amber-500 text-xs">The file size of the image is over 1.5GB and may take a while to download.</p>
{/if}

<p class="text-gray-700 pt-8 max-w-xl">
Want to learn more including building your own Containerfile? Check out the <Link externalRef="{extensionSite}"
>extension documentation</Link
deboer-tim marked this conversation as resolved.
Show resolved Hide resolved
>.
</p>
</div>
</div>
20 changes: 20 additions & 0 deletions packages/frontend/src/lib/BootcSelkie.svelte

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/frontend/src/lib/upstream/Link.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,8 +24,7 @@ function click() {
if (internalRef) {
router.goto(internalRef);
} else if (externalRef) {
// TODO: Does not work at the moment
deboer-tim marked this conversation as resolved.
Show resolved Hide resolved
//window.openExternal(externalRef);
bootcClient.openLink(externalRef);
} else {
dispatch('click');
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/BootcAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export abstract class BootcApi {
abstract listHistoryInfo(): Promise<BootcBuildInfo[]>;
abstract openFolder(folder: string): Promise<boolean>;
abstract generateUniqueBuildID(name: string): Promise<string>;
abstract openLink(link: string): Promise<void>;
}