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: details page for disk images #866

Merged
merged 2 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 3 additions & 5 deletions packages/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getRouterState } from './api/client';
import Homepage from './Homepage.svelte';
import { rpcBrowser } from '/@/api/client';
import { Messages } from '/@shared/src/messages/Messages';
import Logs from './Logs.svelte';
import DiskImageDetails from './lib/disk-image/DiskImageDetails.svelte';

router.mode.hash();

Expand All @@ -36,10 +36,8 @@ onMount(() => {
<Route path="/build" breadcrumb="Build">
<Build />
</Route>
<Route path="/logs/:base64BuildImageName/:base64FolderLocation" breadcrumb="Logs" let:meta>
<Logs
base64BuildImageName={meta.params.base64BuildImageName}
base64FolderLocation={meta.params.base64FolderLocation} />
<Route path="/details/:id/*" breadcrumb="Disk Image Details" let:meta>
<DiskImageDetails id={meta.params.id} />
</Route>
<Route path="/build/:name/:tag" breadcrumb="Build" let:meta>
<Build imageName={decodeURIComponent(meta.params.name)} imageTag={decodeURIComponent(meta.params.tag)} />
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/lib/BootcActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ test('Test clicking on logs button', async () => {
const logsButton = screen.getAllByRole('button', { name: 'Build Logs' })[0];
logsButton.click();

expect(window.location.href).toContain('/logs');
expect(window.location.href).toContain('/build');
});
5 changes: 1 addition & 4 deletions packages/frontend/src/lib/BootcActions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ async function deleteBuild(): Promise<void> {

// Navigate to the build
async function gotoLogs(): Promise<void> {
// Convert object.folder to base64
const base64FolderLocation = btoa(object.folder);
const base64BuildImageName = btoa(object.image);
router.goto(`/logs/${base64BuildImageName}/${base64FolderLocation}`);
router.goto(`/details/${btoa(object.id)}/build`);
}
</script>

Expand Down
11 changes: 11 additions & 0 deletions packages/frontend/src/lib/BootcImageColumn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ test('Expect to render as name:tag', async () => {
const name = screen.getByText('image1:latest');
expect(name).not.toBeNull();
});

test('Expect click goes to details page', async () => {
render(BootcImageColumn, { object: mockHistoryInfo });

const name = screen.getByText('image1:latest');
expect(name).not.toBeNull();

name.click();

expect(window.location.href).toContain('/summary');
});
13 changes: 10 additions & 3 deletions packages/frontend/src/lib/BootcImageColumn.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
<script lang="ts">
import { router } from 'tinro';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';

export let object: BootcBuildInfo;

function openDetails() {
router.goto(`/details/${btoa(object.id)}/summary`);
}
</script>

<div class="text-[var(--pd-table-body-text-highlight)] overflow-hidden text-ellipsis">
{object.image}:{object.tag}
</div>
<button class="hover:cursor-pointer flex flex-col max-w-full" on:click={() => openDetails()}>
<div class="text-[var(--pd-table-body-text-highlight)] max-w-full overflow-hidden text-ellipsis">
{object.image}:{object.tag}
</div>
</button>
66 changes: 66 additions & 0 deletions packages/frontend/src/lib/disk-image/DiskImageDetails.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**********************************************************************
* 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 '@testing-library/jest-dom/vitest';

import { render, screen } from '@testing-library/svelte';
import { beforeEach, expect, test, vi } from 'vitest';
import { bootcClient } from '/@/api/client';

import DiskImageDetails from './DiskImageDetails.svelte';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import { tick } from 'svelte';

const image: BootcBuildInfo = {
id: 'id1',
image: 'my-image',
imageId: 'image-id',
tag: 'latest',
engineId: 'podman',
type: ['ami'],
folder: '/bootc',
};

vi.mock('/@/api/client', async () => {
return {
bootcClient: {
listHistoryInfo: vi.fn(),
},
rpcBrowser: {
subscribe: () => {
return {
unsubscribe: () => {},
};
},
},
};
});

beforeEach(() => {
vi.clearAllMocks();
});

test('Confirm renders disk image details', async () => {
vi.mocked(bootcClient.listHistoryInfo).mockResolvedValue([image]);

render(DiskImageDetails, { id: btoa(image.id) });

// allow UI time to update
await tick();

expect(screen.getByText(image.image + ':' + image.tag)).toBeInTheDocument();
});
65 changes: 65 additions & 0 deletions packages/frontend/src/lib/disk-image/DiskImageDetails.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts">
import { DetailsPage, Tab } from '@podman-desktop/ui-svelte';
import { router } from 'tinro';
import DiskImageIcon from '/@/lib/DiskImageIcon.svelte';
import DiskImageDetailsBuild from './DiskImageDetailsBuild.svelte';
import Route from '../Route.svelte';
import DiskImageDetailsSummary from './DiskImageDetailsSummary.svelte';
import { onMount } from 'svelte';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import { getTabUrl, isTabSelected } from '../upstream/Util';
import { historyInfo } from '/@/stores/historyInfo';

export let id: string;

let diskImage: BootcBuildInfo;

let detailsPage: DetailsPage;

onMount(() => {
const actualId = atob(id);
console.log('id: ' + actualId);
console.log('hist: ' + historyInfo.subscribe.length);

Choose a reason for hiding this comment

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

Suggested change
console.log('id: ' + actualId);
console.log('hist: ' + historyInfo.subscribe.length);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

return historyInfo.subscribe(value => {
const matchingImage = value.find(image => image.id === actualId);
console.log('match: ' + matchingImage?.id);

Choose a reason for hiding this comment

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

Suggested change
console.log('match: ' + matchingImage?.id);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

if (matchingImage) {
try {
diskImage = matchingImage;
} catch (err) {
console.error(err);
}
} else if (detailsPage) {
// the disk image has been deleted
goToHomePage();
}
});
});

export function goToHomePage(): void {
router.goto('/');
}
</script>

<DetailsPage
bind:this={detailsPage}
title="{diskImage?.image}:{diskImage?.tag}"
breadcrumbLeftPart="Bootable Containers"
breadcrumbRightPart="Disk Image Details"
breadcrumbTitle="Go back to homepage"
onclose={goToHomePage}
onbreadcrumbClick={goToHomePage}>
<DiskImageIcon slot="icon" size="30px" />
<svelte:fragment slot="tabs">
<Tab title="Summary" selected={isTabSelected($router.path, 'summary')} url={getTabUrl($router.path, 'summary')} />
<Tab title="Build Log" selected={isTabSelected($router.path, 'build')} url={getTabUrl($router.path, 'build')} />
</svelte:fragment>
<svelte:fragment slot="content">
<Route path="/summary" breadcrumb="Summary">
<DiskImageDetailsSummary image={diskImage} />
</Route>
<Route path="/build" breadcrumb="Build Log">
<DiskImageDetailsBuild folder={diskImage?.folder} />
</Route>
</svelte:fragment>
</DetailsPage>
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@

import { render, screen, waitFor } from '@testing-library/svelte';
import { vi, test, expect, beforeAll } from 'vitest';
import Logs from './Logs.svelte';
import { bootcClient } from './api/client';
import DiskImageDetailsBuild from './DiskImageDetailsBuild.svelte';
import { bootcClient } from '/@/api/client';

vi.mock('./api/client', async () => ({
vi.mock('/@/api/client', async () => ({
bootcClient: {
loadLogsFromFolder: vi.fn(),
getConfigurationValue: vi.fn(),
},
rpcBrowser: {
subscribe: () => {
return {
unsubscribe: () => {},
};
},
},
}));

beforeAll(() => {
Expand Down Expand Up @@ -59,10 +66,8 @@ test('Render logs and terminal setup', async () => {
vi.mocked(bootcClient.loadLogsFromFolder).mockResolvedValue(mockLogs);
vi.mocked(bootcClient.getConfigurationValue).mockResolvedValue(14);

const base64FolderLocation = btoa('/path/to/logs');
const base64BuildImageName = btoa('test-image');

render(Logs, { base64FolderLocation, base64BuildImageName });
const folderLocation = '/path/to/logs';
render(DiskImageDetailsBuild, { folder: folderLocation });

// Wait for the logs to be shown
await waitFor(() => {
Expand All @@ -77,12 +82,10 @@ test('Handles empty logs correctly', async () => {
vi.mocked(bootcClient.loadLogsFromFolder).mockResolvedValue('');
vi.mocked(bootcClient.getConfigurationValue).mockResolvedValue(14);

const base64FolderLocation = btoa('/empty/logs');
const base64BuildImageName = btoa('empty-image');

render(Logs, { base64FolderLocation, base64BuildImageName });
const folderLocation = '/empty/logs';
render(DiskImageDetailsBuild, { folder: folderLocation });

// Verify no logs message is displayed when logs are empty
const emptyMessage = await screen.findByText(/Unable to read image-build.log file from \/empty\/logs/);
const emptyMessage = await screen.findByText('Unable to read image-build.log file from /empty/logs');
expect(emptyMessage).toBeDefined();
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
<script lang="ts">
import '@xterm/xterm/css/xterm.css';

import { DetailsPage, EmptyScreen, FormPage } from '@podman-desktop/ui-svelte';
import { EmptyScreen } from '@podman-desktop/ui-svelte';
import { FitAddon } from '@xterm/addon-fit';
import { Terminal } from '@xterm/xterm';
import { onDestroy, onMount } from 'svelte';
import { router } from 'tinro';
import DiskImageIcon from './lib/DiskImageIcon.svelte';
import { bootcClient } from './api/client';
import { getTerminalTheme } from './lib/upstream/terminal-theme';
import { bootcClient } from '/@/api/client';
import { getTerminalTheme } from '/@/lib/upstream/terminal-theme';

export let base64FolderLocation: string;
export let base64BuildImageName: string;

// Decode the base64 folder location to a normal string path
const folderLocation = atob(base64FolderLocation);
const buildImageName = atob(base64BuildImageName);
export let folder: string | undefined;

// Log
let logsXtermDiv: HTMLDivElement;
Expand All @@ -31,7 +25,11 @@ let logsTerminal: Terminal;
let logInterval: NodeJS.Timeout;

async function fetchFolderLogs() {
const logs = await bootcClient.loadLogsFromFolder(folderLocation);
if (!folder) {
return;
}

const logs = await bootcClient.loadLogsFromFolder(folder);

// We will write only the new logs to the terminal,
// this is a simple way of updating the logs as we update it by calling the function
Expand Down Expand Up @@ -110,27 +108,16 @@ export function goToHomePage(): void {
}
</script>

<DetailsPage
title="{buildImageName} build logs"
breadcrumbLeftPart="Bootable Containers"
breadcrumbRightPart="{buildImageName} build logs"
breadcrumbTitle="Go back to homepage"
onclose={goToHomePage}
onbreadcrumbClick={goToHomePage}>
<DiskImageIcon slot="icon" size="30px" />
<svelte:fragment slot="content">
<EmptyScreen
icon={undefined}
title="No log file"
message="Unable to read image-build.log file from {folderLocation}"
hidden={noLogs === false} />

<div
class="min-w-full flex flex-col"
class:invisible={noLogs === true}
class:h-0={noLogs === true}
class:h-full={noLogs === false}
bind:this={logsXtermDiv}>
</div>
</svelte:fragment>
</DetailsPage>
<EmptyScreen
icon={undefined}
title="No log file"
message="Unable to read image-build.log file from {folder}"
hidden={noLogs === false} />

<div
class="min-w-full flex flex-col p-[5px] pr-0 bg-[var(--pd-terminal-background)]"
class:invisible={noLogs === true}
class:h-0={noLogs === true}
class:h-full={noLogs === false}
bind:this={logsXtermDiv}>
</div>
Loading