ui/update: init update machine
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { createSignal, Show } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import { InstallModal } from "@/src/workflows/InstallMachine/InstallMachine";
|
|
||||||
import { useMachineName } from "@/src/hooks/clan";
|
import { useMachineName } from "@/src/hooks/clan";
|
||||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||||
import styles from "./SidebarSectionInstall.module.css";
|
import styles from "./SidebarSectionInstall.module.css";
|
||||||
|
import { UpdateModal } from "@/src/workflows/InstallMachine/UpdateMachine";
|
||||||
|
|
||||||
export interface SidebarSectionUpdateProps {
|
export interface SidebarSectionUpdateProps {
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
@@ -26,7 +26,7 @@ export const SidebarSectionUpdate = (props: SidebarSectionUpdateProps) => {
|
|||||||
Update machine
|
Update machine
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={showUpdate()}>
|
<Show when={showUpdate()}>
|
||||||
<InstallModal
|
<UpdateModal
|
||||||
open={showUpdate()}
|
open={showUpdate()}
|
||||||
machineName={useMachineName()}
|
machineName={useMachineName()}
|
||||||
onClose={() => setShowUpdate(false)}
|
onClose={() => setShowUpdate(false)}
|
||||||
|
|||||||
@@ -111,10 +111,7 @@ export const AddMachine = (props: AddMachineProps) => {
|
|||||||
return defaultClass;
|
return defaultClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (currentStep.id) {
|
return defaultClass;
|
||||||
default:
|
|
||||||
return defaultClass;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ export interface InstallStoreType {
|
|||||||
progress: ApiCall<"run_machine_flash">;
|
progress: ApiCall<"run_machine_flash">;
|
||||||
};
|
};
|
||||||
install: {
|
install: {
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
machineName: string;
|
machineName: string;
|
||||||
mainDisk: string;
|
mainDisk?: string;
|
||||||
// ...TODO Vars
|
// ...TODO Vars
|
||||||
progress: ApiCall<"run_machine_install">;
|
progress: ApiCall<"run_machine_install">;
|
||||||
promptValues: PromptValues;
|
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>;
|
type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>;
|
||||||
|
|
||||||
const ConfigureAddress = () => {
|
export const ConfigureAddress = (props: { next?: string }) => {
|
||||||
const stepSignal = useStepper<InstallSteps>();
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ const ConfigureAddress = () => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
{...field}
|
{...field}
|
||||||
label="IP Address"
|
label="IP Address"
|
||||||
description="Hostname of the installation target"
|
description="Hostname of the machine"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
required
|
required
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
@@ -197,7 +197,9 @@ const ConfigureAddress = () => {
|
|||||||
!isReachable() ||
|
!isReachable() ||
|
||||||
isReachable() !== getValue(formStore, "targetHost")
|
isReachable() !== getValue(formStore, "targetHost")
|
||||||
}
|
}
|
||||||
fallback={<NextButton type="submit">Next</NextButton>}
|
fallback={
|
||||||
|
<NextButton type="submit">{props.next || "next"}</NextButton>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
endIcon="ArrowRight"
|
endIcon="ArrowRight"
|
||||||
@@ -234,6 +236,14 @@ const CheckHardware = () => {
|
|||||||
createSignal(false);
|
createSignal(false);
|
||||||
|
|
||||||
const handleUpdateSummary = async () => {
|
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);
|
setUpdatingHardwareReport(true);
|
||||||
|
|
||||||
const port = store.install.port
|
const port = store.install.port
|
||||||
@@ -409,7 +419,7 @@ const ConfigureDisk = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfigureData = () => {
|
export const ConfigureData = () => {
|
||||||
const stepSignal = useStepper<InstallSteps>();
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
@@ -523,7 +533,7 @@ const PromptsFields = (props: PromptsFieldsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit} class="h-full">
|
||||||
<StepLayout
|
<StepLayout
|
||||||
body={
|
body={
|
||||||
<div class="flex flex-col gap-2">
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography hierarchy="label" size="xs" color="primary" weight="bold">
|
<Typography hierarchy="label" size="xs" color="primary" weight="bold">
|
||||||
@@ -606,7 +616,15 @@ const InstallSummary = () => {
|
|||||||
const handleInstall = async () => {
|
const handleInstall = async () => {
|
||||||
// Here you would typically trigger the installation process
|
// Here you would typically trigger the installation process
|
||||||
console.log("Installation started");
|
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");
|
stepSignal.setActiveStep("install:progress");
|
||||||
|
|
||||||
const setDisk = client.fetch("set_machine_disk_schema", {
|
const setDisk = client.fetch("set_machine_disk_schema", {
|
||||||
@@ -717,7 +735,7 @@ const InstallSummary = () => {
|
|||||||
</Orienter>
|
</Orienter>
|
||||||
<Divider orientation="horizontal" />
|
<Divider orientation="horizontal" />
|
||||||
<Orienter orientation="horizontal">
|
<Orienter orientation="horizontal">
|
||||||
<Display label="Main Disk" value={store.install.mainDisk} />
|
<Display label="Main Disk" value={store.install?.mainDisk} />
|
||||||
</Orienter>
|
</Orienter>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user