From 4cb029da27f7eb90655da303da45fe1765423f3c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 6 Jan 2025 10:16:26 +0100 Subject: [PATCH 1/6] UI: init slide animation keyframe --- pkgs/webview-ui/app/src/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkgs/webview-ui/app/src/index.css b/pkgs/webview-ui/app/src/index.css index fc082a8dd..15e304bca 100644 --- a/pkgs/webview-ui/app/src/index.css +++ b/pkgs/webview-ui/app/src/index.css @@ -22,6 +22,12 @@ src: url(../.fonts/ArchivoSemiCondensed-SemiBold.woff2) format("woff2"); } +@keyframes slide { + to { + background-position: 200% 0; + } +} + :root { --clr-bg-def-1: theme(colors.white); --clr-bg-def-2: theme(colors.secondary.50); From 4a8261e53bc05cd61f0c4002e6c5b60c9baecdb3 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 6 Jan 2025 10:16:40 +0100 Subject: [PATCH 2/6] UI: init badge component --- .../app/src/components/badge/index.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pkgs/webview-ui/app/src/components/badge/index.tsx diff --git a/pkgs/webview-ui/app/src/components/badge/index.tsx b/pkgs/webview-ui/app/src/components/badge/index.tsx new file mode 100644 index 000000000..451654106 --- /dev/null +++ b/pkgs/webview-ui/app/src/components/badge/index.tsx @@ -0,0 +1,39 @@ +import { JSX } from "solid-js"; +import cx from "classnames"; +import Icon, { IconVariant } from "../icon"; +import { Typography } from "../Typography"; + +interface BadgeProps { + color: keyof typeof colorMap; + children: JSX.Element; + icon?: IconVariant; + class?: string; +} + +const colorMap = { + primary: cx("bg-primary-800 text-primary-100"), + secondary: cx("bg-secondary-800 text-secondary-100"), + blue: "bg-blue-100 text-blue-800", + gray: "bg-gray-100 text-gray-800", + green: "bg-green-100 text-green-800", + orange: "bg-orange-100 text-orange-800", + red: "bg-red-100 text-red-800", + yellow: "bg-yellow-100 text-yellow-800", +}; + +export const Badge = (props: BadgeProps) => { + return ( +
+ {props.icon && } + + {props.children} + +
+ ); +}; From e3d29deb08dd816b4494197e9ce52b6cde46e8e3 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 6 Jan 2025 10:18:50 +0100 Subject: [PATCH 3/6] UI: init components {group,section,sectionHeader} --- .../app/src/components/group/index.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pkgs/webview-ui/app/src/components/group/index.tsx diff --git a/pkgs/webview-ui/app/src/components/group/index.tsx b/pkgs/webview-ui/app/src/components/group/index.tsx new file mode 100644 index 000000000..ef04e66f0 --- /dev/null +++ b/pkgs/webview-ui/app/src/components/group/index.tsx @@ -0,0 +1,60 @@ +import cx from "classnames"; +import { JSX } from "solid-js"; +import Icon, { IconVariant } from "../icon"; + +interface GroupProps { + children: JSX.Element; +} + +export const Group = (props: GroupProps) => ( +
+ {props.children} +
+); + +export type SectionVariant = "attention" | "danger"; + +interface SectionHeaderProps { + variant: SectionVariant; + headline: JSX.Element; +} +const variantColorsMap: Record = { + attention: cx("bg-[#9BD8F2] fg-def-1"), + danger: cx("bg-semantic-2 fg-semantic-2"), +}; + +const variantIconColorsMap: Record = { + attention: cx("fg-def-1"), + danger: cx("fg-semantic-3"), +}; + +const variantIconMap: Record = { + attention: "Attention", + danger: "Warning", +}; + +// SectionHeader component +export const SectionHeader = (props: SectionHeaderProps) => ( +
+ { + + } + {props.headline} +
+); + +// Section component +interface SectionProps { + children: JSX.Element; +} +export const Section = (props: SectionProps) => ( +
{props.children}
+); From f759edd4bbedc3ae40b226662db5afcb835b844d Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 6 Jan 2025 10:20:03 +0100 Subject: [PATCH 4/6] UI: typography add color: 'inherit' --- pkgs/webview-ui/app/src/components/Typography/index.tsx | 8 +++----- pkgs/webview-ui/app/src/components/group/index.tsx | 2 +- pkgs/webview-ui/app/src/components/inputBase/index.tsx | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pkgs/webview-ui/app/src/components/Typography/index.tsx b/pkgs/webview-ui/app/src/components/Typography/index.tsx index 29957aa35..fcc829ab0 100644 --- a/pkgs/webview-ui/app/src/components/Typography/index.tsx +++ b/pkgs/webview-ui/app/src/components/Typography/index.tsx @@ -80,14 +80,11 @@ interface _TypographyProps { size: AllowedSizes; children: JSX.Element; weight?: Weight; - color?: Color; + color?: Color | "inherit"; inverted?: boolean; tag?: Tag; class?: string; classList?: Record; - // Disable using the color prop - // A font color is provided via class / classList or inherited - useExternColor?: boolean; } export const Typography = (props: _TypographyProps) => { @@ -95,7 +92,8 @@ export const Typography = (props: _TypographyProps) => { (
{ diff --git a/pkgs/webview-ui/app/src/components/inputBase/index.tsx b/pkgs/webview-ui/app/src/components/inputBase/index.tsx index eda2298ff..b879e7550 100644 --- a/pkgs/webview-ui/app/src/components/inputBase/index.tsx +++ b/pkgs/webview-ui/app/src/components/inputBase/index.tsx @@ -132,7 +132,7 @@ export const InputLabel = (props: InputLabelProps) => { {props.required && ( Date: Mon, 6 Jan 2025 10:21:51 +0100 Subject: [PATCH 5/6] UI: fix select render nested portal within dialog --- pkgs/webview-ui/app/src/Form/fields/Select.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkgs/webview-ui/app/src/Form/fields/Select.tsx b/pkgs/webview-ui/app/src/Form/fields/Select.tsx index 26990bbcf..5b92cf391 100644 --- a/pkgs/webview-ui/app/src/Form/fields/Select.tsx +++ b/pkgs/webview-ui/app/src/Form/fields/Select.tsx @@ -18,6 +18,7 @@ import { } from "@/src/components/inputBase"; import { FieldLayout } from "./layout"; import Icon from "@/src/components/icon"; +import { useContext } from "corvu/dialog"; export interface Option { value: string; @@ -45,9 +46,12 @@ interface SelectInputpProps { placeholder?: string; multiple?: boolean; loading?: boolean; + dialogContextId?: string; } export function SelectInput(props: SelectInputpProps) { + const dialogContext = useContext(props.dialogContextId); + const _id = createUniqueId(); const [reference, setReference] = createSignal(); @@ -223,7 +227,7 @@ export function SelectInput(props: SelectInputpProps) { } /> - +
); - return ( -
-
- - Install:{" "} - - - {props.name} - -
- {/* Stepper container */} -
- {/* A Step with a circle a number inside. Label is below */} - - {([idx, label]) => ( -
- - = step()} - fallback={} - > - {idx} - - - - {label} - -
- )} -
-
-
- - - handleNext()} - footer={
} - /> - - - - - Single Disk - - - Change schema - - - - - -
- - -
- - Hardware Report - - - Target} - field={ - - 192.157.124.81 - - } - > - -
-
- - Disk Configuration - - - Disk Layout} - field={ - - Single Disk - - } - > -
- Main Disk} - field={ - - Samsung evo 850 efkjhasd - - } - > -
-
- + return ( + + {/* Register each step as form field */} + {/* @ts-expect-error: object type is not statically supported */} + {(field, fieldProps) => <>} + {/* @ts-expect-error: object type is not statically supported */} + {(field, fieldProps) => <>} + + {/* Modal Header */} +
+ + Install:{" "} + + + {props.name} + +
+ {/* Stepper header */} +
+ + {([idx, label]) => ( +
- Setup your device. + = step()} + fallback={} + > + {idx} + - This will erase the disk and bootstrap fresh. + {label} - - } - /> -
- - - -
-
+
+ )} + +
+ +
+ + + { + const prev = getValue(formStore, "1"); + setValue(formStore, "1", { ...prev, ...data }); + handleNext(); + }} + initial={ + getValue(formStore, "1") || { + target: props.targetHost || "", + report: false, + } + } + footer={
} + /> + + + } + handleNext={(data) => { + const prev = getValue(formStore, "2"); + setValue(formStore, "2", { ...prev, ...data }); + handleNext(); + }} + initial={getValue(formStore, "2")} + /> + + + handleNext()} + // @ts-expect-error: This cannot be known. + initial={getValues(formStore)} + footer={ +
+ + +
+ } + /> +
+ +
+ + } + > + +
+
+ + Install: + + + {props.name} + +
+
+ +
+ + {progressText()} + + +
+
+
+ ); }; @@ -401,7 +366,6 @@ const MachineForm = (props: MachineDetailsProps) => { initialValues: props.initialData, }); - const sshKey = () => getValue(formStore, "sshKey"); const targetHost = () => getValue(formStore, "machine.deploy.targetHost"); const machineName = () => getValue(formStore, "machine.name") || props.initialData.machine.name; @@ -479,9 +443,10 @@ const MachineForm = (props: MachineDetailsProps) => { }; return ( <> -
- General - +
+ + +
{(field, props) => ( @@ -564,37 +529,6 @@ const MachineForm = (props: MachineDetailsProps) => { /> )} - - {(field, props) => ( - <> - { - event.preventDefault(); // Prevent the native file dialog from opening - const input = event.target; - const files = await selectSshKeys(); - - // Set the files - Object.defineProperty(input, "files", { - value: files, - writable: true, - }); - // Define the files property on the input element - const changeEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - input.dispatchEvent(changeEvent); - }} - placeholder={"When empty the default key(s) will be used"} - value={field.value} - error={field.error} - helperText="Provide the SSH key used to connect to the machine" - label="SSH Key" - /> - - )} -
@@ -641,9 +575,7 @@ const MachineForm = (props: MachineDetailsProps) => { > @@ -692,140 +624,16 @@ export const MachineDetails = () => { return ( <>
-
- } - > - {(data) => ( - <> - - - )} - -
+ } + > + {(data) => ( + <> + + + )} + ); }; - -interface Wifi extends FieldValues { - name: string; - ssid?: string; - password?: string; -} - -interface WifiForm extends FieldValues { - networks: Wifi[]; -} - -interface MachineWifiProps { - base_url: string; - machine_name: string; - initialData: Wifi[]; -} -function WifiModule(props: MachineWifiProps) { - // You can use formData to initialize your form fields: - // const initialFormState = formData(); - - const [formStore, { Form, Field }] = createForm({ - initialValues: { - networks: props.initialData, - }, - }); - - const [nets, setNets] = createSignal<1[]>( - new Array(props.initialData.length || 1).fill(1), - ); - - const handleSubmit = async (values: WifiForm) => { - const networks = values.networks - .filter((i) => i.ssid) - .reduce( - (acc, curr) => ({ - ...acc, - [curr.ssid || ""]: { ssid: curr.ssid, password: curr.password }, - }), - {}, - ); - - console.log("submitting", values, networks); - // const r = await callApi("set_iwd_service_for_machine", { - // base_url: props.base_url, - // machine_name: props.machine_name, - // networks: networks, - // }); - // if (r.status === "error") { - toast.error("Failed to set wifi. Feature disabled temporarily"); - // } - // if (r.status === "success") { - // toast.success("Wifi set successfully"); - // } - }; - - return ( - - Preconfigure wireless networks - - {(_, idx) => ( -
- - {(field, props) => ( - - )} - - - {(field, props) => ( - - )} - - -
- )} -
- - { -
- -
- } - - ); -} diff --git a/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx new file mode 100644 index 000000000..a23e9388b --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx @@ -0,0 +1,110 @@ +import { callApi } from "@/src/api"; +import { + createForm, + SubmitHandler, + validate, + required, + FieldValues, +} from "@modular-forms/solid"; +import { createQuery } from "@tanstack/solid-query"; +import { StepProps } from "./hardware-step"; +import { SelectInput } from "@/src/Form/fields/Select"; +import { Typography } from "@/src/components/Typography"; +import { Group } from "@/src/components/group"; + +export interface DiskValues extends FieldValues { + placeholders: { + mainDisk: string; + }; + schema: string; +} +export const DiskStep = (props: StepProps) => { + const [formStore, { Form, Field }] = createForm({ + initialValues: { ...props.initial, schema: "single-disk" }, + }); + + const handleSubmit: SubmitHandler = async (values, event) => { + console.log("Submit Disk", { values }); + const valid = await validate(formStore); + console.log("Valid", valid); + if (!valid) return; + props.handleNext(values); + }; + + const diskSchemaQuery = createQuery(() => ({ + queryKey: [props.dir, props.machine_id, "disk_schemas"], + queryFn: async () => { + const result = await callApi("get_disk_schemas", { + base_path: props.dir, + machine_name: props.machine_id, + }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + })); + + return ( +
+ + + {(field, fieldProps) => ( + <> + + {(field.value || "No schema selected").split("-").join(" ")} + + + Change schema + + + )} + + + + + {(field, fieldProps) => ( + ({ label: o, value: o })) || [ + { label: "No options", value: "" }, + ] + } + // options={ + // deviceQuery.data?.blockdevices.map((d) => ({ + // value: d.path, + // label: `${d.path} -- ${d.size} bytes`, + // })) || [] + // } + error={field.error} + label="Main Disk" + value={field.value || ""} + placeholder="Select a disk" + selectProps={fieldProps} + required + /> + )} + + + {props.footer} +
+ ); +}; diff --git a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx index 8ce0ab499..e29577020 100644 --- a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx @@ -16,33 +16,34 @@ import { } from "@modular-forms/solid"; import { createEffect, createSignal, JSX, Match, Switch } from "solid-js"; import toast from "solid-toast"; -import { Group } from "../details"; import { TextInput } from "@/src/Form/fields"; import { createQuery } from "@tanstack/solid-query"; +import { Badge } from "@/src/components/badge"; +import { Group } from "@/src/components/group"; -interface Hardware extends FieldValues { +export type HardwareValues = FieldValues & { report: boolean; target: string; -} +}; -export interface StepProps { +export interface StepProps { machine_id: string; dir: string; - handleNext: () => void; + handleNext: (data: T) => void; footer: JSX.Element; - initial?: Partial; + initial?: T; } -export const HWStep = (props: StepProps) => { - const [formStore, { Form, Field }] = createForm({ - initialValues: props.initial || {}, +export const HWStep = (props: StepProps) => { + const [formStore, { Form, Field }] = createForm({ + initialValues: (props.initial as HardwareValues) || {}, }); - const handleSubmit: SubmitHandler = async (values, event) => { + const handleSubmit: SubmitHandler = async (values, event) => { console.log("Submit Hardware", { values }); const valid = await validate(formStore); console.log("Valid", valid); if (!valid) return; - props.handleNext(); + props.handleNext(values); }; const [isGenerating, setIsGenerating] = createSignal(false); @@ -148,20 +149,46 @@ export const HWStep = (props: StepProps) => { <> + + No report + -
Detected
+ + Report detected + +
-
Nixos report Detected
+ + Legacy Report detected + +
diff --git a/pkgs/webview-ui/app/src/routes/machines/install/summary-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/summary-step.tsx new file mode 100644 index 000000000..01dc27518 --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/machines/install/summary-step.tsx @@ -0,0 +1,99 @@ +import { StepProps } from "./hardware-step"; +import { Typography } from "@/src/components/Typography"; +import { FieldLayout } from "@/src/Form/fields/layout"; +import { InputLabel } from "@/src/components/inputBase"; +import { Group, Section, SectionHeader } from "@/src/components/group"; +import { AllStepsValues } from "../details"; +import { Badge } from "@/src/components/badge"; +import Icon from "@/src/components/icon"; + +export const SummaryStep = (props: StepProps) => { + const hwValues = () => props.initial?.["1"]; + const diskValues = () => props.initial?.["2"]; + return ( + <> +
+ + Hardware Report + + + Detected} + field={ + hwValues()?.report ? ( + + + + ) : ( + + + + ) + } + > + Target} + field={ + + {hwValues()?.target} + + } + > + +
+
+ + Disk Configuration + + + Disk Layout} + field={ + + {diskValues()?.schema} + + } + > +
+ Main Disk} + field={ + + {diskValues()?.placeholders.mainDisk} + + } + > +
+
+ + + Setup your device. + + + This will erase the disk and bootstrap fresh. + + + } + /> + {props.footer} + + ); +};