From 78162f74795bd19bdf60ff6bbfc605ae4edc00a9 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 3 Jan 2025 16:38:50 +0100 Subject: [PATCH] UI: refactor machine install workflow into modal with steps --- .../app/src/routes/machines/details.tsx | 380 +++++++++++++----- .../routes/machines/install/hardware-step.tsx | 179 +++++++++ 2 files changed, 454 insertions(+), 105 deletions(-) create mode 100644 pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index f7ca5ac93..d2cd4420e 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -2,7 +2,7 @@ import { callApi, SuccessData, SuccessQuery } from "@/src/api"; import { activeURI } from "@/src/App"; import { Button } from "@/src/components/button"; import { FileInput } from "@/src/components/FileInput"; -import Icon from "@/src/components/icon"; +import Icon, { IconVariant } from "@/src/components/icon"; import { TextInput } from "@/src/Form/fields/TextInput"; import { selectSshKeys } from "@/src/hooks"; import { @@ -13,12 +13,17 @@ import { } from "@modular-forms/solid"; import { useParams } from "@solidjs/router"; import { createQuery } from "@tanstack/solid-query"; -import { createSignal, For, Show } from "solid-js"; +import { createSignal, For, JSX, Match, Show, Switch } from "solid-js"; import toast from "solid-toast"; import { MachineAvatar } from "./avatar"; import { Header } from "@/src/layout/header"; import { InputLabel } from "@/src/components/inputBase"; import { FieldLayout } from "@/src/Form/fields/layout"; +import { Modal } from "@/src/components/modal"; +import { Typography } from "@/src/components/Typography"; +import cx from "classnames"; +import { SelectInput } from "@/src/Form/fields/Select"; +import { HWStep } from "./install/hardware-step"; type MachineFormInterface = MachineData & { sshKey?: File; @@ -33,6 +38,65 @@ interface InstallForm extends FieldValues { disk?: string; } +interface GroupProps { + children: JSX.Element; +} +export const Group = (props: GroupProps) => ( +
+ {props.children} +
+); + +type AdmonitionVariant = "attention" | "danger"; +interface SectionHeaderProps { + variant: AdmonitionVariant; + 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", +}; + +export const SectionHeader = (props: SectionHeaderProps) => ( +
+ { + + } + {props.headline} +
+); + +const steps = { + "1": "Hardware detection", + "2": "Disk schema", + "3": "Installation", +}; + +interface SectionProps { + children: JSX.Element; +} +const Section = (props: SectionProps) => ( +
{props.children}
+); + interface InstallMachineProps { name?: string; targetHost?: string | null; @@ -90,7 +154,6 @@ const InstallMachine = (props: InstallMachineProps) => { }; const handleDiskConfirm = async (e: Event) => { - e.preventDefault(); const curr_uri = activeURI(); const disk = getValue(formStore, "disk"); const disk_id = props.disks.find((d) => d.name === disk)?.id_link; @@ -98,9 +161,9 @@ const InstallMachine = (props: InstallMachineProps) => { return; } }; + const [stepsDone, setStepsDone] = createSignal([]); const generateReport = async (e: Event) => { - e.preventDefault(); const curr_uri = activeURI(); if (!curr_uri || !props.name) { return; @@ -113,7 +176,7 @@ const InstallMachine = (props: InstallMachineProps) => { machine: props.name, keyfile: props.sshKey?.name, target_host: props.targetHost, - backend: "NIXOS_FACTER", + backend: "nixos-facter", }, }); toast.dismiss(loading_toast); @@ -126,97 +189,202 @@ const InstallMachine = (props: InstallMachineProps) => { toast.success("Report generated successfully"); } }; + + type StepIdx = keyof typeof steps; + const [step, setStep] = createSignal("1"); + + const handleNext = () => { + console.log("Next"); + setStep((c) => `${+c + 1}` as StepIdx); + }; + const handlePrev = () => { + console.log("Next"); + setStep((c) => `${+c - 1}` as StepIdx); + }; + + const Footer = () => ( +
+ + +
+ ); return ( - <> -
-

- Install: +
+
+ + Install:{" "} + + {props.name} -

-

- Install the system for the first time. This will erase the disk and - bootstrap a new device. -

+ + + {/* Stepper container */} +
+ {/* A Step with a circle a number inside. Label is below */} + + {([idx, label]) => ( +
+ + = step()} + fallback={} + > + {idx} + + + + {label} + +
+ )} +
+
-
-
Hardware detection
-
-
- -
-
-
Disk schema
-
-
- -
-
-
- - {(field, fieldProps) => "disk"} - -
- - + Disk Configuration + + + Disk Layout} + field={ + + Single Disk + + } + > +
+ Main Disk} + field={ + + Samsung evo 850 efkjhasd + + } + > +
+ + + + Setup your device. + + + This will erase the disk and bootstrap fresh. + + + } + /> +
+ + + + + ); }; @@ -235,6 +403,8 @@ const MachineForm = (props: MachineDetailsProps) => { const machineName = () => getValue(formStore, "machine.name") || props.initialData.machine.name; + const [installModalOpen, setInstallModalOpen] = createSignal(false); + const handleSubmit = async (values: MachineFormInterface) => { console.log("submitting", values); @@ -309,7 +479,7 @@ const MachineForm = (props: MachineDetailsProps) => {
General -
+ {(field, props) => ( { class="w-full" // disabled={!online()} onClick={() => { - const modal = document.getElementById( - "install_modal", - ) as HTMLDialogElement | null; - modal?.showModal(); + setInstallModalOpen(true); }} endIcon={} > @@ -463,16 +630,19 @@ const MachineForm = (props: MachineDetailsProps) => {
- - - + setInstallModalOpen(false)} + class="min-w-[600px]" + > + + Update the system if changes should be synced after the installation 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 new file mode 100644 index 000000000..8ce0ab499 --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx @@ -0,0 +1,179 @@ +import { callApi } from "@/src/api"; +import { activeURI } from "@/src/App"; +import { Button } from "@/src/components/button"; +import Icon from "@/src/components/icon"; +import { InputError, InputLabel } from "@/src/components/inputBase"; +import { FieldLayout } from "@/src/Form/fields/layout"; +import { + createForm, + SubmitHandler, + FieldValues, + validate, + required, + getValue, + submit, + setValue, +} 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"; + +interface Hardware extends FieldValues { + report: boolean; + target: string; +} + +export interface StepProps { + machine_id: string; + dir: string; + handleNext: () => void; + footer: JSX.Element; + initial?: Partial; +} +export const HWStep = (props: StepProps) => { + const [formStore, { Form, Field }] = createForm({ + initialValues: props.initial || {}, + }); + + 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(); + }; + + const [isGenerating, setIsGenerating] = createSignal(false); + + const hwReportQuery = createQuery(() => ({ + queryKey: [props.dir, props.machine_id, "hw_report"], + queryFn: async () => { + const result = await callApi("show_machine_hardware_config", { + clan_dir: props.dir, + machine_name: props.machine_id, + }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + })); + // Workaround to set the form state + createEffect(() => { + const report = hwReportQuery.data; + if (report === "nixos-facter" || report === "nixos-generate-config") { + setValue(formStore, "report", true); + } + }); + + const generateReport = async (e: Event) => { + const curr_uri = activeURI(); + if (!curr_uri) return; + + const loading_toast = toast.loading("Generating hardware report..."); + + await validate(formStore, "target"); + const target = getValue(formStore, "target"); + + if (!target) { + toast.error("Target ip must be provided"); + return; + } + setIsGenerating(true); + const r = await callApi("generate_machine_hardware_info", { + opts: { + flake: { loc: curr_uri }, + machine: props.machine_id, + target_host: target, + backend: "nixos-facter", + }, + }); + setIsGenerating(false); + toast.dismiss(loading_toast); + // TODO: refresh the machine details + + if (r.status === "error") { + toast.error(`Failed to generate report. ${r.errors[0].message}`); + } + if (r.status === "success") { + toast.success("Report generated successfully"); + } + hwReportQuery.refetch(); + submit(formStore); + }; + + return ( + + + + {(field, fieldProps) => ( + + )} + + + + + {(field, fieldProps) => ( + } + label={ + + Hardware report + + } + field={ + + +
Loading...
+
+ +
Error...
+
+ + {(data) => ( + <> + + + + + +
Detected
+
+ +
Nixos report Detected
+
+
+ + )} +
+
+ } + /> + )} +
+
+ {props.footer} + + ); +};