diff --git a/pkgs/clan-app/ui/src/workflows/Install/install.tsx b/pkgs/clan-app/ui/src/workflows/Install/install.tsx index e7251870c..841f551be 100644 --- a/pkgs/clan-app/ui/src/workflows/Install/install.tsx +++ b/pkgs/clan-app/ui/src/workflows/Install/install.tsx @@ -1,58 +1,15 @@ -import { Button } from "@/src/components/Button/Button"; -import { Divider } from "@/src/components/Divider/Divider"; -import { Fieldset } from "@/src/components/Form/Fieldset"; -import { HostFileInput } from "@/src/components/Form/HostFileInput"; import { Modal } from "@/src/components/Modal/Modal"; -import { Select } from "@/src/components/Select/Select"; -import { Typography } from "@/src/components/Typography/Typography"; -import { callApi } from "@/src/hooks/api"; import { createStepper, StepperProvider, useStepper, } from "@/src/hooks/stepper"; -import { - createForm, - FieldValues, - getError, - SubmitHandler, - valiForm, -} from "@modular-forms/solid"; -import { JSX, Show } from "solid-js"; +import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid"; +import { Show } from "solid-js"; import { Dynamic } from "solid-js/web"; -import * as v from "valibot"; - -const CreateFlashSchema = v.object({ - ssh_key: v.pipe( - v.string("Please select a key."), - v.nonEmpty("Please select a key."), - ), - language: v.pipe(v.string(), v.nonEmpty("Please choose a language.")), - keymap: v.pipe(v.string(), v.nonEmpty("Please select a keyboard layout.")), -}); - -export const InstallHeader = (props: { machineName: string }) => { - return ( - - Installing: {props.machineName} - - ); -}; - -export const CreateHeader = (props: { machineName: string }) => { - return ( -
- - Create installer - -
- ); -}; +import { InitialStep } from "./steps/Initial"; +import { createInstallerSteps } from "./steps/createInstaller"; +import { installSteps } from "./steps/installSteps"; interface InstallForm extends FieldValues { data_from_step_1: string; @@ -60,23 +17,6 @@ interface InstallForm extends FieldValues { data_from_step_3?: string; } -type NextButtonProps = JSX.ButtonHTMLAttributes & {}; - -const NextButton = (props: NextButtonProps) => { - const stepSignal = useStepper(); - return ( - - ); -}; - const InstallStepper = () => { const stepSignal = useStepper(); @@ -98,304 +38,12 @@ const InstallStepper = () => { ); }; -export const BackButton = () => { - const stepSignal = useStepper(); - return ( - - ); -}; - export interface InstallModalProps { machineName: string; initialStep?: string; } -const InitialChoice = () => { - const stepSignal = useStepper(); - return ( -
-
-
-
- - Remote setup - - - Is your machine currently online? Does it have an IP-address, can - you SSH into it? And does it support Kexec? - -
- -
- - -
- - I don't have an installer, yet - - -
-
-
- ); -}; - -type FlashFormType = v.InferInput; - -const CreateIso = () => { - const [formStore, { Form, Field }] = createForm({ - validate: valiForm(CreateFlashSchema), - }); - const stepSignal = useStepper(); - - // TODO: push values to the parent form Store - const handleSubmit: SubmitHandler = (values, event) => { - console.log("ISO creation submitted", values); - // Here you would typically trigger the ISO creation process - stepSignal.next(); - }; - - const onSelectFile = async () => { - const req = callApi("get_system_file", { - file_request: { - mode: "select_folder", - title: "Select a folder for you new Clan", - }, - }); - - const resp = await req.result; - - if (resp.status === "error") { - // just throw the first error, I can't imagine why there would be multiple - // errors for this call - throw new Error(resp.errors[0].message); - } - - if (resp.status === "success" && resp.data) { - return resp.data[0]; - } - - throw new Error("No data returned from api call"); - }; - - return ( -
- -
- - {(field, input) => ( - - )} - -
-
- - {(field, props) => ( - - )} - -
- - } - footer={ -
- - -
- } - /> - - ); -}; - -interface StepLayoutProps { - body: JSX.Element; - footer: JSX.Element; -} -const StepLayout = (props: StepLayoutProps) => { - return ( -
- {props.body} - {props.footer} -
- ); -}; - -const steps = [ - { - id: "init", - content: InitialChoice, - }, - { - id: "create:iso-0", - content: () => ( - -
-
- - Create a portable installer - - - Grab a disposable USB stick and plug it in - -
-
-
- - We will erase everything on it during this process - - - Create a portable installer tool that can turn any machine into - a fully configured Clan machine. - -
- - } - footer={} - /> - ), - }, - { - id: "create:iso-1", - title: CreateHeader, - content: CreateIso, - }, - { - id: "install:machine-0", - title: InstallHeader, - content: () => ( -
- Enter the targetHost - -
- ), - }, - { - id: "install:confirm", - title: InstallHeader, - content: (props: { machineName: string }) => ( -
- Confirm the installation of {props.machineName} - -
- ), - }, - { - id: "install:progress", - title: InstallHeader, - content: () => ( -
-

Installation in progress...

-

Please wait while we set up your machine.

-
- ), - }, -] as const; - -const RegularFooter = () => { - const stepper = useStepper(); - return ( -
- - stepper.next()} /> -
- ); -}; +const steps = [InitialStep, ...createInstallerSteps, ...installSteps] as const; export type InstallSteps = typeof steps; diff --git a/pkgs/clan-app/ui/src/workflows/Install/steps/Initial.tsx b/pkgs/clan-app/ui/src/workflows/Install/steps/Initial.tsx new file mode 100644 index 000000000..e12054206 --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/Install/steps/Initial.tsx @@ -0,0 +1,64 @@ +import { useStepper } from "@/src/hooks/stepper"; +import { InstallSteps } from "../install"; +import { Typography } from "@/src/components/Typography/Typography"; +import { Button } from "@/src/components/Button/Button"; +import { Divider } from "@/src/components/Divider/Divider"; + +const InitialChoice = () => { + const stepSignal = useStepper(); + return ( +
+
+
+
+ + Remote setup + + + Is your machine currently online? Does it have an IP-address, can + you SSH into it? And does it support Kexec? + +
+ +
+ + +
+ + I don't have an installer, yet + + +
+
+
+ ); +}; + +export const InitialStep = { + id: "init", + content: InitialChoice, +}; diff --git a/pkgs/clan-app/ui/src/workflows/Install/steps/createInstaller.tsx b/pkgs/clan-app/ui/src/workflows/Install/steps/createInstaller.tsx new file mode 100644 index 000000000..50dbfdefd --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/Install/steps/createInstaller.tsx @@ -0,0 +1,206 @@ +import { useStepper } from "@/src/hooks/stepper"; +import { + createForm, + getError, + SubmitHandler, + valiForm, +} from "@modular-forms/solid"; +import * as v from "valibot"; +import { InstallSteps } from "../install"; +import { callApi } from "@/src/hooks/api"; +import { Fieldset } from "@/src/components/Form/Fieldset"; +import { HostFileInput } from "@/src/components/Form/HostFileInput"; +import { Select } from "@/src/components/Select/Select"; +import { BackButton, NextButton, StepFooter, StepLayout } from "../../Steps"; +import { Typography } from "@/src/components/Typography/Typography"; + +const CreateHeader = (props: { machineName: string }) => { + return ( +
+ + Create installer + +
+ ); +}; + +const CreateFlashSchema = v.object({ + ssh_key: v.pipe( + v.string("Please select a key."), + v.nonEmpty("Please select a key."), + ), + language: v.pipe(v.string(), v.nonEmpty("Please choose a language.")), + keymap: v.pipe(v.string(), v.nonEmpty("Please select a keyboard layout.")), +}); + +type FlashFormType = v.InferInput; + +const CreateIso = () => { + const [formStore, { Form, Field }] = createForm({ + validate: valiForm(CreateFlashSchema), + }); + const stepSignal = useStepper(); + + // TODO: push values to the parent form Store + const handleSubmit: SubmitHandler = (values, event) => { + console.log("ISO creation submitted", values); + // Here you would typically trigger the ISO creation process + stepSignal.next(); + }; + + const onSelectFile = async () => { + const req = callApi("get_system_file", { + file_request: { + mode: "select_folder", + title: "Select a folder for you new Clan", + }, + }); + + const resp = await req.result; + + if (resp.status === "error") { + // just throw the first error, I can't imagine why there would be multiple + // errors for this call + throw new Error(resp.errors[0].message); + } + + if (resp.status === "success" && resp.data) { + return resp.data[0]; + } + + throw new Error("No data returned from api call"); + }; + + return ( +
+ +
+ + {(field, input) => ( + + )} + +
+
+ + {(field, props) => ( + + )} + +
+ + } + footer={ +
+ + +
+ } + /> + + ); +}; + +export const createInstallerSteps = [ + { + id: "create:iso-0", + content: () => ( + +
+
+ + Create a portable installer + + + Grab a disposable USB stick and plug it in + +
+
+
+ + We will erase everything on it during this process + + + Create a portable installer tool that can turn any machine into + a fully configured Clan machine. + +
+ + } + footer={} + /> + ), + }, + { + id: "create:iso-1", + title: CreateHeader, + content: CreateIso, + }, +] as const; diff --git a/pkgs/clan-app/ui/src/workflows/Install/steps/installSteps.tsx b/pkgs/clan-app/ui/src/workflows/Install/steps/installSteps.tsx new file mode 100644 index 000000000..17bde23d2 --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/Install/steps/installSteps.tsx @@ -0,0 +1,43 @@ +import { Typography } from "@/src/components/Typography/Typography"; +import { NextButton } from "../../Steps"; + +export const InstallHeader = (props: { machineName: string }) => { + return ( + + Installing: {props.machineName} + + ); +}; + +export const installSteps = [ + { + id: "install:machine-0", + title: InstallHeader, + content: () => ( +
+ Enter the targetHost + +
+ ), + }, + { + id: "install:confirm", + title: InstallHeader, + content: (props: { machineName: string }) => ( +
+ Confirm the installation of {props.machineName} + +
+ ), + }, + { + id: "install:progress", + title: InstallHeader, + content: () => ( +
+

Installation in progress...

+

Please wait while we set up your machine.

+
+ ), + }, +] as const; diff --git a/pkgs/clan-app/ui/src/workflows/Steps.tsx b/pkgs/clan-app/ui/src/workflows/Steps.tsx new file mode 100644 index 000000000..004e84bf9 --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/Steps.tsx @@ -0,0 +1,70 @@ +import { JSX } from "solid-js"; +import { useStepper } from "../hooks/stepper"; +import { Button } from "../components/Button/Button"; +import { InstallSteps } from "./Install/install"; + +interface StepLayoutProps { + body: JSX.Element; + footer: JSX.Element; +} +export const StepLayout = (props: StepLayoutProps) => { + return ( +
+ {props.body} + {props.footer} +
+ ); +}; + +type NextButtonProps = JSX.ButtonHTMLAttributes & {}; + +export const NextButton = (props: NextButtonProps) => { + // TODO: Make this type generic + const stepSignal = useStepper(); + return ( + + ); +}; + +export const BackButton = () => { + const stepSignal = useStepper(); + return ( + + ); +}; + +/** + * Renders a footer with Back and Next buttons. + * The Next button will trigger the next step in the stepper. + * The Back button will go to the previous step. + * + * Does not trigger submission on any form + * + * Use this for overview steps where no form submission is required. + */ +export const StepFooter = () => { + const stepper = useStepper(); + return ( +
+ + stepper.next()} /> +
+ ); +};