From bfce1b3c96cf3abe6203a0a5c99bfb6044118b44 Mon Sep 17 00:00:00 2001 From: Mason Hu Date: Mon, 16 Sep 2024 17:01:19 +0200 Subject: [PATCH] feat: show cached images for instance creation Signed-off-by: Mason Hu --- src/pages/images/ImageSelector.tsx | 50 ++++++++++++++++++++++-------- src/types/image.d.ts | 2 ++ src/util/images.tsx | 8 +++-- tests/helpers/instances.ts | 13 ++++++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/pages/images/ImageSelector.tsx b/src/pages/images/ImageSelector.tsx index d4b009f305..a4e22a2946 100644 --- a/src/pages/images/ImageSelector.tsx +++ b/src/pages/images/ImageSelector.tsx @@ -1,6 +1,7 @@ import { FC, OptionHTMLAttributes, useState } from "react"; import { Button, + CheckboxInput, Col, MainTable, Modal, @@ -19,6 +20,7 @@ import { isContainerOnlyImage, isVmOnlyImage, LOCAL_ISO, + LOCAL_IMAGE, } from "util/images"; import Loader from "components/Loader"; import { getArchitectureAliases } from "util/architectures"; @@ -55,6 +57,7 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { const [arch, setArch] = useState("amd64"); const [type, setType] = useState(undefined); const [variant, setVariant] = useState(ANY); + const [hideRemote, setHideRemote] = useState(false); const { project } = useParams<{ project: string }>(); const loadImages = (file: string, server: string): Promise => { @@ -105,15 +108,19 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { const archSupported = getArchitectureAliases( settings?.environment?.architectures ?? [], ); - const images = isLoading + let images = isLoading ? [] : localImages - .filter((image) => !image.cached) .map(localLxdToRemoteImage) - .concat([...canonicalImages].reverse().sort(byLtsFirst)) - .concat([...minimalImages].reverse().sort(byLtsFirst)) - .concat([...imagesLxdImages]) - .filter((image) => archSupported.includes(image.arch)); + .sort((a, b) => Number(b.cached) - Number(a.cached)); + + if (!hideRemote) { + images = images + .concat([...canonicalImages].reverse().sort(byLtsFirst)) + .concat([...minimalImages].reverse().sort(byLtsFirst)) + .concat([...imagesLxdImages]) + .filter((image) => archSupported.includes(image.arch)); + } const archAll = [...new Set(images.map((item) => item.arch))] .filter((arch) => arch !== "") @@ -210,19 +217,20 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { : item.variant; const getSource = () => { - if (item.created_at) { - return "Local"; + let source = "Custom"; + if (!item.cached && item.created_at) { + source = "Local"; } if (item.server === canonicalServer) { - return "Ubuntu"; + source = "Ubuntu"; } if (item.server === minimalServer) { - return "Ubuntu Minimal"; + source = "Ubuntu Minimal"; } if (item.server === imagesLxdServer) { - return "LXD Images"; + source = "LXD Images"; } - return "Custom"; + return source; }; return { @@ -260,7 +268,12 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { onClick: selectImage, }, { - content: getSource(), + content: ( + <> + {getSource()} + {item.cached && cached} + + ), role: "cell", "aria-label": "Source", onClick: selectImage, @@ -272,6 +285,11 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { type="button" dense className="u-no-margin--bottom" + appearance={ + item.cached || item.server === LOCAL_IMAGE + ? "positive" + : "default" + } > Select @@ -403,6 +421,12 @@ const ImageSelector: FC = ({ onSelect, onClose }) => { ]} value={type ?? ""} /> + setHideRemote((prev) => !prev)} + /> diff --git a/src/types/image.d.ts b/src/types/image.d.ts index f01948f491..a923b9265c 100644 --- a/src/types/image.d.ts +++ b/src/types/image.d.ts @@ -15,6 +15,7 @@ export interface LxdImage { os: string; release: string; variant?: string; + version?: string; }; update_source?: { alias: string; @@ -61,6 +62,7 @@ export interface RemoteImage { volume?: LxdStorageVolume; type?: LxdImageType; fingerprint?: string; + cached?: boolean; } export interface RemoteImageList { diff --git a/src/util/images.tsx b/src/util/images.tsx index dbeca8806f..da7644c0cd 100644 --- a/src/util/images.tsx +++ b/src/util/images.tsx @@ -1,5 +1,6 @@ import { LxdImage, RemoteImage } from "types/image"; import { LxdStorageVolume } from "types/storage"; +import { capitalizeFirstLetter } from "./helpers"; export const isVmOnlyImage = (image: RemoteImage): boolean | undefined => { if (image.server === LOCAL_ISO || image.type === "virtual-machine") { @@ -46,11 +47,14 @@ export const localLxdToRemoteImage = (image: LxdImage): RemoteImage => { aliases: image.update_source?.alias ?? image.aliases?.[0]?.name ?? "", fingerprint: image.fingerprint, arch: image.architecture === "x86_64" ? "amd64" : image.architecture, - os: image.properties?.os ?? "", + os: capitalizeFirstLetter(image.properties?.os ?? ""), created_at: new Date(image.uploaded_at).getTime(), release: image.properties?.release ?? "", - server: LOCAL_IMAGE, + release_title: image.properties?.version ?? "", type: image.type, + cached: image.cached, + server: image.cached ? image.update_source?.server : LOCAL_IMAGE, + variant: image.properties?.variant, }; }; diff --git a/tests/helpers/instances.ts b/tests/helpers/instances.ts index a935eead3a..351f83e09f 100644 --- a/tests/helpers/instances.ts +++ b/tests/helpers/instances.ts @@ -27,7 +27,12 @@ export const createInstance = async ( await page.getByRole("button", { name: "Browse images" }).click(); await page.getByPlaceholder("Search an image").click(); await page.getByPlaceholder("Search an image").fill(image); - await page.getByRole("button", { name: "Select" }).first().click(); + await page + .getByRole("row") + .filter({ hasNotText: "cached" }) + .getByRole("button", { name: "Select" }) + .first() + .click(); await page .getByRole("combobox", { name: "Instance type" }) .selectOption(type); @@ -121,7 +126,11 @@ export const createAndStartInstance = async ( await page.getByRole("button", { name: "Browse images" }).click(); await page.getByPlaceholder("Search an image").click(); await page.getByPlaceholder("Search an image").fill("alpine/3.19/cloud"); - await page.getByRole("button", { name: "Select" }).click(); + await page + .getByRole("row") + .filter({ hasNotText: "cached" }) + .getByRole("button", { name: "Select" }) + .click(); await page .getByRole("combobox", { name: "Instance type" }) .selectOption(type);