Skip to content

Commit

Permalink
feat: add example details page
Browse files Browse the repository at this point in the history
### What does this PR do?

* Adds clickable example details pages
* Tests for said pages
* Clickable link to source repo

### 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 podman-desktop#974

Follow up issue is podman-desktop#1016

### How to test this PR?

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

Click "More Details" on one of the examples

Signed-off-by: Charlie Drage <[email protected]>
  • Loading branch information
cdrage committed Nov 13, 2024
1 parent 64d6363 commit 0d30f60
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 11 deletions.
5 changes: 5 additions & 0 deletions packages/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Examples from './Examples.svelte';
import Navigation from './Navigation.svelte';
import DiskImagesList from './lib/disk-image/DiskImagesList.svelte';
import Dashboard from './lib/dashboard/Dashboard.svelte';
import ExampleDetails from './lib/ExampleDetails.svelte';
router.mode.hash();
Expand Down Expand Up @@ -41,6 +42,10 @@ onMount(() => {
<Route path="/examples" breadcrumb="Examples">
<Examples />
</Route>

<Route path="/example/:id" breadcrumb="Example Details" let:meta>
<ExampleDetails id={meta.params.id} />
</Route>
<Route path="/disk-images/" breadcrumb="Disk Images">
<DiskImagesList />
</Route>
Expand Down
12 changes: 6 additions & 6 deletions packages/frontend/src/lib/ExampleCard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ test('renders ExampleCard with correct content', async () => {
expect(architectureText).toBeInTheDocument();
});

test('openURL function is called when Source button is clicked', async () => {
test('redirection to /example/:id is called when More details button is clicked', async () => {
// Render the component with the example prop
render(ExampleCard, { props: { example } });

// Find and click the "Source" button
const sourceButton = screen.getByTitle('Source');
await fireEvent.click(sourceButton);
// Find and click the "More details" button
const detailsButton = screen.getByTitle('More Details');
await fireEvent.click(detailsButton);

// Ensure bootcClient.openLink is called with the correct URL
expect(bootcClient.openLink).toHaveBeenCalledWith('https://example.com/example1');
// Ensure the router.goto is called with the correct path
expect(router.goto).toHaveBeenCalledWith('/example/example1');
});

test('pullImage function is called when Pull image button is clicked', async () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/frontend/src/lib/ExampleCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ let { example }: Props = $props();
let pullInProgress = $state(false);
async function openURL(): Promise<void> {
await bootcClient.openLink(example.repository);
//await bootcClient.openLink(example.repository);
router.goto(`/example/${example.id}`);
}
async function pullImage(): Promise<void> {
Expand Down Expand Up @@ -76,10 +77,10 @@ async function gotoBuild(): Promise<void> {
<Button
on:click={openURL}
icon={faArrowUpRightFromSquare}
aria-label="Source"
title="Source"
aria-label="MoreDetails"
title="More Details"
type="link"
class="mr-2">Source</Button>
class="mr-2">More Details</Button>

{#if example?.state === 'pulled'}
<Button on:click={gotoBuild} icon={DiskImageIcon} aria-label="Build image" title="Build image" class="w-28"
Expand Down
92 changes: 92 additions & 0 deletions packages/frontend/src/lib/ExampleDetails.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**********************************************************************
* 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, fireEvent, screen } from '@testing-library/svelte';
import { expect, test, vi } from 'vitest';
import ExampleDetails from './ExampleDetails.svelte';
import { bootcClient } from '../api/client';
import { router } from 'tinro';
import type { Example } from '/@shared/src/models/examples';

// Mock bootcClient methods
vi.mock('/@/api/client', () => {
return {
bootcClient: {
getExamples: vi.fn(),
openLink: vi.fn(),
},
};
});

// Mock router
vi.mock('tinro', () => {
return {
router: {
goto: vi.fn(),
},
};
});

// Sample example data for testing
const example = {
id: 'example1',
name: 'Example 1',
categories: ['category1'],
description: 'Description 1',
repository: 'https://example.com/example1',
readme: '# Example Markdown Readme Text',
} as Example;

test('renders ExampleDetails with correct content', async () => {
// Mock getExamples to return the test example data
vi.mocked(bootcClient.getExamples).mockResolvedValue({
examples: [example],
categories: [{ id: 'category1', name: 'category1' }],
});

// Render the component with the example ID prop
render(ExampleDetails, { id: 'example1' });

// Wait until Example 1 to appear
await screen.findByLabelText('Example 1');

// Make sure that Example 1 exists
const exampleName = screen.getByLabelText('Example 1');
expect(exampleName).toBeInTheDocument();

// Make sure that Markdown is rendered
const readme = screen.getByText('Example Markdown Readme Text');
expect(readme).toBeInTheDocument();

// Check that when clicking "More Details" button the openLink method is called
const moreDetailsButton = screen.getByRole('button', { name: 'More Details' });
await fireEvent.click(moreDetailsButton);
expect(bootcClient.openLink).toHaveBeenCalledWith('https://example.com/example1');
});

test('redirects to /examples when "Go back to Examples" is clicked', async () => {
render(ExampleDetails, { id: 'example1' });

// Find and click the breadcrumb link to go back
const breadcrumbLink = screen.getByText('Examples');
await fireEvent.click(breadcrumbLink);

// Verify the router.goto method is called with the correct path
expect(router.goto).toHaveBeenCalledWith('/examples');
});
66 changes: 66 additions & 0 deletions packages/frontend/src/lib/ExampleDetails.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import { DetailsPage, Tab, Button } from '@podman-desktop/ui-svelte';
import MarkdownRenderer from './markdown/MarkdownRenderer.svelte';
import ExampleDetailsLayout from './ExampleDetailsLayout.svelte';
import { router } from 'tinro';
import { onMount } from 'svelte';
import { faArrowUpRightFromSquare, faRocket } from '@fortawesome/free-solid-svg-icons';
import { bootcClient } from '/@/api/client';
import type { Example } from '/@shared/src/models/examples';
import DiskImageIcon from './DiskImageIcon.svelte';
export let id: string;
let example: Example;
export function goToExamplesPage(): void {
router.goto('/examples');
}
async function openURL(): Promise<void> {
await bootcClient.openLink(example.repository);
}
onMount(async () => {
// Get all the examples
let examples = await bootcClient.getExamples();
// Find the example with the given id
const foundExample = examples.examples.find(example => example.id === id);
if (foundExample) {
example = foundExample;
} else {
console.error(`Example with id ${id} not found`);
}
});
</script>

<DetailsPage
title={example?.name}
breadcrumbLeftPart="Examples"
breadcrumbRightPart={example?.name}
breadcrumbTitle="Go back to Examples"
onclose={goToExamplesPage}
onbreadcrumbClick={goToExamplesPage}>
<DiskImageIcon slot="icon" size="30px" />
<svelte:fragment slot="content">
<div class="bg-[var(--pd-content-bg)] h-full overflow-y-auto">
<ExampleDetailsLayout detailsTitle="Example details" detailsLabel="Example details">
<svelte:fragment slot="content">
<MarkdownRenderer source={example?.readme} />
</svelte:fragment>
<svelte:fragment slot="details">
<div class="flex flex-col w-full space-y-4 rounded-md bg-[var(--pd-content-bg)] p-4">
<div class="flex flex-col w-full space-y-2">
<Button
on:click={openURL}
icon={faArrowUpRightFromSquare}
aria-label="More Details"
title="More Details"
type="link"
class="mr-2">Source Repository</Button>
</div>
</div>
</svelte:fragment>
</ExampleDetailsLayout>
</div>
</svelte:fragment>
</DetailsPage>
38 changes: 38 additions & 0 deletions packages/frontend/src/lib/ExampleDetailsLayout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**********************************************************************
* 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 { expect, test } from 'vitest';
import DetailsComponent from './ExampleDetailsLayout.svelte';

test('renders DetailsComponent with title and content slots', async () => {
const detailsTitle = 'Test Title';
const detailsLabel = 'Test Label';

render(DetailsComponent, { props: { detailsTitle, detailsLabel } });

// Verify title
const titleElement = screen.getByText(detailsTitle);
expect(titleElement).toBeInTheDocument();

// Verify content
const detailsPanel = screen.getByLabelText(`${detailsLabel} panel`);
expect(detailsPanel).toBeInTheDocument();
expect(detailsPanel).toBeVisible();
});
44 changes: 44 additions & 0 deletions packages/frontend/src/lib/ExampleDetailsLayout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
export let detailsTitle: string;
export let detailsLabel: string;
let open: boolean = true;
const toggle = (): void => {
open = !open;
};
</script>

<div class="flex flex-col w-full overflow-y-auto">
<slot name="header" />
<div class="grid w-full lg:grid-cols-[1fr_auto] max-lg:grid-cols-[auto]">
<div class="p-5 inline-grid">
<slot name="content" />
</div>
<div class="inline-grid max-lg:order-first">
<div class="max-lg:w-full max-lg:min-w-full" class:w-[375px]={open} class:min-w-[375px]={open}>
<div
class:hidden={!open}
class:block={open}
class="h-fit lg:bg-[var(--pd-content-card-bg)] text-[var(--pd-content-card-title)] lg:rounded-l-md lg:mt-5 lg:py-4 max-lg:block"
aria-label={`${detailsLabel} panel`}>
<div class="flex flex-col lg:px-4 space-y-4 mx-auto">
<div class="w-full flex flex-row justify-between max-lg:hidden">
<span>{detailsTitle}</span>
<button on:click={toggle} aria-label={`hide ${detailsLabel}`}
><i class="fas fa-angle-right text-[var(--pd-content-card-icon)]"></i></button>
</div>
<slot name="details" />
</div>
</div>
<div
class:hidden={open}
class:block={!open}
class="bg-[var(--pd-content-card-bg)] mt-5 p-4 rounded-md h-fit max-lg:hidden"
aria-label={`toggle ${detailsLabel}`}>
<button on:click={toggle} aria-label={`show ${detailsLabel}`}
><i class="fas fa-angle-left text-[var(--pd-content-card-icon)]"></i></button>
</div>
</div>
</div>
</div>
</div>
1 change: 0 additions & 1 deletion packages/frontend/src/lib/ExamplesCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ onMount(async () => {
// Function to update examples based on available images
function updateExamplesWithPulledImages() {
if (bootcAvailableImages) {
console.log('updateExamplesWithPulledImages');
// Set each state to 'unpulled' by default before updating, as this prevents 'flickering'
// and unsure states when images are being updated
for (const example of examples) {
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/lib/markdown/MarkdownRenderer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import SvelteMarkdown from 'svelte-markdown';
export let source: string | undefined;
</script>

<article class="prose min-w-full text-base">
<SvelteMarkdown source={source} />
</article>

0 comments on commit 0d30f60

Please sign in to comment.