ui/update: init update machine
This commit is contained in:
@@ -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
|
||||
</Button>
|
||||
<Show when={showUpdate()}>
|
||||
<InstallModal
|
||||
<UpdateModal
|
||||
open={showUpdate()}
|
||||
machineName={useMachineName()}
|
||||
onClose={() => setShowUpdate(false)}
|
||||
|
||||
@@ -111,10 +111,7 @@ export const AddMachine = (props: AddMachineProps) => {
|
||||
return defaultClass;
|
||||
}
|
||||
|
||||
switch (currentStep.id) {
|
||||
default:
|
||||
return defaultClass;
|
||||
}
|
||||
return defaultClass;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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;
|
||||
|
||||
242
pkgs/clan-app/ui/src/workflows/InstallMachine/UpdateMachine.tsx
Normal file
242
pkgs/clan-app/ui/src/workflows/InstallMachine/UpdateMachine.tsx
Normal file
@@ -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<UpdateSteps>();
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
component={stepSignal.currentStep().content}
|
||||
onDone={props.onDone}
|
||||
next="update"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface UpdateModalProps {
|
||||
machineName: string;
|
||||
initialStep?: UpdateSteps[number]["id"];
|
||||
mount?: Node;
|
||||
onClose?: () => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export const UpdateHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
<Typography hierarchy="label" size="default">
|
||||
Update: {props.machineName}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
type UpdateTopic = [
|
||||
"generators",
|
||||
"upload-secrets",
|
||||
"nixos-anywhere",
|
||||
"formatting",
|
||||
"rebooting",
|
||||
"installing",
|
||||
][number];
|
||||
|
||||
const UpdateProgress = () => {
|
||||
const stepSignal = useStepper<UpdateSteps>();
|
||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
const handleCancel = async () => {
|
||||
const progress = store.install.progress;
|
||||
if (progress) {
|
||||
await progress.cancel();
|
||||
}
|
||||
stepSignal.previous();
|
||||
};
|
||||
const updateState =
|
||||
useNotifyOrigin<ProcessMessage<unknown, UpdateTopic>>("run_machine_update");
|
||||
|
||||
return (
|
||||
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
|
||||
<img
|
||||
src="/logos/usb-stick-min.png"
|
||||
alt="usb logo"
|
||||
class="absolute top-2 z-0"
|
||||
/>
|
||||
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
size="default"
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Machine is being updated
|
||||
</Typography>
|
||||
<LoadingBar />
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="default"
|
||||
class=""
|
||||
color="secondary"
|
||||
inverted
|
||||
>
|
||||
Update {updateState()?.topic}...
|
||||
</Typography>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
class="mt-3 w-fit"
|
||||
size="s"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UpdateDoneProps {
|
||||
onDone: () => void;
|
||||
}
|
||||
const UpdateDone = (props: UpdateDoneProps) => {
|
||||
const stepSignal = useStepper<UpdateSteps>();
|
||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
return (
|
||||
<div class="flex size-full flex-col items-center justify-center bg-inv-4">
|
||||
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
|
||||
<div class="rounded-full bg-semantic-success-4">
|
||||
<Icon icon="Checkmark" class="size-9" />
|
||||
</div>
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
size="default"
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Machine update finished!
|
||||
</Typography>
|
||||
<div class="mt-3 flex w-full justify-center">
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
endIcon="Close"
|
||||
size="s"
|
||||
onClick={() => props.onDone()}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, Record<string, string>>;
|
||||
|
||||
export const UpdateModal = (props: UpdateModalProps) => {
|
||||
const stepper = createStepper(
|
||||
{
|
||||
steps,
|
||||
},
|
||||
{
|
||||
initialStep: props.initialStep || "update:data",
|
||||
initialStoreData: {
|
||||
install: { machineName: props.machineName },
|
||||
} as Partial<InstallStoreType>,
|
||||
},
|
||||
);
|
||||
|
||||
const MetaHeader = () => {
|
||||
// @ts-expect-error some steps might not provide a title
|
||||
const HeaderComponent = () => stepper.currentStep()?.title;
|
||||
return (
|
||||
<Show when={HeaderComponent()}>
|
||||
{(C) => <Dynamic component={C()} machineName={props.machineName} />}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
const [store, set] = getStepStore<InstallStoreType>(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 (
|
||||
<StepperProvider stepper={stepper}>
|
||||
<Modal
|
||||
class={cx("w-screen", sizeClasses())}
|
||||
title="Update machine"
|
||||
onClose={() => {
|
||||
console.log("Update modal closed");
|
||||
props.onClose?.();
|
||||
}}
|
||||
open={props.open}
|
||||
// @ts-expect-error some steps might not have
|
||||
metaHeader={stepper.currentStep()?.title ? <MetaHeader /> : undefined}
|
||||
// @ts-expect-error some steps might not have
|
||||
disablePadding={stepper.currentStep()?.isSplash}
|
||||
>
|
||||
<UpdateStepper onDone={() => props.onClose} />
|
||||
</Modal>
|
||||
</StepperProvider>
|
||||
);
|
||||
};
|
||||
@@ -59,7 +59,7 @@ const ConfigureAdressSchema = v.object({
|
||||
|
||||
type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>;
|
||||
|
||||
const ConfigureAddress = () => {
|
||||
export const ConfigureAddress = (props: { next?: string }) => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
@@ -134,7 +134,7 @@ const ConfigureAddress = () => {
|
||||
<TextInput
|
||||
{...field}
|
||||
label="IP Address"
|
||||
description="Hostname of the installation target"
|
||||
description="Hostname of the machine"
|
||||
value={field.value}
|
||||
required
|
||||
orientation="horizontal"
|
||||
@@ -197,7 +197,9 @@ const ConfigureAddress = () => {
|
||||
!isReachable() ||
|
||||
isReachable() !== getValue(formStore, "targetHost")
|
||||
}
|
||||
fallback={<NextButton type="submit">Next</NextButton>}
|
||||
fallback={
|
||||
<NextButton type="submit">{props.next || "next"}</NextButton>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
endIcon="ArrowRight"
|
||||
@@ -234,6 +236,14 @@ const CheckHardware = () => {
|
||||
createSignal(false);
|
||||
|
||||
const handleUpdateSummary = async () => {
|
||||
if (!store.install.targetHost) {
|
||||
console.error(
|
||||
"Target host not set, this is required for updating hardware report",
|
||||
);
|
||||
setUpdatingHardwareReport(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatingHardwareReport(true);
|
||||
|
||||
const port = store.install.port
|
||||
@@ -409,7 +419,7 @@ const ConfigureDisk = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigureData = () => {
|
||||
export const ConfigureData = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
@@ -523,7 +533,7 @@ const PromptsFields = (props: PromptsFieldsProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form onSubmit={handleSubmit} class="h-full">
|
||||
<StepLayout
|
||||
body={
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -583,7 +593,7 @@ const PromptsFields = (props: PromptsFieldsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Display = (props: { value: string; label: string }) => {
|
||||
const Display = (props: { value?: string; label: string }) => {
|
||||
return (
|
||||
<>
|
||||
<Typography hierarchy="label" size="xs" color="primary" weight="bold">
|
||||
@@ -606,7 +616,15 @@ const InstallSummary = () => {
|
||||
const handleInstall = async () => {
|
||||
// Here you would typically trigger the installation process
|
||||
console.log("Installation started");
|
||||
if (!store.install.mainDisk) {
|
||||
console.error("Main disk not set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!store.install.targetHost) {
|
||||
console.error("Target host not set, this is required for installing");
|
||||
return;
|
||||
}
|
||||
stepSignal.setActiveStep("install:progress");
|
||||
|
||||
const setDisk = client.fetch("set_machine_disk_schema", {
|
||||
@@ -717,7 +735,7 @@ const InstallSummary = () => {
|
||||
</Orienter>
|
||||
<Divider orientation="horizontal" />
|
||||
<Orienter orientation="horizontal">
|
||||
<Display label="Main Disk" value={store.install.mainDisk} />
|
||||
<Display label="Main Disk" value={store.install?.mainDisk} />
|
||||
</Orienter>
|
||||
</Fieldset>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user