From e5aee573cedf5f50e16e33c5867f44a8d43ac334 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Mon, 21 Oct 2024 09:55:38 -0400 Subject: [PATCH 1/5] feat: add examples page ### What does this PR do? * Minor edit to category (see json update) * Adds an example page (rendered portion) that shows all the examples we currently have * Pull and build image buttons * Reactively do in-progress for pulling ### Screenshot / video of UI ### What issues does this PR fix or reference? Closes https://github.com/containers/podman-desktop-extension-bootc/issues/895 ### How to test this PR? 1. Go to examples page 2. Press pull image button 3. Then build image button after it finishes Signed-off-by: Charlie Drage --- packages/frontend/src/App.svelte | 4 + packages/frontend/src/Examples.spec.ts | 136 ++++++++++++++++++ packages/frontend/src/Examples.svelte | 56 ++++++++ packages/frontend/src/Navigation.svelte | 1 + packages/frontend/src/lib/Card.svelte | 33 +++++ packages/frontend/src/lib/ExampleCard.spec.ts | 115 +++++++++++++++ packages/frontend/src/lib/ExampleCard.svelte | 91 ++++++++++++ .../frontend/src/lib/ExampleCards.spec.ts | 125 ++++++++++++++++ packages/frontend/src/lib/ExamplesCard.svelte | 68 +++++++++ packages/shared/src/models/examples.ts | 5 + 10 files changed, 634 insertions(+) create mode 100644 packages/frontend/src/Examples.spec.ts create mode 100644 packages/frontend/src/Examples.svelte create mode 100644 packages/frontend/src/lib/Card.svelte create mode 100644 packages/frontend/src/lib/ExampleCard.spec.ts create mode 100644 packages/frontend/src/lib/ExampleCard.svelte create mode 100644 packages/frontend/src/lib/ExampleCards.spec.ts create mode 100644 packages/frontend/src/lib/ExamplesCard.svelte diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte index 68ab055b..fc268cec 100644 --- a/packages/frontend/src/App.svelte +++ b/packages/frontend/src/App.svelte @@ -9,6 +9,7 @@ import { getRouterState } from './api/client'; import { rpcBrowser } from '/@/api/client'; import { Messages } from '/@shared/src/messages/Messages'; import DiskImageDetails from './lib/disk-image/DiskImageDetails.svelte'; +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'; @@ -37,6 +38,9 @@ onMount(() => { + + + diff --git a/packages/frontend/src/Examples.spec.ts b/packages/frontend/src/Examples.spec.ts new file mode 100644 index 00000000..3a9c5755 --- /dev/null +++ b/packages/frontend/src/Examples.spec.ts @@ -0,0 +1,136 @@ +/********************************************************************** + * 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, vi } from 'vitest'; +import type { ExamplesList } from '/@shared/src/models/examples'; +import type { ImageInfo } from '@podman-desktop/api'; + +import Examples from './Examples.svelte'; +import { bootcClient } from './api/client'; +import { tick } from 'svelte'; + +vi.mock('./api/client', async () => { + return { + bootcClient: { + getExamples: vi.fn(), + listBootcImages: vi.fn(), + }, + rpcBrowser: { + subscribe: () => { + return { + unsubscribe: () => {}, + }; + }, + }, + }; +}); + +// Create a mock ExamplesList +const examplesList: ExamplesList = { + examples: [ + { + id: 'example1', + name: 'Example 1', + categories: ['Category 1'], + description: 'Description 1', + repository: 'https://example.com/example1', + image: 'quay.io/example/example1', + tag: 'latest', + }, + { + id: 'example2', + name: 'Example 2', + categories: ['Category 2'], + description: 'Description 2', + repository: 'https://example.com/example2', + image: 'quay.io/example/example2', + tag: 'latest', + }, + ], + categories: [ + { + id: 'Category 1', + name: 'Category 1', + }, + { + id: 'Category 2', + name: 'Category 2', + }, + ], +}; + +// Mock the list of images returned, make sure example1 is in it, only mock the RepoTags since that is what is used / checked and nothing else +const imagesList = [ + { + RepoTags: ['quay.io/example/example1:latest'], + }, +] as ImageInfo[]; + +test('Test examples render correctly', async () => { + // Mock the getExamples method + vi.mocked(bootcClient.getExamples).mockResolvedValue(examplesList); + + // Render the examples component + render(Examples); + + // Wait for 2 examples to appear, aria-label="Example 1" and aria-label="Example 2" + await screen.findByLabelText('Example 1'); + await screen.findByLabelText('Example 2'); + + // Confirm example 1 and 2 exist + const example1 = screen.getByLabelText('Example 1'); + expect(example1).toBeInTheDocument(); + const example2 = screen.getByLabelText('Example 2'); + expect(example2).toBeInTheDocument(); +}); + +test('Test examples correctly marks examples either Pull image or Build image depending on availability', async () => { + // Mock the getExamples method + vi.mocked(bootcClient.getExamples).mockResolvedValue(examplesList); + + // Mock image list has example1 in it (button should be Build Image instead of Pull Image) + vi.mocked(bootcClient.listBootcImages).mockResolvedValue(imagesList); + + // Render the examples component + render(Examples); + + // Wait for aria-label aria-label="Example 1" to appear + await screen.findByLabelText('Example 1'); + + // Confirm that the example 1 is rendered + const example1 = screen.getByLabelText('Example 1'); + expect(example1).toBeInTheDocument(); + + // Confirm example 2 is rendered + const example2 = screen.getByLabelText('Example 2'); + expect(example2).toBeInTheDocument(); + + // Use the tick function to wait for the next render (updating pulled state) + await tick(); + + // Make sure that example1 says Build image since it's available. + const buildImage1 = example1.querySelector('[aria-label="Build image"]'); + expect(buildImage1).toBeInTheDocument(); + + // Make sure that example2 says Pull image since it's not available. + const pullImage2 = example2.querySelector('[aria-label="Pull image"]'); + expect(pullImage2).toBeInTheDocument(); +}); diff --git a/packages/frontend/src/Examples.svelte b/packages/frontend/src/Examples.svelte new file mode 100644 index 00000000..cf85e50b --- /dev/null +++ b/packages/frontend/src/Examples.svelte @@ -0,0 +1,56 @@ + + + +
+
+
+ {#each groups.entries() as [category, examples]} + + {/each} +
+
+
+
diff --git a/packages/frontend/src/Navigation.svelte b/packages/frontend/src/Navigation.svelte index 359d3578..8e303779 100644 --- a/packages/frontend/src/Navigation.svelte +++ b/packages/frontend/src/Navigation.svelte @@ -16,5 +16,6 @@ export let meta: TinroRouteMeta;
+
diff --git a/packages/frontend/src/lib/Card.svelte b/packages/frontend/src/lib/Card.svelte new file mode 100644 index 00000000..31a7256a --- /dev/null +++ b/packages/frontend/src/lib/Card.svelte @@ -0,0 +1,33 @@ + + + +
+
+
+
+ {#if title} +
+ {title} +
+ {/if} + {#if description} +
+ {description} +
+ {/if} +
+
+
+
+ +
+
+
diff --git a/packages/frontend/src/lib/ExampleCard.spec.ts b/packages/frontend/src/lib/ExampleCard.spec.ts new file mode 100644 index 00000000..f969c5ea --- /dev/null +++ b/packages/frontend/src/lib/ExampleCard.spec.ts @@ -0,0 +1,115 @@ +/********************************************************************** + * 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 ExampleCard from './ExampleCard.svelte'; +import { bootcClient } from '../api/client'; +import { router } from 'tinro'; +import type { Example, ExampleState } from '/@shared/src/models/examples'; + +// Mock bootcClient methods +vi.mock('../api/client', () => { + return { + bootcClient: { + pullImage: vi.fn(), + openLink: vi.fn(), + }, + }; +}); + +// Mock router +vi.mock('tinro', () => { + return { + router: { + goto: vi.fn(), + }, + }; +}); + +// Example object to be used in tests +const example = { + id: 'example1', + name: 'Example 1', + description: 'Description 1', + repository: 'https://example.com/example1', + categories: ['Category 1'], + image: 'quay.io/example/example1', + tag: 'latest', + architectures: ['amd64'], + state: 'unpulled', +} as Example; + +test('renders ExampleCard with correct content', async () => { + // Render the component with the example prop + render(ExampleCard, { props: { example } }); + + // Verify example name and description are displayed + const exampleName = screen.getByText('Example 1'); + expect(exampleName).toBeInTheDocument(); + + const exampleDescription = screen.getByText('Description 1'); + expect(exampleDescription).toBeInTheDocument(); + + // Verify architectures are displayed + const architectureText = screen.getByText('amd64'); + expect(architectureText).toBeInTheDocument(); +}); + +test('openURL function is called when Source 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); + + // Ensure bootcClient.openLink is called with the correct URL + expect(bootcClient.openLink).toHaveBeenCalledWith('https://example.com/example1'); +}); + +test('pullImage function is called when Pull image button is clicked', async () => { + // Render the component with the example prop (state is 'unpulled') + render(ExampleCard, { props: { example } }); + + // Find and click the "Pull image" button + const pullButton = screen.getByTitle('Pull image'); + await fireEvent.click(pullButton); + + // Ensure bootcClient.pullImage is called with the correct image name + expect(bootcClient.pullImage).toHaveBeenCalledWith('quay.io/example/example1'); +}); + +test('Build image button is displayed if example is pulled', async () => { + // Modify example state to 'pulled' + const pulledExample = { ...example, state: 'pulled' as ExampleState }; + + // Render the component with the pulled example prop + render(ExampleCard, { props: { example: pulledExample } }); + + // Ensure the "Build image" button is displayed + const buildButton = screen.getByTitle('Build image'); + expect(buildButton).toBeInTheDocument(); + + // Click the "Build image" button + await fireEvent.click(buildButton); + + // Ensure the router.goto is called with the correct path + expect(router.goto).toHaveBeenCalledWith('/disk-images/build/quay.io%2Fexample%2Fexample1/latest'); +}); diff --git a/packages/frontend/src/lib/ExampleCard.svelte b/packages/frontend/src/lib/ExampleCard.svelte new file mode 100644 index 00000000..f871c25a --- /dev/null +++ b/packages/frontend/src/lib/ExampleCard.svelte @@ -0,0 +1,91 @@ + + +
+
+ + {#if example.architectures} +
+
+ {#each example.architectures as architecture} + {architecture} + {/each} +
+ + + {#if example.tag} +
+ {example.tag} +
+ {/if} +
+ {/if} + + +
+ +
+ {example.name} + {example.description} +
+
+ + +
+ + + {#if !example.state || example.state === 'unpulled'} + + {:else if example.state === 'pulled'} + + {/if} +
+
+
diff --git a/packages/frontend/src/lib/ExampleCards.spec.ts b/packages/frontend/src/lib/ExampleCards.spec.ts new file mode 100644 index 00000000..ed70323b --- /dev/null +++ b/packages/frontend/src/lib/ExampleCards.spec.ts @@ -0,0 +1,125 @@ +/********************************************************************** + * 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, vi } from 'vitest'; +import type { Example, Category } from '../../../shared/src/models/examples'; +import ExamplesCard from './ExamplesCard.svelte'; +import { bootcClient } from '../api/client'; +import { tick } from 'svelte'; +import type { ImageInfo } from '@podman-desktop/api'; + +// Mock bootcClient methods +vi.mock('../api/client', () => { + return { + bootcClient: { + listBootcImages: vi.fn(), + }, + rpcBrowser: { + subscribe: vi.fn().mockReturnValue({ + unsubscribe: vi.fn(), + }), + }, + }; +}); + +// Mock category and examples data +const category: Category = { + id: 'category1', + name: 'Category 1', +}; + +const examples: Example[] = [ + { + id: 'example1', + name: 'Example 1', + description: 'Description 1', + repository: 'https://example.com/example1', + image: 'quay.io/example/example1', + tag: 'latest', + categories: ['Category 1'], + state: 'unpulled', + architectures: ['amd64'], + }, + { + id: 'example2', + name: 'Example 2', + description: 'Description 2', + repository: 'https://example.com/example2', + image: 'quay.io/example/example2', + tag: 'latest', + categories: ['Category 1'], + state: 'unpulled', + architectures: ['amd64'], + }, +]; + +// Mock listBootcImages response to simulate available images +const imagesList = [ + { + RepoTags: ['quay.io/example/example1:latest'], + }, +] as ImageInfo[]; + +test('renders ExamplesCard with correct content', async () => { + // Mock the listBootcImages method + vi.mocked(bootcClient.listBootcImages).mockResolvedValue(imagesList); + + // Render the ExamplesCard component with category and examples props + render(ExamplesCard, { props: { category, examples } }); + + // Verify category name is displayed + const categoryName = screen.getByText('Category 1'); + expect(categoryName).toBeInTheDocument(); + + // Verify example names are displayed + const example1 = screen.getByText('Example 1'); + expect(example1).toBeInTheDocument(); + + const example2 = screen.getByText('Example 2'); + expect(example2).toBeInTheDocument(); +}); + +test('updates example state to "pulled" when images are available', async () => { + // Mock the listBootcImages method + vi.mocked(bootcClient.listBootcImages).mockResolvedValue(imagesList); + + // Render the ExamplesCard component with category and examples props + render(ExamplesCard, { props: { category, examples } }); + + // Use tick to wait for the next render (update the state) + await tick(); + + // Verify that example1 has been updated to state "pulled" + const buildButton = screen.getByTitle('Build image'); + expect(buildButton).toBeInTheDocument(); + + // Verify example2 still says "Pull image" since it's not in the list of available images + const pullButton = screen.getByTitle('Pull image'); + expect(pullButton).toBeInTheDocument(); +}); + +test('displays message when there are no examples in the category', async () => { + // Render the ExamplesCard component with empty examples array + render(ExamplesCard, { props: { category, examples: [] } }); + + // Verify the message is displayed + const noExamplesMessage = screen.getByText('There is no example in this category.'); + expect(noExamplesMessage).toBeInTheDocument(); +}); diff --git a/packages/frontend/src/lib/ExamplesCard.svelte b/packages/frontend/src/lib/ExamplesCard.svelte new file mode 100644 index 00000000..6dfdabbe --- /dev/null +++ b/packages/frontend/src/lib/ExamplesCard.svelte @@ -0,0 +1,68 @@ + + + +
+ {#if examples.length === 0} +
There is no example in this category.
+ {/if} +
+ {#each examples as example} + + {/each} +
+
+
diff --git a/packages/shared/src/models/examples.ts b/packages/shared/src/models/examples.ts index 980247a5..1674b87f 100644 --- a/packages/shared/src/models/examples.ts +++ b/packages/shared/src/models/examples.ts @@ -25,6 +25,8 @@ export interface Category { // validator for certain types arm / amd64 export type ArchitectureType = 'arm64' | 'amd64'; +export type ExampleState = 'pulled' | 'unpulled'; + export interface Example { id: string; name: string; @@ -45,6 +47,9 @@ export interface Example { // Will assume 'Containerfile' in the root of the repo. basedir?: string; + + // The state of the example + state?: ExampleState; } export interface ExamplesList { From ac382f76267bdcb795d6b8af05dbd73e9a5b0cfb Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Mon, 21 Oct 2024 10:35:12 -0400 Subject: [PATCH 2/5] update based on review Signed-off-by: Charlie Drage --- packages/frontend/src/lib/ExampleCard.spec.ts | 2 +- packages/frontend/src/lib/ExampleCard.svelte | 2 +- .../frontend/src/lib/ExampleCards.spec.ts | 2 +- packages/frontend/src/lib/ExamplesCard.svelte | 21 ++++++++----------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/lib/ExampleCard.spec.ts b/packages/frontend/src/lib/ExampleCard.spec.ts index f969c5ea..66760f9a 100644 --- a/packages/frontend/src/lib/ExampleCard.spec.ts +++ b/packages/frontend/src/lib/ExampleCard.spec.ts @@ -20,7 +20,7 @@ import '@testing-library/jest-dom/vitest'; import { render, fireEvent, screen } from '@testing-library/svelte'; import { expect, test, vi } from 'vitest'; import ExampleCard from './ExampleCard.svelte'; -import { bootcClient } from '../api/client'; +import { bootcClient } from '/@/api/client'; import { router } from 'tinro'; import type { Example, ExampleState } from '/@shared/src/models/examples'; diff --git a/packages/frontend/src/lib/ExampleCard.svelte b/packages/frontend/src/lib/ExampleCard.svelte index f871c25a..7360deb8 100644 --- a/packages/frontend/src/lib/ExampleCard.svelte +++ b/packages/frontend/src/lib/ExampleCard.svelte @@ -1,5 +1,5 @@ - +
{#if examples.length === 0}
There is no example in this category.
From 0450b1044ed1cfbe6c1d7294d9757801ce5f55ba Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Tue, 22 Oct 2024 10:20:37 -0400 Subject: [PATCH 3/5] update size Signed-off-by: Charlie Drage --- packages/backend/assets/examples.json | 9 ++++-- packages/frontend/src/Examples.spec.ts | 20 ++++++++----- packages/frontend/src/lib/ExampleCard.svelte | 30 +++++++++++++++---- packages/frontend/src/lib/ExamplesCard.svelte | 12 ++++++-- packages/shared/src/models/examples.ts | 3 ++ 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/backend/assets/examples.json b/packages/backend/assets/examples.json index af17a393..d5b70f8c 100644 --- a/packages/backend/assets/examples.json +++ b/packages/backend/assets/examples.json @@ -10,7 +10,8 @@ "tag": "latest", "categories": ["fedora"], "architectures": ["amd64"], - "basedir": "httpd" + "basedir": "httpd", + "size": 2000 }, { "id": "fedora-tailscale", @@ -21,7 +22,8 @@ "tag": "latest", "categories": ["fedora"], "architectures": ["amd64"], - "basedir": "tailscale" + "basedir": "tailscale", + "size": 1720 }, { "id": "fedora-podman-systemd", @@ -32,7 +34,8 @@ "tag": "latest", "categories": ["fedora"], "architectures": ["amd64"], - "basedir": "app-podman-systemd" + "basedir": "app-podman-systemd", + "size": 1530 } ], "categories": [ diff --git a/packages/frontend/src/Examples.spec.ts b/packages/frontend/src/Examples.spec.ts index 3a9c5755..1fec8c83 100644 --- a/packages/frontend/src/Examples.spec.ts +++ b/packages/frontend/src/Examples.spec.ts @@ -18,7 +18,7 @@ import '@testing-library/jest-dom/vitest'; -import { render, screen } from '@testing-library/svelte'; +import { render, screen, waitFor } from '@testing-library/svelte'; import { expect, test, vi } from 'vitest'; import type { ExamplesList } from '/@shared/src/models/examples'; import type { ImageInfo } from '@podman-desktop/api'; @@ -126,11 +126,15 @@ test('Test examples correctly marks examples either Pull image or Build image de // Use the tick function to wait for the next render (updating pulled state) await tick(); - // Make sure that example1 says Build image since it's available. - const buildImage1 = example1.querySelector('[aria-label="Build image"]'); - expect(buildImage1).toBeInTheDocument(); - - // Make sure that example2 says Pull image since it's not available. - const pullImage2 = example2.querySelector('[aria-label="Pull image"]'); - expect(pullImage2).toBeInTheDocument(); + // Wait until example1 says Build image as it updates reactively + // same for example 2 but Pull image + waitFor(() => { + const buildImage1 = example1.querySelector('[aria-label="Build image"]'); + expect(buildImage1).toBeInTheDocument(); + }); + + waitFor(() => { + const pullImage2 = example2.querySelector('[aria-label="Pull image"]'); + expect(pullImage2).toBeInTheDocument(); + }); }); diff --git a/packages/frontend/src/lib/ExampleCard.svelte b/packages/frontend/src/lib/ExampleCard.svelte index 7360deb8..0af25e4f 100644 --- a/packages/frontend/src/lib/ExampleCard.svelte +++ b/packages/frontend/src/lib/ExampleCard.svelte @@ -1,7 +1,7 @@ -
+
- +
{#if examples.length === 0}
There is no example in this category.
diff --git a/packages/frontend/src/lib/ExampleCards.spec.ts b/packages/frontend/src/lib/ExamplesCard.ts similarity index 100% rename from packages/frontend/src/lib/ExampleCards.spec.ts rename to packages/frontend/src/lib/ExamplesCard.ts From 2e21ee2d255f0d280f68898ed8ecd7ce968a9500 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 23 Oct 2024 15:16:55 -0400 Subject: [PATCH 5/5] use svelte5, update to code review Signed-off-by: Charlie Drage --- packages/backend/assets/examples.json | 6 ++-- packages/frontend/src/Examples.spec.ts | 4 +-- packages/frontend/src/lib/Card.svelte | 10 ++++--- packages/frontend/src/lib/ExampleCard.svelte | 29 ++++++------------- packages/frontend/src/lib/ExamplesCard.svelte | 18 +++++++----- 5 files changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/backend/assets/examples.json b/packages/backend/assets/examples.json index d5b70f8c..c70f2404 100644 --- a/packages/backend/assets/examples.json +++ b/packages/backend/assets/examples.json @@ -11,7 +11,7 @@ "categories": ["fedora"], "architectures": ["amd64"], "basedir": "httpd", - "size": 2000 + "size": 2000000000 }, { "id": "fedora-tailscale", @@ -23,7 +23,7 @@ "categories": ["fedora"], "architectures": ["amd64"], "basedir": "tailscale", - "size": 1720 + "size": 1720000000 }, { "id": "fedora-podman-systemd", @@ -35,7 +35,7 @@ "categories": ["fedora"], "architectures": ["amd64"], "basedir": "app-podman-systemd", - "size": 1530 + "size": 1530000000 } ], "categories": [ diff --git a/packages/frontend/src/Examples.spec.ts b/packages/frontend/src/Examples.spec.ts index 1fec8c83..c7fd8197 100644 --- a/packages/frontend/src/Examples.spec.ts +++ b/packages/frontend/src/Examples.spec.ts @@ -128,12 +128,12 @@ test('Test examples correctly marks examples either Pull image or Build image de // Wait until example1 says Build image as it updates reactively // same for example 2 but Pull image - waitFor(() => { + await waitFor(() => { const buildImage1 = example1.querySelector('[aria-label="Build image"]'); expect(buildImage1).toBeInTheDocument(); }); - waitFor(() => { + await waitFor(() => { const pullImage2 = example2.querySelector('[aria-label="Pull image"]'); expect(pullImage2).toBeInTheDocument(); }); diff --git a/packages/frontend/src/lib/Card.svelte b/packages/frontend/src/lib/Card.svelte index a4dfe0a5..288acdb5 100644 --- a/packages/frontend/src/lib/Card.svelte +++ b/packages/frontend/src/lib/Card.svelte @@ -1,8 +1,10 @@
diff --git a/packages/frontend/src/lib/ExampleCard.svelte b/packages/frontend/src/lib/ExampleCard.svelte index 0af25e4f..e92bfd17 100644 --- a/packages/frontend/src/lib/ExampleCard.svelte +++ b/packages/frontend/src/lib/ExampleCard.svelte @@ -5,10 +5,15 @@ import { bootcClient } from '/@/api/client'; import { Button } from '@podman-desktop/ui-svelte'; import { router } from 'tinro'; import DiskImageIcon from './DiskImageIcon.svelte'; +import { filesize } from 'filesize'; -export let example: Example; +interface Props { + example: Example; +} + +let { example }: Props = $props(); -let pullInProgress = false; +let pullInProgress = $state(false); async function openURL(): Promise { await bootcClient.openLink(example.repository); @@ -26,20 +31,6 @@ async function gotoBuild(): Promise { router.goto(`/disk-images/build/${encodeURIComponent(example.image)}/${encodeURIComponent(example.tag)}`); } } - -// Function that takes something in MB and returns it in GB is more than 1GB -// in a nice string format -function formatStringSize(size: number): string { - if (size > 1000) { - return `${(size / 1000).toFixed(1)} GB`; - } - return `${size} MB`; -} - -// Make sure if example.pulled is updated, we force a re-render -$: { - example.state; -}
@@ -58,7 +49,7 @@ $: { {#if example.size}
- {formatStringSize(example.size)} + {filesize(example.size)}
{/if} @@ -99,12 +90,10 @@ $: { icon={faArrowDown} aria-label="Pull image" title="Pull image" - class="w-28" inProgress={pullInProgress}>Pull image {:else} - + {/if}
diff --git a/packages/frontend/src/lib/ExamplesCard.svelte b/packages/frontend/src/lib/ExamplesCard.svelte index 6608f09a..bace0d40 100644 --- a/packages/frontend/src/lib/ExamplesCard.svelte +++ b/packages/frontend/src/lib/ExamplesCard.svelte @@ -7,9 +7,13 @@ import { bootcClient, rpcBrowser } from '../api/client'; import type { ImageInfo } from '@podman-desktop/api'; import { Messages } from '/@shared/src/messages/Messages'; -export let category: Category; -export let examples: Example[]; -let bootcAvailableImages: ImageInfo[] = []; +interface Props { + category: Category; + examples: Example[]; +} +let { category, examples = $bindable() }: Props = $props(); + +let bootcAvailableImages = $state([]); onMount(async () => { bootcAvailableImages = await bootcClient.listBootcImages(); @@ -32,6 +36,7 @@ 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) { @@ -54,16 +59,13 @@ function updateExamplesWithPulledImages() { } // Reactive statement to call the function each time bootcAvailableImages updates -$: if (bootcAvailableImages) { +$effect(() => { updateExamplesWithPulledImages(); -} +});
- {#if examples.length === 0} -
There is no example in this category.
- {/if}
{#each examples as example}