diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSectionUpdate.tsx b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSectionUpdate.tsx index 98c69c2d1..cc4fe12c2 100644 --- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSectionUpdate.tsx +++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSectionUpdate.tsx @@ -1,9 +1,9 @@ import { createSignal, Show } from "solid-js"; import { Button } from "@/src/components/Button/Button"; -import { InstallModal } from "@/src/workflows/InstallMachine/InstallMachine"; import { useMachineName } from "@/src/hooks/clan"; import { useMachineStateQuery } from "@/src/hooks/queries"; import styles from "./SidebarSectionInstall.module.css"; +import { UpdateModal } from "@/src/workflows/InstallMachine/UpdateMachine"; export interface SidebarSectionUpdateProps { clanURI: string; @@ -26,7 +26,7 @@ export const SidebarSectionUpdate = (props: SidebarSectionUpdateProps) => { Update machine - setShowUpdate(false)} diff --git a/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.tsx b/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.tsx index 585514903..9a81182a3 100644 --- a/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.tsx +++ b/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.tsx @@ -111,10 +111,7 @@ export const AddMachine = (props: AddMachineProps) => { return defaultClass; } - switch (currentStep.id) { - default: - return defaultClass; - } + return defaultClass; }; return ( diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.module.css b/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.module.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.tsx b/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.tsx index 9b2a0ca0a..c48434a44 100644 --- a/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.tsx +++ b/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.tsx @@ -51,11 +51,11 @@ export interface InstallStoreType { progress: ApiCall<"run_machine_flash">; }; install: { - targetHost: string; + targetHost?: string; port?: string; password?: string; machineName: string; - mainDisk: string; + mainDisk?: string; // ...TODO Vars progress: ApiCall<"run_machine_install">; promptValues: PromptValues; diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/UpdateMachine.tsx b/pkgs/clan-app/ui/src/workflows/InstallMachine/UpdateMachine.tsx new file mode 100644 index 000000000..a70de06e0 --- /dev/null +++ b/pkgs/clan-app/ui/src/workflows/InstallMachine/UpdateMachine.tsx @@ -0,0 +1,242 @@ +import { Modal } from "@/src/components/Modal/Modal"; +import { + createStepper, + getStepStore, + StepperProvider, + useStepper, +} from "@/src/hooks/stepper"; +import { Show } from "solid-js"; +import { Dynamic } from "solid-js/web"; +import { ConfigureAddress, ConfigureData } from "./steps/installSteps"; + +import cx from "classnames"; +import { InstallStoreType } from "./InstallMachine"; +import { Typography } from "@/src/components/Typography/Typography"; +import { Button } from "@/src/components/Button/Button"; +import Icon from "@/src/components/Icon/Icon"; +import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify"; +import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar"; + +// TODO: Deduplicate +interface UpdateStepperProps { + onDone: () => void; +} +const UpdateStepper = (props: UpdateStepperProps) => { + const stepSignal = useStepper(); + + return ( + + ); +}; + +export interface UpdateModalProps { + machineName: string; + initialStep?: UpdateSteps[number]["id"]; + mount?: Node; + onClose?: () => void; + open: boolean; +} + +export const UpdateHeader = (props: { machineName: string }) => { + return ( + + Update: {props.machineName} + + ); +}; + +type UpdateTopic = [ + "generators", + "upload-secrets", + "nixos-anywhere", + "formatting", + "rebooting", + "installing", +][number]; + +const UpdateProgress = () => { + const stepSignal = useStepper(); + const [store, get] = getStepStore(stepSignal); + + const handleCancel = async () => { + const progress = store.install.progress; + if (progress) { + await progress.cancel(); + } + stepSignal.previous(); + }; + const updateState = + useNotifyOrigin>("run_machine_update"); + + return ( +
+ usb logo +
+ + Machine is being updated + + + + Update {updateState()?.topic}... + + +
+
+ ); +}; + +interface UpdateDoneProps { + onDone: () => void; +} +const UpdateDone = (props: UpdateDoneProps) => { + const stepSignal = useStepper(); + const [store, get] = getStepStore(stepSignal); + + return ( +
+
+
+ +
+ + Machine update finished! + +
+ +
+
+
+ ); +}; + +const steps = [ + { + id: "update:data", + title: UpdateHeader, + content: ConfigureData, + }, + { + id: "update:address", + title: UpdateHeader, + content: ConfigureAddress, + }, + { + id: "update:progress", + content: UpdateProgress, + isSplash: true, + class: "max-w-[30rem] h-[18rem]", + }, + { + id: "update:done", + content: UpdateDone, + isSplash: true, + class: "max-w-[30rem] h-[18rem]", + }, +] as const; + +export type UpdateSteps = typeof steps; +export type PromptValues = Record>; + +export const UpdateModal = (props: UpdateModalProps) => { + const stepper = createStepper( + { + steps, + }, + { + initialStep: props.initialStep || "update:data", + initialStoreData: { + install: { machineName: props.machineName }, + } as Partial, + }, + ); + + const MetaHeader = () => { + // @ts-expect-error some steps might not provide a title + const HeaderComponent = () => stepper.currentStep()?.title; + return ( + + {(C) => } + + ); + }; + const [store, set] = getStepStore(stepper); + + set("install", { machineName: props.machineName }); + + // allows each step to adjust the size of the modal + const sizeClasses = () => { + const defaultClass = "max-w-3xl h-[30rem]"; + + const currentStep = stepper.currentStep(); + if (!currentStep) { + return defaultClass; + } + + switch (currentStep.id) { + case "update:progress": + case "update:done": + return currentStep.class; + + default: + return defaultClass; + } + }; + + return ( + + { + console.log("Update modal closed"); + props.onClose?.(); + }} + open={props.open} + // @ts-expect-error some steps might not have + metaHeader={stepper.currentStep()?.title ? : undefined} + // @ts-expect-error some steps might not have + disablePadding={stepper.currentStep()?.isSplash} + > + props.onClose} /> + + + ); +}; diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx b/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx index 91cf750f9..9ba0a74a7 100644 --- a/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx +++ b/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx @@ -59,7 +59,7 @@ const ConfigureAdressSchema = v.object({ type ConfigureAdressForm = v.InferInput; -const ConfigureAddress = () => { +export const ConfigureAddress = (props: { next?: string }) => { const stepSignal = useStepper(); const [store, set] = getStepStore(stepSignal); @@ -134,7 +134,7 @@ const ConfigureAddress = () => { { !isReachable() || isReachable() !== getValue(formStore, "targetHost") } - fallback={Next} + fallback={ + {props.next || "next"} + } >