Merge pull request 'UI/install: bootstrap visuals for {createImage, Installer}' (#4605) from install-ui into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4605
This commit is contained in:
@@ -4,11 +4,12 @@ import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
|||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Button } from "@kobalte/core/button";
|
import { Button } from "@kobalte/core/button";
|
||||||
import { Alert as KAlert } from "@kobalte/core/alert";
|
import { Alert as KAlert } from "@kobalte/core/alert";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
export interface AlertProps {
|
export interface AlertProps {
|
||||||
type: "success" | "error" | "warning" | "info";
|
type: "success" | "error" | "warning" | "info";
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description?: string;
|
||||||
icon?: IconVariant;
|
icon?: IconVariant;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
}
|
}
|
||||||
@@ -25,9 +26,11 @@ export const Alert = (props: AlertProps) => (
|
|||||||
<Typography hierarchy="body" size="default" weight="bold" color="inherit">
|
<Typography hierarchy="body" size="default" weight="bold" color="inherit">
|
||||||
{props.title}
|
{props.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography hierarchy="body" size="xs" color="inherit">
|
<Show when={props.description}>
|
||||||
{props.description}
|
<Typography hierarchy="body" size="xs" color="inherit">
|
||||||
</Typography>
|
{props.description}
|
||||||
|
</Typography>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
{props.onDismiss && (
|
{props.onDismiss && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { JSX } from "solid-js";
|
||||||
import styles from "./LoadingBar.module.css";
|
import styles from "./LoadingBar.module.css";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
export const LoadingBar = () => <div class={styles.loading_bar} />;
|
export type LoadingBarProps = JSX.HTMLAttributes<HTMLDivElement> & {};
|
||||||
|
export const LoadingBar = (props: LoadingBarProps) => (
|
||||||
|
<div {...props} class={cx(styles.loading_bar, props.class)} />
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.modal_content {
|
.modal_content {
|
||||||
@apply min-w-[320px] max-w-[512px];
|
@apply min-w-[320px] max-w-[512px];
|
||||||
@apply rounded-md;
|
@apply rounded-md overflow-hidden;
|
||||||
|
|
||||||
/* todo replace with a theme() color */
|
/* todo replace with a theme() color */
|
||||||
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
|
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
|
|
||||||
.modal_body {
|
.modal_body {
|
||||||
@apply rounded-md p-6 pt-4 bg-def-1;
|
@apply rounded-md p-6 pt-4 bg-def-1;
|
||||||
|
|
||||||
|
&[data-no-padding] {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header_divider {
|
.header_divider {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createSignal, JSX, Show } from "solid-js";
|
import { Component, createSignal, JSX, Show } from "solid-js";
|
||||||
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
export interface ModalContext {
|
export interface ModalContext {
|
||||||
close(): void;
|
close(): void;
|
||||||
@@ -16,7 +17,8 @@ export interface ModalProps {
|
|||||||
children: (ctx: ModalContext) => JSX.Element;
|
children: (ctx: ModalContext) => JSX.Element;
|
||||||
mount?: Node;
|
mount?: Node;
|
||||||
class?: string;
|
class?: string;
|
||||||
metaHeader?: () => JSX.Element;
|
metaHeader?: Component;
|
||||||
|
disablePadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
@@ -44,15 +46,17 @@ export const Modal = (props: ModalProps) => {
|
|||||||
<Icon icon="Close" size="0.75rem" />
|
<Icon icon="Close" size="0.75rem" />
|
||||||
</KDialog.CloseButton>
|
</KDialog.CloseButton>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.metaHeader?.()}>
|
<Show when={props.metaHeader}>
|
||||||
{(metaHeader) => (
|
{(metaHeader) => (
|
||||||
<>
|
<>
|
||||||
{metaHeader()}
|
<div class="flex h-9 items-center px-6 py-2">
|
||||||
|
<Dynamic component={metaHeader()} />
|
||||||
|
</div>
|
||||||
<div class={styles.header_divider} />
|
<div class={styles.header_divider} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.modal_body}>
|
<div class={styles.modal_body} data-no-padding={props.disablePadding}>
|
||||||
{props.children({
|
{props.children({
|
||||||
close: () => {
|
close: () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -38,3 +38,66 @@ export const CreateInstallerDisk: Story = {
|
|||||||
initialStep: "create:disk",
|
initialStep: "create:disk",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
export const CreateInstallerProgress: Story = {
|
||||||
|
description: "Showed while the USB stick is being flashed",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "create:progress",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const CreateInstallerDone: Story = {
|
||||||
|
description: "Installation done step",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "create:done",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallConfigureAddress: Story = {
|
||||||
|
description: "Installation configure address step",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:address",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallCheckHardware: Story = {
|
||||||
|
description: "Installation check hardware step",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:check-hardware",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallSelectDisk: Story = {
|
||||||
|
description: "Select disk to install the system on",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:disk",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallVars: Story = {
|
||||||
|
description: "Fill required credentials and data for the installation",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:data",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallSummary: Story = {
|
||||||
|
description: "Summary of the installation steps",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:summary",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallProgress: Story = {
|
||||||
|
description: "Shown while the installation is in progress",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:progress",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const InstallDone: Story = {
|
||||||
|
description: "Shown after the installation is done",
|
||||||
|
args: {
|
||||||
|
machineName: "Test Machine",
|
||||||
|
initialStep: "install:done",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ export const InstallModal = (props: InstallModalProps) => {
|
|||||||
{ initialStep: props.initialStep || "init" },
|
{ initialStep: props.initialStep || "init" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const MetaHeader = () => {
|
||||||
|
// @ts-expect-error some steps might not have
|
||||||
|
const HeaderComponent = () => stepper.currentStep()?.title;
|
||||||
|
return (
|
||||||
|
<Show when={HeaderComponent()}>
|
||||||
|
{(C) => <Dynamic component={C()} machineName={props.machineName} />}
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepperProvider stepper={stepper}>
|
<StepperProvider stepper={stepper}>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -62,17 +72,10 @@ export const InstallModal = (props: InstallModalProps) => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
console.log("Install aborted");
|
console.log("Install aborted");
|
||||||
}}
|
}}
|
||||||
metaHeader={() => {
|
// @ts-expect-error some steps might not have
|
||||||
// @ts-expect-error some steps might not have a title
|
metaHeader={stepper.currentStep()?.title ? <MetaHeader /> : undefined}
|
||||||
const HeaderComponent = stepper.currentStep()?.title;
|
// @ts-expect-error some steps might not have
|
||||||
return (
|
disablePadding={stepper.currentStep()?.isSplash}
|
||||||
<Show when={HeaderComponent}>
|
|
||||||
{(C) => (
|
|
||||||
<Dynamic component={C()} machineName={props.machineName} />
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(ctx) => <InstallStepper />}
|
{(ctx) => <InstallStepper />}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { BackButton, NextButton, StepFooter, StepLayout } from "../../Steps";
|
|||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Alert } from "@/src/components/Alert/Alert";
|
import { Alert } from "@/src/components/Alert/Alert";
|
||||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||||
|
import { Button } from "@/src/components/Button/Button";
|
||||||
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
|
|
||||||
const Prose = () => (
|
const Prose = () => (
|
||||||
<StepLayout
|
<StepLayout
|
||||||
@@ -26,7 +28,9 @@ const Prose = () => (
|
|||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="xs"
|
size="xs"
|
||||||
weight="medium"
|
weight="medium"
|
||||||
color="inherit"
|
color="quaternary"
|
||||||
|
family="mono"
|
||||||
|
inverted
|
||||||
>
|
>
|
||||||
Create a portable installer
|
Create a portable installer
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -35,6 +39,7 @@ const Prose = () => (
|
|||||||
size="default"
|
size="default"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
class="text-balance"
|
||||||
>
|
>
|
||||||
Grab a disposable USB stick and plug it in
|
Grab a disposable USB stick and plug it in
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -57,16 +62,9 @@ const Prose = () => (
|
|||||||
|
|
||||||
const CreateHeader = (props: { machineName: string }) => {
|
const CreateHeader = (props: { machineName: string }) => {
|
||||||
return (
|
return (
|
||||||
<div class="px-6 py-2">
|
<Typography hierarchy="label" size="default" family="mono" weight="medium">
|
||||||
<Typography
|
Create installer
|
||||||
hierarchy="label"
|
</Typography>
|
||||||
size="default"
|
|
||||||
family="mono"
|
|
||||||
weight="medium"
|
|
||||||
>
|
|
||||||
Create installer
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,6 +244,7 @@ const ChooseDisk = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
<Alert
|
<Alert
|
||||||
type="error"
|
type="error"
|
||||||
|
icon="Info"
|
||||||
title="You're about to format this drive"
|
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 on the target device"
|
||||||
/>
|
/>
|
||||||
@@ -265,8 +264,55 @@ const ChooseDisk = () => {
|
|||||||
|
|
||||||
const FlashProgress = () => {
|
const FlashProgress = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="flex h-60 w-full flex-col items-center justify-end bg-inv-4">
|
||||||
<LoadingBar />
|
<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"
|
||||||
|
>
|
||||||
|
USB stick is being flashed
|
||||||
|
</Typography>
|
||||||
|
<LoadingBar class="" />
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Device has been successfully flashed!
|
||||||
|
</Typography>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
title="Plug the flashed device into the machine that you want to install."
|
||||||
|
description=""
|
||||||
|
/>
|
||||||
|
<div class="mt-3 flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
hierarchy="primary"
|
||||||
|
endIcon="ArrowRight"
|
||||||
|
onClick={() => stepSignal.next()}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -288,7 +334,12 @@ export const createInstallerSteps = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "create:progress",
|
id: "create:progress",
|
||||||
title: CreateHeader,
|
|
||||||
content: FlashProgress,
|
content: FlashProgress,
|
||||||
|
isSplash: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "create:done",
|
||||||
|
content: FlashDone,
|
||||||
|
isSplash: true,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { NextButton } from "../../Steps";
|
import { BackButton, NextButton, StepLayout } from "../../Steps";
|
||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
getError,
|
||||||
|
SubmitHandler,
|
||||||
|
valiForm,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||||
|
import * as v from "valibot";
|
||||||
|
import { useStepper } from "@/src/hooks/stepper";
|
||||||
|
import { InstallSteps } from "../install";
|
||||||
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
|
import { Alert } from "@/src/components/Alert/Alert";
|
||||||
|
import { createSignal, Show } from "solid-js";
|
||||||
|
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";
|
||||||
|
|
||||||
export const InstallHeader = (props: { machineName: string }) => {
|
export const InstallHeader = (props: { machineName: string }) => {
|
||||||
return (
|
return (
|
||||||
@@ -9,26 +26,354 @@ export const InstallHeader = (props: { machineName: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ConfigureAdressSchema = v.object({
|
||||||
|
targetHost: v.pipe(
|
||||||
|
v.string("Please set a target host."),
|
||||||
|
v.nonEmpty("Please set a target host."),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>;
|
||||||
|
|
||||||
|
const ConfigureAddress = () => {
|
||||||
|
const [formStore, { Form, Field }] = createForm<ConfigureAdressForm>({
|
||||||
|
validate: valiForm(ConfigureAdressSchema),
|
||||||
|
});
|
||||||
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
|
||||||
|
// TODO: push values to the parent form Store
|
||||||
|
const handleSubmit: SubmitHandler<ConfigureAdressForm> = (values, event) => {
|
||||||
|
console.log("ISO creation submitted", values);
|
||||||
|
// Here you would typically trigger the ISO creation process
|
||||||
|
stepSignal.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<StepLayout
|
||||||
|
body={
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Fieldset>
|
||||||
|
<Field name="targetHost">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="IP Address"
|
||||||
|
description="Hostname of the installation target"
|
||||||
|
value={field.value}
|
||||||
|
required
|
||||||
|
orientation="horizontal"
|
||||||
|
validationState={
|
||||||
|
getError(formStore, "targetHost") ? "invalid" : "valid"
|
||||||
|
}
|
||||||
|
input={{
|
||||||
|
...props,
|
||||||
|
placeholder: "i.e. flash-installer.local",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<NextButton type="submit">Next</NextButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckHardware = () => {
|
||||||
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
// TODO: Hook this up with api
|
||||||
|
const [report, setReport] = createSignal<boolean>(true);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
stepSignal.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StepLayout
|
||||||
|
body={
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Fieldset>
|
||||||
|
<Orienter orientation="horizontal">
|
||||||
|
<Typography hierarchy="label" size="xs" weight="bold">
|
||||||
|
Check hardware
|
||||||
|
</Typography>
|
||||||
|
<Button hierarchy="secondary" startIcon="Report">
|
||||||
|
Run hardware report
|
||||||
|
</Button>
|
||||||
|
</Orienter>
|
||||||
|
<Divider orientation="horizontal" />
|
||||||
|
<Show when={report()}>
|
||||||
|
<Alert
|
||||||
|
icon="Checkmark"
|
||||||
|
type="info"
|
||||||
|
title="Hardware report exists"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<NextButton onClick={handleNext}>Next</NextButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DiskSchema = v.object({
|
||||||
|
mainDisk: v.pipe(
|
||||||
|
v.string("Please select a disk"),
|
||||||
|
v.nonEmpty("Please select a disk"),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type DiskForm = v.InferInput<typeof DiskSchema>;
|
||||||
|
|
||||||
|
const ConfigureDisk = () => {
|
||||||
|
const [formStore, { Form, Field }] = createForm<DiskForm>({
|
||||||
|
validate: valiForm(DiskSchema),
|
||||||
|
});
|
||||||
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<DiskForm> = (values, event) => {
|
||||||
|
console.log("submitted", values);
|
||||||
|
// Here you would typically trigger the ISO creation process
|
||||||
|
stepSignal.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<StepLayout
|
||||||
|
body={
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Fieldset>
|
||||||
|
<Field name="mainDisk">
|
||||||
|
{(field, props) => (
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
label={{
|
||||||
|
label: "Main disk",
|
||||||
|
description: "Select the disk to install the system on",
|
||||||
|
}}
|
||||||
|
// TODO: Get from api
|
||||||
|
options={[{ value: "disk", label: "Disk0" }]}
|
||||||
|
placeholder="Select a disk"
|
||||||
|
name={field.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<NextButton type="submit">Next</NextButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DynamicForm = Record<string, string>;
|
||||||
|
|
||||||
|
const ConfigureData = () => {
|
||||||
|
const [formStore, { Form, Field }] = createForm<DynamicForm>({
|
||||||
|
// TODO: Dynamically validate fields
|
||||||
|
});
|
||||||
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<DynamicForm> = (values, event) => {
|
||||||
|
console.log("vars submitted", values);
|
||||||
|
// Here you would typically trigger the ISO creation process
|
||||||
|
stepSignal.next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<StepLayout
|
||||||
|
body={
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Fieldset legend="Root password">
|
||||||
|
<Field name="root-password">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="Root password"
|
||||||
|
description="Set the root password for the machine"
|
||||||
|
value={field.value}
|
||||||
|
required
|
||||||
|
orientation="horizontal"
|
||||||
|
validationState={
|
||||||
|
getError(formStore, "root-password") ? "invalid" : "valid"
|
||||||
|
}
|
||||||
|
icon="EyeClose"
|
||||||
|
input={{
|
||||||
|
...props,
|
||||||
|
type: "password",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="WIFI TU-YAN">
|
||||||
|
<Field name="networkSSID">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="ssid"
|
||||||
|
description="Name of the wifi network"
|
||||||
|
value={field.value}
|
||||||
|
required
|
||||||
|
orientation="horizontal"
|
||||||
|
validationState={
|
||||||
|
getError(formStore, "wifi/password") ? "invalid" : "valid"
|
||||||
|
}
|
||||||
|
input={{
|
||||||
|
...props,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="password">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
{...field}
|
||||||
|
label="password"
|
||||||
|
description="Password for the wifi network"
|
||||||
|
value={field.value}
|
||||||
|
required
|
||||||
|
orientation="horizontal"
|
||||||
|
validationState={
|
||||||
|
getError(formStore, "wifi/password") ? "invalid" : "valid"
|
||||||
|
}
|
||||||
|
icon="EyeClose"
|
||||||
|
input={{
|
||||||
|
...props,
|
||||||
|
type: "password",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<NextButton type="submit">Next</NextButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Display = (props: { value: string; label: string }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography hierarchy="label" size="xs" color="primary" weight="bold">
|
||||||
|
{props.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium">
|
||||||
|
{props.value}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InstallSummary = () => {
|
||||||
|
const stepSignal = useStepper<InstallSteps>();
|
||||||
|
|
||||||
|
const handleInstall = () => {
|
||||||
|
// Here you would typically trigger the installation process
|
||||||
|
console.log("Installation started");
|
||||||
|
stepSignal.setActiveStep("install:progress");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StepLayout
|
||||||
|
body={
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Fieldset legend="Deploy to">
|
||||||
|
<Orienter orientation="horizontal">
|
||||||
|
{/* TOOD: Display the values emited from previous steps */}
|
||||||
|
<Display label="Target" value="flash-installer.local" />
|
||||||
|
</Orienter>
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset legend="Disk Configuration">
|
||||||
|
<Orienter orientation="horizontal">
|
||||||
|
<Display label="Disk Schema" value="Single" />
|
||||||
|
</Orienter>
|
||||||
|
<Divider orientation="horizontal" />
|
||||||
|
<Orienter orientation="horizontal">
|
||||||
|
<Display
|
||||||
|
label="Main Disk"
|
||||||
|
value="nvme-WD_PC_SN740_SDDQNQD-512G"
|
||||||
|
/>
|
||||||
|
</Orienter>
|
||||||
|
</Fieldset>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<NextButton type="button" onClick={handleInstall} endIcon="Flash">
|
||||||
|
Install
|
||||||
|
</NextButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const installSteps = [
|
export const installSteps = [
|
||||||
{
|
{
|
||||||
id: "install:machine-0",
|
id: "install:address",
|
||||||
title: InstallHeader,
|
title: InstallHeader,
|
||||||
content: () => (
|
content: ConfigureAddress,
|
||||||
<div>
|
|
||||||
Enter the targetHost
|
|
||||||
<NextButton />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "install:confirm",
|
id: "install:check-hardware",
|
||||||
title: InstallHeader,
|
title: InstallHeader,
|
||||||
content: (props: { machineName: string }) => (
|
content: CheckHardware,
|
||||||
<div>
|
},
|
||||||
Confirm the installation of {props.machineName}
|
{
|
||||||
<NextButton />
|
id: "install:check-hardware",
|
||||||
</div>
|
title: InstallHeader,
|
||||||
|
content: CheckHardware,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "install:disk",
|
||||||
|
title: InstallHeader,
|
||||||
|
content: ConfigureDisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "install:data",
|
||||||
|
title: InstallHeader,
|
||||||
|
content: ConfigureData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "install:summary",
|
||||||
|
title: () => (
|
||||||
|
<Typography hierarchy="label" size="default">
|
||||||
|
Summary
|
||||||
|
</Typography>
|
||||||
),
|
),
|
||||||
|
content: InstallSummary,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "install:progress",
|
id: "install:progress",
|
||||||
@@ -40,4 +385,9 @@ export const installSteps = [
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "install:done",
|
||||||
|
title: InstallHeader,
|
||||||
|
content: () => <div>Done</div>,
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -40,13 +40,11 @@ export const BackButton = () => {
|
|||||||
<Button
|
<Button
|
||||||
hierarchy="secondary"
|
hierarchy="secondary"
|
||||||
disabled={!stepSignal.hasPrevious()}
|
disabled={!stepSignal.hasPrevious()}
|
||||||
startIcon="ArrowLeft"
|
icon="ArrowLeft"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
stepSignal.previous();
|
stepSignal.previous();
|
||||||
}}
|
}}
|
||||||
>
|
></Button>
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ export const BackButton = () => {
|
|||||||
export const StepFooter = () => {
|
export const StepFooter = () => {
|
||||||
const stepper = useStepper<InstallSteps>();
|
const stepper = useStepper<InstallSteps>();
|
||||||
return (
|
return (
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between py-4">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<NextButton type="button" onClick={() => stepper.next()} />
|
<NextButton type="button" onClick={() => stepper.next()} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user