From 6a6e9c1c2ce1775a496d2f131857d8cd7f24d60f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:09:34 +0100 Subject: [PATCH 01/13] API: Display only local block devices, remote ones should be retrieved from hw-report --- pkgs/clan-cli/clan_cli/api/directory.py | 28 ++++--------------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/api/directory.py b/pkgs/clan-cli/clan_cli/api/directory.py index 6adab0d4b..6749cbb0b 100644 --- a/pkgs/clan-cli/clan_cli/api/directory.py +++ b/pkgs/clan-cli/clan_cli/api/directory.py @@ -118,37 +118,17 @@ def blk_from_dict(data: dict) -> BlkInfo: ) -@dataclass -class BlockDeviceOptions: - hostname: str | None = None - keyfile: str | None = None - - @API.register -def show_block_devices(options: BlockDeviceOptions) -> Blockdevices: +def show_block_devices() -> Blockdevices: """ - Abstract api method to show block devices. + Api method to show local block devices. + It must return a list of block devices. """ - keyfile = options.keyfile - remote = ( - [ - "ssh", - *(["-i", f"{keyfile}"] if keyfile else []), - # Disable strict host key checking - "-o StrictHostKeyChecking=accept-new", - # Disable known hosts file - "-o UserKnownHostsFile=/dev/null", - f"{options.hostname}", - ] - if options.hostname - else [] - ) cmd = nix_shell( - ["nixpkgs#util-linux", *(["nixpkgs#openssh"] if options.hostname else [])], + ["nixpkgs#util-linux"], [ - *remote, "lsblk", "--json", "--output", From ea47dd77115312e89f6d65ed26a10aea77a36c3e Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:10:11 +0100 Subject: [PATCH 02/13] UI: remove unused blockdevices route --- .../app/src/routes/blockdevices/view.tsx | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 pkgs/webview-ui/app/src/routes/blockdevices/view.tsx diff --git a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx deleted file mode 100644 index f7d7c7b3f..000000000 --- a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { callApi } from "@/src/api"; -import { Component, For, Show } from "solid-js"; - -import { createQuery } from "@tanstack/solid-query"; -import { Button } from "@/src/components/button"; -import Icon from "@/src/components/icon"; - -export const BlockDevicesView: Component = () => { - const { - data: devices, - refetch: loadDevices, - isFetching, - } = createQuery(() => ({ - queryKey: ["block_devices"], - queryFn: async () => { - const result = await callApi("show_block_devices", { options: {} }); - if (result.status === "error") throw new Error("Failed to fetch data"); - - return result.data; - }, - staleTime: 1000 * 60 * 5, - })); - - return ( -
-
- -
-
- {isFetching ? ( - - ) : ( - - {(devices) => ( - - {(device) => ( -
-
-
Name
-
- {" "} - storage{" "} - {device.name} -
-
-
- -
-
Size
-
{device.size}
-
-
-
- )} -
- )} -
- )} -
-
- ); -}; From b44543f849b3017c2acc34125d2574bbca4784d0 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:10:56 +0100 Subject: [PATCH 03/13] UI: init InputError component --- .../app/src/components/inputBase/index.tsx | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/pkgs/webview-ui/app/src/components/inputBase/index.tsx b/pkgs/webview-ui/app/src/components/inputBase/index.tsx index 728a72451..b9608da1f 100644 --- a/pkgs/webview-ui/app/src/components/inputBase/index.tsx +++ b/pkgs/webview-ui/app/src/components/inputBase/index.tsx @@ -1,7 +1,7 @@ import cx from "classnames"; -import { createEffect, JSX, splitProps } from "solid-js"; +import { JSX, Ref, Show, splitProps } from "solid-js"; import Icon, { IconVariant } from "../icon"; -import { Typography } from "../Typography"; +import { Typography, TypographyProps } from "../Typography"; type Variants = "outlined" | "ghost"; interface InputBaseProps { @@ -17,6 +17,9 @@ interface InputBaseProps { readonly?: boolean; error?: boolean; icon?: IconVariant; + /** Overrides the input element */ + inputElem?: JSX.Element; + divRef?: Ref; } const variantBorder: Record = { @@ -27,11 +30,7 @@ const variantBorder: Record = { const fgStateClasses = cx("aria-disabled:fg-def-4 aria-readonly:fg-def-3"); export const InputBase = (props: InputBaseProps) => { - const [, inputProps] = splitProps(props, ["class"]); - - createEffect(() => { - console.log("InputBase", props.value, props.variant); - }); + const [internal, inputProps] = splitProps(props, ["class", "divRef"]); return (
{ // Cursor "aria-readonly:cursor-no-drop", - props.class, + props.class )} classList={{ [cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error, @@ -66,6 +65,7 @@ export const InputBase = (props: InputBaseProps) => { aria-readonly={props.readonly} tabIndex={0} role="textbox" + ref={internal.divRef} > {props.icon && ( { )} - + + +
); }; -interface InputLabelProps extends JSX.LabelHTMLAttributes { +export interface InputLabelProps + extends JSX.LabelHTMLAttributes { description?: string; required?: boolean; error?: boolean; help?: string; + labelAction?: JSX.Element; } export const InputLabel = (props: InputLabelProps) => { - const [labelProps, forwardProps] = splitProps(props, ["class"]); + const [labelProps, forwardProps] = splitProps(props, [ + "class", + "labelAction", + ]); return ( ); }; + +interface InputErrorProps { + error: string; + typographyProps?: TypographyProps; +} +export const InputError = (props: InputErrorProps) => { + const [typoClasses, rest] = splitProps( + props.typographyProps || { class: "" }, + ["class"] + ); + return ( + + {props.error} + + ); +}; From ee4cdb2b76868c5ab5fac4aa2540fdfd163741ad Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:11:19 +0100 Subject: [PATCH 04/13] UI typography: export reusable types --- pkgs/webview-ui/app/src/components/Typography/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkgs/webview-ui/app/src/components/Typography/index.tsx b/pkgs/webview-ui/app/src/components/Typography/index.tsx index fe138cf9f..dc5410d58 100644 --- a/pkgs/webview-ui/app/src/components/Typography/index.tsx +++ b/pkgs/webview-ui/app/src/components/Typography/index.tsx @@ -3,7 +3,7 @@ import { Dynamic } from "solid-js/web"; import cx from "classnames"; import "./css/typography.css"; -type Hierarchy = "body" | "title" | "headline" | "label"; +export type Hierarchy = "body" | "title" | "headline" | "label"; type Color = "primary" | "secondary" | "tertiary"; type Weight = "normal" | "medium" | "bold"; type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4"; @@ -75,7 +75,7 @@ const weightMap: Record = { bold: cx("fnt-weight-bold"), }; -interface TypographyProps { +interface _TypographyProps { hierarchy: H; size: AllowedSizes; children: JSX.Element; @@ -86,7 +86,8 @@ interface TypographyProps { class?: string; classList?: Record; } -export const Typography = (props: TypographyProps) => { + +export const Typography = (props: _TypographyProps) => { return ( (props: TypographyProps) => { ); }; + +export type TypographyProps = _TypographyProps; From de5479169cce50dfb3026e32960743fc31f344fd Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:11:36 +0100 Subject: [PATCH 05/13] UI: init FieldLayout wrapper --- .../webview-ui/app/src/Form/fields/layout.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pkgs/webview-ui/app/src/Form/fields/layout.tsx diff --git a/pkgs/webview-ui/app/src/Form/fields/layout.tsx b/pkgs/webview-ui/app/src/Form/fields/layout.tsx new file mode 100644 index 000000000..3a9a4dd70 --- /dev/null +++ b/pkgs/webview-ui/app/src/Form/fields/layout.tsx @@ -0,0 +1,29 @@ +import { JSX, splitProps } from "solid-js"; +import cx from "classnames"; + +interface LayoutProps extends JSX.HTMLAttributes { + field?: JSX.Element; + label?: JSX.Element; + error?: JSX.Element; +} +export const FieldLayout = (props: LayoutProps) => { + const [intern, divProps] = splitProps(props, [ + "field", + "label", + "error", + "class", + ]); + return ( +
+ +
{props.field}
+ {props.error && {props.error}} +
+ ); +}; From abaee70d356617d1c9df684ff7d4d1983617d5ca Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:13:06 +0100 Subject: [PATCH 06/13] UI: fixup Select component design & api --- .../webview-ui/app/src/Form/fields/Select.tsx | 244 ++++++++++-------- .../app/src/components/inputBase/index.tsx | 4 +- 2 files changed, 138 insertions(+), 110 deletions(-) diff --git a/pkgs/webview-ui/app/src/Form/fields/Select.tsx b/pkgs/webview-ui/app/src/Form/fields/Select.tsx index 9afa52b6c..c8f67c6d7 100644 --- a/pkgs/webview-ui/app/src/Form/fields/Select.tsx +++ b/pkgs/webview-ui/app/src/Form/fields/Select.tsx @@ -7,14 +7,22 @@ import { createMemo, } from "solid-js"; import { Portal } from "solid-js/web"; -import cx from "classnames"; -import { Label } from "../base/label"; import { useFloating } from "../base"; -import { autoUpdate, flip, hide, shift, size } from "@floating-ui/dom"; +import { autoUpdate, flip, hide, offset, shift, size } from "@floating-ui/dom"; +import { Button } from "@/src/components/button"; +import { + InputBase, + InputError, + InputLabel, + InputLabelProps, +} from "@/src/components/inputBase"; +import { FieldLayout } from "./layout"; +import Icon from "@/src/components/icon"; export interface Option { value: string; label: string; + disabled?: boolean; } interface SelectInputpProps { @@ -22,7 +30,7 @@ interface SelectInputpProps { selectProps: JSX.InputHTMLAttributes; options: Option[]; label: JSX.Element; - altLabel?: JSX.Element; + labelProps?: InputLabelProps; helperText?: JSX.Element; error?: string; required?: boolean; @@ -36,6 +44,7 @@ interface SelectInputpProps { disabled?: boolean; placeholder?: string; multiple?: boolean; + loading?: boolean; } export function SelectInput(props: SelectInputpProps) { @@ -61,6 +70,7 @@ export function SelectInput(props: SelectInputpProps) { }); }, }), + offset({ mainAxis: 2 }), shift(), flip(), hide({ @@ -114,126 +124,144 @@ export function SelectInput(props: SelectInputpProps) { return ( <> - + ); } diff --git a/pkgs/webview-ui/app/src/components/inputBase/index.tsx b/pkgs/webview-ui/app/src/components/inputBase/index.tsx index b9608da1f..39780146d 100644 --- a/pkgs/webview-ui/app/src/components/inputBase/index.tsx +++ b/pkgs/webview-ui/app/src/components/inputBase/index.tsx @@ -55,7 +55,7 @@ export const InputBase = (props: InputBaseProps) => { // Cursor "aria-readonly:cursor-no-drop", - props.class + props.class, )} classList={{ [cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error, @@ -160,7 +160,7 @@ interface InputErrorProps { export const InputError = (props: InputErrorProps) => { const [typoClasses, rest] = splitProps( props.typographyProps || { class: "" }, - ["class"] + ["class"], ); return ( Date: Fri, 20 Dec 2024 18:13:41 +0100 Subject: [PATCH 07/13] UI/TextInput: use FieldLayout --- .../app/src/Form/fields/TextInput.tsx | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/pkgs/webview-ui/app/src/Form/fields/TextInput.tsx b/pkgs/webview-ui/app/src/Form/fields/TextInput.tsx index 74640f345..c37214b30 100644 --- a/pkgs/webview-ui/app/src/Form/fields/TextInput.tsx +++ b/pkgs/webview-ui/app/src/Form/fields/TextInput.tsx @@ -1,62 +1,51 @@ -import { createEffect, Show, type JSX } from "solid-js"; -import cx from "classnames"; -import { Label } from "../base/label"; -import { InputBase, InputLabel } from "@/src/components/inputBase"; +import { splitProps, type JSX } from "solid-js"; +import { InputBase, InputError, InputLabel } from "@/src/components/inputBase"; import { Typography } from "@/src/components/Typography"; +import { FieldLayout } from "./layout"; interface TextInputProps { - value: string; - inputProps?: JSX.InputHTMLAttributes; - label: JSX.Element; - altLabel?: JSX.Element; - helperText?: JSX.Element; + // Common error?: string; required?: boolean; - type?: string; - inlineLabel?: JSX.Element; - class?: string; - adornment?: { - position: "start" | "end"; - content: JSX.Element; - }; disabled?: boolean; + // Passed to input + value: string; + inputProps?: JSX.InputHTMLAttributes; placeholder?: string; + // Passed to label + label: JSX.Element; + help?: string; + // Passed to layouad + class?: string; } export function TextInput(props: TextInputProps) { - // createEffect(() => { - // console.log("TextInput", props.error, props.value); - // }); + const [layoutProps, rest] = splitProps(props, ["class"]); return ( -
- - {props.label} - - - {props.error && ( - - {props.error} - - )} -
+ {props.label} + + } + field={ + + } + error={props.error && } + {...layoutProps} + /> ); } From 2e2df3f09b791e3bf4a033b17c78957971ff3799 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Dec 2024 18:14:14 +0100 Subject: [PATCH 08/13] UI: migrate flash installer page --- pkgs/webview-ui/app/src/routes/flash/view.tsx | 331 +++++++++--------- 1 file changed, 168 insertions(+), 163 deletions(-) diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 65748cbc5..dcdff3d97 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -2,10 +2,12 @@ import { callApi } from "@/src/api"; import { Button } from "@/src/components/button"; import { FileInput } from "@/src/components/FileInput"; import Icon from "@/src/components/icon"; -import { SelectInput } from "@/src/components/SelectInput"; -import { TextInput } from "@/src/Form/fields/TextInput"; + import { Typography } from "@/src/components/Typography"; import { Header } from "@/src/layout/header"; + +import { SelectInput } from "@/src/Form/fields/Select"; +import { TextInput } from "@/src/Form/fields/TextInput"; import { createForm, required, @@ -16,6 +18,8 @@ import { import { createQuery } from "@tanstack/solid-query"; import { createEffect, createSignal, For, Show } from "solid-js"; import toast from "solid-toast"; +import { FieldLayout } from "@/src/Form/fields/layout"; +import { InputLabel } from "@/src/components/inputBase"; interface Wifi extends FieldValues { ssid: string; @@ -89,9 +93,7 @@ export const Flash = () => { const deviceQuery = createQuery(() => ({ queryKey: ["block_devices"], queryFn: async () => { - const result = await callApi("show_block_devices", { - options: {}, - }); + const result = await callApi("show_block_devices", {}); if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; }, @@ -145,41 +147,43 @@ export const Flash = () => { const handleSubmit = async (values: FlashFormValues) => { console.log("Submit:", values); - try { - await callApi("flash_machine", { - machine: { - name: values.machine.devicePath, - flake: { - loc: values.machine.flake, - }, - }, - mode: "format", - disks: [{ name: "main", device: values.disk }], - system_config: { - language: values.language, - keymap: values.keymap, - ssh_keys_path: values.sshKeys.map((file) => file.name), - }, - dry_run: false, - write_efi_boot_entries: false, - debug: false, - }); - } catch (error) { - toast.error(`Error could not flash disk: ${error}`); - console.error("Error submitting form:", error); - } + toast.error("Not fully implemented yet"); + // Disabled for now. To prevent accidental flashing of local disks + // try { + // await callApi("flash_machine", { + // machine: { + // name: values.machine.devicePath, + // flake: { + // loc: values.machine.flake, + // }, + // }, + // mode: "format", + // disks: [{ name: "main", device: values.disk }], + // system_config: { + // language: values.language, + // keymap: values.keymap, + // ssh_keys_path: values.sshKeys.map((file) => file.name), + // }, + // dry_run: false, + // write_efi_boot_entries: false, + // debug: false, + // }); + // } catch (error) { + // toast.error(`Error could not flash disk: ${error}`); + // console.error("Error submitting form:", error); + // } }; return ( <>
- + USB Utility image. - This will make bootstrapping a new machine easier by providing secure - remote connection to any machine when plugged in. + Will make bootstrapping new machines easier by providing secure remote + connection to any machine when plugged in.
@@ -220,37 +224,30 @@ export const Flash = () => { {(field, props) => ( { - e.preventDefault(); - deviceQuery.refetch(); - }} - startIcon={} - > - } - formStore={formStore} + loading={deviceQuery.isFetching} selectProps={props} label="Flash Disk" - value={String(field.value)} + labelProps={{ + labelAction: ( + + + } + /> {(network, index) => ( -
- - {(field, props) => ( - - )} - - - {(field, props) => ( -
+
+
+ + {(field, props) => ( - togglePasswordVisibility(index()) - } - startIcon={ - passwordVisibility()[index()] ? ( - - ) : ( - - ) - } - > - ), - }} + class="col-span-3" required /> -
- )} - -
- + )} + + + {(field, props) => ( +
+ + // togglePasswordVisibility(index()) + // } + // startIcon={ + // passwordVisibility()[index()] ? ( + // + // ) : ( + // + // ) + // } + // > + // ), + // }} + required + /> +
+ )} +
+
)} -
- -
-