diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 3fa59ec4..68ab055b 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -6,10 +6,12 @@ import Route from './lib/Route.svelte'; 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'; import DiskImageDetails from './lib/disk-image/DiskImageDetails.svelte'; +import Navigation from './Navigation.svelte'; +import DiskImagesList from './lib/disk-image/DiskImagesList.svelte'; +import Dashboard from './lib/dashboard/Dashboard.svelte'; router.mode.hash(); @@ -22,7 +24,7 @@ onMount(() => { isMounted = true; return rpcBrowser.subscribe(Messages.MSG_NAVIGATE_BUILD, (x: string) => { - router.goto(`/build/${x}`); + router.goto(`/disk-images/build/${x}`); }); }); @@ -30,16 +32,22 @@ onMount(() => {
- - + + + + - - + + - + - + + + + +
diff --git a/packages/frontend/src/Build.spec.ts b/packages/frontend/src/Build.spec.ts index 045943db..47164569 100644 --- a/packages/frontend/src/Build.spec.ts +++ b/packages/frontend/src/Build.spec.ts @@ -830,5 +830,5 @@ test('confirm successful build goes to logs', async () => { // check that clicking redirects to the build logs page expect(router.goto).not.toHaveBeenCalled(); await userEvent.click(build); - expect(router.goto).toHaveBeenCalledWith(`/details/bmFtZTE=/build`); + expect(router.goto).toHaveBeenCalledWith(`/disk-image/bmFtZTE=/build`); }); diff --git a/packages/frontend/src/Build.svelte b/packages/frontend/src/Build.svelte index a5ec127e..0045659b 100644 --- a/packages/frontend/src/Build.svelte +++ b/packages/frontend/src/Build.svelte @@ -17,6 +17,7 @@ import DiskImageIcon from './lib/DiskImageIcon.svelte'; import { Button, Input, EmptyScreen, FormPage, Checkbox, ErrorMessage } from '@podman-desktop/ui-svelte'; import Link from './lib/Link.svelte'; import { historyInfo } from '/@/stores/historyInfo'; +import { goToDiskImages } from './lib/navigation'; export let imageName: string | undefined = undefined; export let imageTag: string | undefined = undefined; @@ -262,7 +263,7 @@ async function buildBootcImage() { const found = $historyInfo.find(info => info.id === buildID); if (found) { - router.goto(`/details/${btoa(found.id)}/build`); + router.goto(`/disk-image/${btoa(found.id)}/build`); break; // Exit the loop if the build is found } @@ -422,20 +423,16 @@ $: if (availableArchitectures) { buildArch = undefined; } } - -export function goToHomePage(): void { - router.goto('/'); -} + breadcrumbTitle="Go back to disk images" + onclose={goToDiskImages} + onbreadcrumbClick={goToDiskImages}>
diff --git a/packages/frontend/src/Navigation.svelte b/packages/frontend/src/Navigation.svelte new file mode 100644 index 00000000..359d3578 --- /dev/null +++ b/packages/frontend/src/Navigation.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/frontend/src/lib/BootcActions.svelte b/packages/frontend/src/lib/BootcActions.svelte index 01ba4141..7e319b25 100644 --- a/packages/frontend/src/lib/BootcActions.svelte +++ b/packages/frontend/src/lib/BootcActions.svelte @@ -15,7 +15,7 @@ async function deleteBuild(): Promise { // Navigate to the build async function gotoLogs(): Promise { - router.goto(`/details/${btoa(object.id)}/build`); + router.goto(`/disk-image/${btoa(object.id)}/build`); } diff --git a/packages/frontend/src/lib/BootcEmptyScreen.svelte b/packages/frontend/src/lib/BootcEmptyScreen.svelte deleted file mode 100644 index cc298d09..00000000 --- a/packages/frontend/src/lib/BootcEmptyScreen.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - -
-
- -
- -
- -

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/BootcImageColumn.svelte b/packages/frontend/src/lib/BootcImageColumn.svelte index b2909176..8d10e048 100644 --- a/packages/frontend/src/lib/BootcImageColumn.svelte +++ b/packages/frontend/src/lib/BootcImageColumn.svelte @@ -5,7 +5,7 @@ import type { BootcBuildInfo } from '/@shared/src/models/bootc'; export let object: BootcBuildInfo; function openDetails() { - router.goto(`/details/${btoa(object.id)}/summary`); + router.goto(`/disk-image/${btoa(object.id)}/summary`); } diff --git a/packages/frontend/src/lib/NoBootcImagesEmptyScreen.svelte b/packages/frontend/src/lib/NoBootcImagesEmptyScreen.svelte deleted file mode 100644 index b757d416..00000000 --- a/packages/frontend/src/lib/NoBootcImagesEmptyScreen.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/frontend/src/lib/BootcEmptyScreen.spec.ts b/packages/frontend/src/lib/dashboard/Dashboard.spec.ts similarity index 70% rename from packages/frontend/src/lib/BootcEmptyScreen.spec.ts rename to packages/frontend/src/lib/dashboard/Dashboard.spec.ts index 0cd5eb1b..134d0dfd 100644 --- a/packages/frontend/src/lib/BootcEmptyScreen.spec.ts +++ b/packages/frontend/src/lib/dashboard/Dashboard.spec.ts @@ -20,8 +20,8 @@ import '@testing-library/jest-dom/vitest'; import { render, screen } from '@testing-library/svelte'; import { expect, test, vi } from 'vitest'; -import { bootcClient } from '../api/client'; -import BootcEmptyScreen from './BootcEmptyScreen.svelte'; +import { bootcClient } from '../../api/client'; +import Dashboard from './Dashboard.svelte'; import type { ImageInfo } from '@podman-desktop/api'; const exampleTestImage = `quay.io/bootc-extension/httpd:latest`; @@ -45,7 +45,7 @@ const mockBootcImages: ImageInfo[] = [ }, ]; -vi.mock('../api/client', async () => { +vi.mock('../../api/client', async () => { return { bootcClient: { listHistoryInfo: vi.fn(), @@ -62,10 +62,10 @@ vi.mock('../api/client', async () => { }; }); -test('Expect welcome screen header on empty build page', async () => { +test('Expect basic dashboard', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); - render(BootcEmptyScreen); + render(Dashboard); const noDeployments = screen.getByRole('heading', { name: 'Welcome to Bootable Containers' }); expect(noDeployments).toBeInTheDocument(); @@ -74,12 +74,14 @@ test('Expect welcome screen header on empty build page', async () => { test('Expect build image button if example image does not exist', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); vi.mocked(bootcClient.listBootcImages).mockResolvedValue(mockBootcImages); - render(BootcEmptyScreen); + render(Dashboard); - // Wait until the "Pull image" button DISSAPEARS - while (screen.queryAllByRole('button', { name: 'Pull image' }).length === 1) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + // Wait until the "Pull image" button disapears + await vi.waitFor(() => { + if (screen.queryAllByRole('button', { name: 'Pull image' }).length === 1) { + throw new Error(); + } + }); // Build image exists since there is the example image in our mocked mockBootcImages const buildImage = screen.getByRole('button', { name: 'Build image' }); @@ -89,12 +91,14 @@ test('Expect build image button if example image does not exist', async () => { test('Expect pull image button if example image does not exist', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); - render(BootcEmptyScreen); + render(Dashboard); // Wait until the "Build image" button disappears - while (screen.queryAllByRole('button', { name: 'Build image' }).length === 1) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + await vi.waitFor(() => { + if (screen.queryAllByRole('button', { name: 'Build image' }).length === 1) { + throw new Error(); + } + }); // Pull image exists since there is no image in our mocked mockBootcImages const pullImage = screen.getByRole('button', { name: 'Pull image' }); @@ -104,25 +108,9 @@ test('Expect pull image button if example image does not exist', async () => { test('Clicking on Pull image button should call bootcClient.pullImage', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); vi.mocked(bootcClient.listBootcImages).mockResolvedValue([]); - render(BootcEmptyScreen); + render(Dashboard); 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); - render(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/dashboard/Dashboard.svelte b/packages/frontend/src/lib/dashboard/Dashboard.svelte new file mode 100644 index 00000000..4bb390ea --- /dev/null +++ b/packages/frontend/src/lib/dashboard/Dashboard.svelte @@ -0,0 +1,120 @@ + + + +
+
+
+ +
+ +
+ +

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/disk-image/DiskImageDetails.svelte b/packages/frontend/src/lib/disk-image/DiskImageDetails.svelte index f7a259df..6aa95671 100644 --- a/packages/frontend/src/lib/disk-image/DiskImageDetails.svelte +++ b/packages/frontend/src/lib/disk-image/DiskImageDetails.svelte @@ -9,6 +9,7 @@ import { onMount } from 'svelte'; import type { BootcBuildInfo } from '/@shared/src/models/bootc'; import { getTabUrl, isTabSelected } from '../upstream/Util'; import { historyInfo } from '/@/stores/historyInfo'; +import { goToDiskImages } from '../navigation'; export let id: string; @@ -28,24 +29,20 @@ onMount(() => { } } else if (detailsPage) { // the disk image has been deleted - goToHomePage(); + goToDiskImages(); } }); }); - -export function goToHomePage(): void { - router.goto('/'); -} + breadcrumbTitle="Go back to disk images" + onclose={goToDiskImages} + onbreadcrumbClick={goToDiskImages}> diff --git a/packages/frontend/src/lib/NoBootcImagesEmptyScreen.spec.ts b/packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.spec.ts similarity index 73% rename from packages/frontend/src/lib/NoBootcImagesEmptyScreen.spec.ts rename to packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.spec.ts index 93ef78f1..a38f4b1f 100644 --- a/packages/frontend/src/lib/NoBootcImagesEmptyScreen.spec.ts +++ b/packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.spec.ts @@ -16,15 +16,17 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import '@testing-library/jest-dom/vitest'; import { render, screen } from '@testing-library/svelte'; import { expect, test } from 'vitest'; -import NoBootcImagesEmptyScreen from './NoBootcImagesEmptyScreen.svelte'; +import DiskImageEmptyScreen from './DiskImageEmptyScreen.svelte'; -test('Expect empty screen', async () => { - render(NoBootcImagesEmptyScreen); - const noDeployments = screen.getByRole('heading', { name: 'No Bootable Container builds have been created yet' }); - expect(noDeployments).toBeInTheDocument(); +test('Expect disk image empty screen', async () => { + render(DiskImageEmptyScreen); + const noDiskImages = screen.getByRole('heading', { name: 'No disk images' }); + expect(noDiskImages).toBeInTheDocument(); }); diff --git a/packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.svelte b/packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.svelte new file mode 100644 index 00000000..51549a63 --- /dev/null +++ b/packages/frontend/src/lib/disk-image/DiskImageEmptyScreen.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/frontend/src/Homepage.spec.ts b/packages/frontend/src/lib/disk-image/DiskImagesList.spec.ts similarity index 73% rename from packages/frontend/src/Homepage.spec.ts rename to packages/frontend/src/lib/disk-image/DiskImagesList.spec.ts index 56d2fa85..c8c7e836 100644 --- a/packages/frontend/src/Homepage.spec.ts +++ b/packages/frontend/src/lib/disk-image/DiskImagesList.spec.ts @@ -17,10 +17,10 @@ import { vi, test, expect } from 'vitest'; import { screen, render } from '@testing-library/svelte'; -import Homepage from './Homepage.svelte'; import type { BootcBuildInfo } from '/@shared/src/models/bootc'; -import { bootcClient } from './api/client'; +import { bootcClient } from '../../api/client'; import { beforeEach } from 'node:test'; +import DiskImagesList from './DiskImagesList.svelte'; const mockHistoryInfo: BootcBuildInfo[] = [ { @@ -45,7 +45,7 @@ const mockHistoryInfo: BootcBuildInfo[] = [ }, ]; -vi.mock('./api/client', async () => { +vi.mock('../../api/client', async () => { return { bootcClient: { listHistoryInfo: vi.fn(), @@ -67,25 +67,22 @@ beforeEach(() => { vi.clearAllMocks(); }); -test('Homepage renders correctly with no past builds', async () => { +test('Disk Images renders correctly with no past builds', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([]); - render(Homepage); - // No bootable container builds found should be present - // so expect the welcome page - expect(screen.queryByText('Welcome to Bootable Containers')).not.toBeNull(); + render(DiskImagesList); + expect(screen.queryByText('No disk images')).not.toBeNull(); }); test('Homepage renders correctly with multiple rows', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo); - render(Homepage); - - // Wait until header 'Welcome to Bootable Containers' is removed - // as that means it's fully loaded - while (screen.queryByText('Welcome to Bootable Containers')) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + render(DiskImagesList); + await vi.waitFor(() => { + if (!screen.queryByText('Disk Images')) { + throw new Error(); + } + }); // Name 'image1:latest' should be present expect(screen.queryByText('image1:latest')).not.toBeNull(); @@ -98,13 +95,12 @@ test('Test clicking on delete button', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo); vi.mocked(bootcClient.deleteBuilds).mockResolvedValue(await Promise.resolve()); - render(Homepage); - - // Wait until header 'Welcome to Bootable Containers' is removed - // as that means it's fully loaded - while (screen.queryByText('Welcome to Bootable Containers')) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + render(DiskImagesList); + await vi.waitFor(() => { + if (!screen.queryByText('Disk Images')) { + throw new Error(); + } + }); // spy on deleteBuild function const spyOnDelete = vi.spyOn(bootcClient, 'deleteBuilds'); @@ -119,13 +115,12 @@ test('Test clicking on delete button', async () => { test('Test clicking on build button', async () => { vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue(mockHistoryInfo); - render(Homepage); - - // Wait until header 'Welcome to Bootable Containers' is removed - // as that means it's fully loaded - while (screen.queryByText('Welcome to Bootable Containers')) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + render(DiskImagesList); + await vi.waitFor(() => { + if (!screen.queryByText('Disk Images')) { + throw new Error(); + } + }); // spy on telemetryLogUsage function const spyOnLogUsage = vi.spyOn(bootcClient, 'telemetryLogUsage'); diff --git a/packages/frontend/src/Homepage.svelte b/packages/frontend/src/lib/disk-image/DiskImagesList.svelte similarity index 80% rename from packages/frontend/src/Homepage.svelte rename to packages/frontend/src/lib/disk-image/DiskImagesList.svelte index 5624863f..7003f48a 100644 --- a/packages/frontend/src/Homepage.svelte +++ b/packages/frontend/src/lib/disk-image/DiskImagesList.svelte @@ -1,16 +1,14 @@ - + - + @@ -143,7 +138,7 @@ const row = new TableRow({ {#if $filtered.length === 0 && searchTerm} {:else if history.length === 0} - + {/if}
diff --git a/packages/frontend/src/lib/navigation.ts b/packages/frontend/src/lib/navigation.ts new file mode 100644 index 00000000..b739a2dd --- /dev/null +++ b/packages/frontend/src/lib/navigation.ts @@ -0,0 +1,28 @@ +/********************************************************************** + * 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 { router } from 'tinro'; +import { bootcClient } from '../api/client'; + +export async function goToDiskImages(): Promise { + router.goto('/disk-images'); +} + +export async function gotoBuild(): Promise { + bootcClient.telemetryLogUsage('nav-build'); + router.goto('/disk-images/build'); +}