Merge pull request 'UI/install: add machine progress, minor stepper fixes' (#4619) from install-ui into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4619
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
|
||||
prompts.password.type = "hidden";
|
||||
prompts.password.persist = true;
|
||||
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
|
||||
prompts.password.description = "Leave empty to generate automatically";
|
||||
|
||||
script = ''
|
||||
prompt_value="$(cat "$prompts"/password)"
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
};
|
||||
type = "hidden";
|
||||
persist = true;
|
||||
description = "You can autogenerate a password, if you leave this prompt blank.";
|
||||
description = "Leave empty to generate automatically";
|
||||
};
|
||||
|
||||
runtimeInputs = [
|
||||
|
||||
@@ -118,3 +118,10 @@ export function StepperProvider<
|
||||
</StepperContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to define steps in a type-safe manner.
|
||||
*/
|
||||
export function defineSteps<T extends readonly StepBase[]>(steps: T) {
|
||||
return steps;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid";
|
||||
import { Show } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { InitialStep } from "./steps/Initial";
|
||||
import { initialSteps } from "./steps/Initial";
|
||||
import { createInstallerSteps } from "./steps/createInstaller";
|
||||
import { installSteps } from "./steps/installSteps";
|
||||
|
||||
@@ -40,10 +40,14 @@ const InstallStepper = () => {
|
||||
|
||||
export interface InstallModalProps {
|
||||
machineName: string;
|
||||
initialStep?: string;
|
||||
initialStep?: InstallSteps[number]["id"];
|
||||
}
|
||||
|
||||
const steps = [InitialStep, ...createInstallerSteps, ...installSteps] as const;
|
||||
const steps = [
|
||||
...initialSteps,
|
||||
...createInstallerSteps,
|
||||
...installSteps,
|
||||
] as const;
|
||||
|
||||
export type InstallSteps = typeof steps;
|
||||
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import { useStepper } from "@/src/hooks/stepper";
|
||||
import { defineSteps, Step, StepBase, 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";
|
||||
import { StepLayout } from "../../Steps";
|
||||
|
||||
const InitialChoice = () => {
|
||||
const ChoiceLocalOrRemote = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-1 px-1">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
Remote setup
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="normal"
|
||||
color="secondary"
|
||||
>
|
||||
Is your machine currently online? Does it have an IP-address, can
|
||||
you SSH into it? And does it support Kexec?
|
||||
I have physical access to the machine.
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
@@ -34,31 +26,105 @@ const InitialChoice = () => {
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("install:machine-0")}
|
||||
></Button>
|
||||
onClick={() => stepSignal.setActiveStep("local:choice")}
|
||||
/>
|
||||
</div>
|
||||
<Divider orientation="horizontal" class="bg-def-3" />
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Typography hierarchy="label" size="xs" weight="bold">
|
||||
I don't have an installer, yet
|
||||
</Typography>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
The Machine is remote and i have ssh access to it.
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
endIcon="Flash"
|
||||
type="button"
|
||||
onClick={() => stepSignal.setActiveStep("create:iso-0")}
|
||||
>
|
||||
Create USB Installer
|
||||
</Button>
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("install:address")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InitialStep = {
|
||||
id: "init",
|
||||
content: InitialChoice,
|
||||
const ChoiceLocalInstaller = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<StepLayout
|
||||
body={
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
I have an installer
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("install:address")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 rounded-md px-4 py-6 text-fg-def-1 bg-def-2">
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex flex-col gap-1 px-1 justify-center">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
I don't have an installer, yet
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
ghost
|
||||
hierarchy="secondary"
|
||||
icon="CaretRight"
|
||||
onClick={() => stepSignal.setActiveStep("create:prose")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div class="flex justify-start">
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
icon="ArrowLeft"
|
||||
onClick={() => stepSignal.previous()}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const initialSteps = defineSteps([
|
||||
{
|
||||
id: "init",
|
||||
content: ChoiceLocalOrRemote,
|
||||
},
|
||||
{
|
||||
id: "local:choice",
|
||||
content: ChoiceLocalInstaller,
|
||||
},
|
||||
] as const);
|
||||
|
||||
@@ -32,7 +32,7 @@ const Prose = () => (
|
||||
family="mono"
|
||||
inverted
|
||||
>
|
||||
Create a portable installer
|
||||
Local Setup
|
||||
</Typography>
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
@@ -41,22 +41,38 @@ const Prose = () => (
|
||||
color="inherit"
|
||||
class="text-balance"
|
||||
>
|
||||
Grab a disposable USB stick and plug it in
|
||||
Here's what you
|
||||
<br />
|
||||
need to do
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Typography hierarchy="body" size="default" weight="bold">
|
||||
We will erase everything on it during this process
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs">
|
||||
Create a portable installer tool that can turn any machine into a
|
||||
fully configured Clan machine.
|
||||
</Typography>
|
||||
<div class="flex flex-col px-4 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<Typography hierarchy="body" size="default" weight="bold">
|
||||
Let's walk through it.
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs" weight="normal">
|
||||
In the following we will help you to write the clan installer
|
||||
software to a USB-stick. This USB-stick will then be used to set
|
||||
up the new machine. Get a USB-stick with at least 8GB of capacity,
|
||||
and plug it into the machine on which you read this text. Note,
|
||||
all data on the USB-Stick will be lost.
|
||||
</Typography>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Typography hierarchy="body" size="default" weight="bold">
|
||||
Why do you need to do this?
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs" weight="normal">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Suspendisse varius enim in eros elementum tristique. Duis cursus,
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={<StepFooter />}
|
||||
footer={<StepFooter nextText="start" />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -128,7 +144,7 @@ const ConfigureImage = () => {
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
label="Public Key"
|
||||
orientation="horizontal"
|
||||
placeholder="Select SSH Key"
|
||||
required={true}
|
||||
@@ -230,14 +246,14 @@ const ChooseDisk = () => {
|
||||
error={field.error}
|
||||
required
|
||||
label={{
|
||||
label: "Disk",
|
||||
description: "Select a usb stick",
|
||||
label: "USB Stick",
|
||||
description: "Select the usb stick",
|
||||
}}
|
||||
options={[
|
||||
{ value: "1", label: "sda1" },
|
||||
{ value: "2", label: "sdb2" },
|
||||
]}
|
||||
placeholder="Disk"
|
||||
placeholder="Choose Device"
|
||||
name={field.name}
|
||||
/>
|
||||
)}
|
||||
@@ -246,7 +262,7 @@ const ChooseDisk = () => {
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="You're about to format this drive"
|
||||
description="It will erase all existing data on the target device"
|
||||
description="It will erase all existing data"
|
||||
/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
@@ -274,7 +290,7 @@ const FlashProgress = () => {
|
||||
>
|
||||
USB stick is being flashed
|
||||
</Typography>
|
||||
<LoadingBar class="" />
|
||||
<LoadingBar />
|
||||
<Button hierarchy="primary" class="w-fit" size="s">
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -296,11 +312,11 @@ const FlashDone = () => {
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Device has been successfully flashed!
|
||||
USB Stick is ready!
|
||||
</Typography>
|
||||
<Alert
|
||||
type="warning"
|
||||
title="Plug the flashed device into the machine that you want to install."
|
||||
title="Remove it and plug it into the machine that you want to install."
|
||||
description=""
|
||||
/>
|
||||
<div class="mt-3 flex w-full justify-end">
|
||||
|
||||
@@ -17,6 +17,8 @@ import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Orienter } from "@/src/components/Form/Orienter";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Select } from "@/src/components/Select/Select";
|
||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
|
||||
export const InstallHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
@@ -103,10 +105,10 @@ const CheckHardware = () => {
|
||||
<Fieldset>
|
||||
<Orienter orientation="horizontal">
|
||||
<Typography hierarchy="label" size="xs" weight="bold">
|
||||
Check hardware
|
||||
Hardware Report
|
||||
</Typography>
|
||||
<Button hierarchy="secondary" startIcon="Report">
|
||||
Run hardware report
|
||||
Update hardware report
|
||||
</Button>
|
||||
</Orienter>
|
||||
<Divider orientation="horizontal" />
|
||||
@@ -123,7 +125,9 @@ const CheckHardware = () => {
|
||||
footer={
|
||||
<div class="flex justify-between">
|
||||
<BackButton />
|
||||
<NextButton onClick={handleNext}>Next</NextButton>
|
||||
<NextButton type="button" onClick={handleNext}>
|
||||
Next
|
||||
</NextButton>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -214,7 +218,7 @@ const ConfigureData = () => {
|
||||
<TextInput
|
||||
{...field}
|
||||
label="Root password"
|
||||
description="Set the root password for the machine"
|
||||
description="Leave empty to generate automatically"
|
||||
value={field.value}
|
||||
required
|
||||
orientation="horizontal"
|
||||
@@ -340,6 +344,58 @@ const InstallSummary = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const InstallProgress = () => {
|
||||
return (
|
||||
<div class="flex h-60 w-full flex-col items-center justify-end bg-inv-4">
|
||||
<div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
size="default"
|
||||
weight="bold"
|
||||
color="inherit"
|
||||
>
|
||||
Machine is beeing installed
|
||||
</Typography>
|
||||
<LoadingBar />
|
||||
<Button hierarchy="primary" class="w-fit" size="s">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FlashDone = () => {
|
||||
const stepSignal = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex w-full flex-col items-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 installation finished!
|
||||
</Typography>
|
||||
<div class="mt-3 flex w-full justify-center">
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
endIcon="Close"
|
||||
size="s"
|
||||
onClick={() => stepSignal.next()}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const installSteps = [
|
||||
{
|
||||
id: "install:address",
|
||||
@@ -351,11 +407,6 @@ export const installSteps = [
|
||||
title: InstallHeader,
|
||||
content: CheckHardware,
|
||||
},
|
||||
{
|
||||
id: "install:check-hardware",
|
||||
title: InstallHeader,
|
||||
content: CheckHardware,
|
||||
},
|
||||
{
|
||||
id: "install:disk",
|
||||
title: InstallHeader,
|
||||
@@ -377,17 +428,12 @@ export const installSteps = [
|
||||
},
|
||||
{
|
||||
id: "install:progress",
|
||||
title: InstallHeader,
|
||||
content: () => (
|
||||
<div>
|
||||
<p>Installation in progress...</p>
|
||||
<p>Please wait while we set up your machine.</p>
|
||||
</div>
|
||||
),
|
||||
content: InstallProgress,
|
||||
isSplash: true,
|
||||
},
|
||||
{
|
||||
id: "install:done",
|
||||
title: InstallHeader,
|
||||
content: () => <div>Done</div>,
|
||||
content: FlashDone,
|
||||
isSplash: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -57,12 +57,17 @@ export const BackButton = () => {
|
||||
*
|
||||
* Use this for overview steps where no form submission is required.
|
||||
*/
|
||||
export const StepFooter = () => {
|
||||
interface StepFooterProps {
|
||||
nextText?: string;
|
||||
}
|
||||
export const StepFooter = (props: StepFooterProps) => {
|
||||
const stepper = useStepper<InstallSteps>();
|
||||
return (
|
||||
<div class="flex justify-between py-4">
|
||||
<BackButton />
|
||||
<NextButton type="button" onClick={() => stepper.next()} />
|
||||
<NextButton type="button" onClick={() => stepper.next()}>
|
||||
{props.nextText || undefined}
|
||||
</NextButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user