Merge pull request 'UI/Install workflow: integrate api until hardware report' (#4646) from ui-more into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4646
This commit is contained in:
hsjobeki
2025-08-08 19:23:00 +00:00
10 changed files with 274 additions and 176 deletions

View File

@@ -22,7 +22,7 @@
} }
.modal_body { .modal_body {
@apply rounded-md p-6 pt-4 bg-def-1; @apply rounded-b-md p-6 pt-4 bg-def-1;
&[data-no-padding] { &[data-no-padding] {
@apply p-0; @apply p-0;

View File

@@ -49,7 +49,7 @@ export const Modal = (props: ModalProps) => {
<Show when={props.metaHeader}> <Show when={props.metaHeader}>
{(metaHeader) => ( {(metaHeader) => (
<> <>
<div class="flex h-9 items-center px-6 py-2"> <div class="flex h-9 items-center px-6 py-2 bg-def-1">
<Dynamic component={metaHeader()} /> <Dynamic component={metaHeader()} />
</div> </div>
<div class={styles.header_divider} /> <div class={styles.header_divider} />

View File

@@ -1,88 +1,88 @@
.trigger { .trigger {
@apply bg-def-1 flex flex-grow justify-between items-center gap-2 h-7 px-2 py-1; @apply bg-def-1 flex flex-grow justify-between items-center gap-2 h-7 px-2 py-1;
@apply rounded-[4px]; @apply rounded-[4px];
&[data-expanded] { &[data-expanded] {
@apply outline-def-2 outline-1 outline; @apply outline-def-2 outline-1 outline;
z-index: 40; z-index: var(--z-index + 5);
}
&[data-highlighted] {
@apply outline outline-1 outline-inv-2;
}
&[data-loading] {
@apply cursor-wait;
}
&:hover {
@apply bg-def-2;
& .icon {
@apply bg-def-1 text-fg-def-1;
} }
}
&[data-highlighted] { &:active {
@apply outline outline-1 outline-inv-2 @apply bg-def-4;
& .icon {
@apply bg-inv-4 text-fg-inv-1;
} }
}
&[data-loading] { &:focus-visible {
@apply cursor-wait; @apply outline-def-2 outline-1 outline;
} }
&:hover {
@apply bg-def-2;
& .icon {
@apply bg-def-1 text-fg-def-1;
}
}
&:active {
@apply bg-def-4;
& .icon {
@apply bg-inv-4 text-fg-inv-1;
}
}
&:focus-visible {
@apply outline-def-2 outline-1 outline;
}
} }
.icon { .icon {
@apply bg-def-2 rounded-sm; @apply bg-def-2 rounded-sm;
@apply flex items-center justify-center h-[14px] w-[14px] p-[2px]; @apply flex items-center justify-center h-[14px] w-[14px] p-[2px];
&[data-disabled] { &[data-disabled] {
@apply cursor-not-allowed; @apply cursor-not-allowed;
} }
&[data-loading] { &[data-loading] {
@apply bg-inherit; @apply bg-inherit;
} }
} }
.options_content { .options_content {
@apply bg-def-1 px-1 py-3 rounded-[4px] -mt-8 pt-10 -mx-1; z-index: var(--z-index);
@apply outline-def-2 outline-1 outline;
transform-origin: var(--kb-popper-content-transform-origin); @apply bg-def-1 px-1 py-3 rounded-[4px] -mt-8 pt-10 -mx-1;
&[data-expanded] { @apply outline-def-2 outline-1 outline;
animation: overlayShow 250ms ease-out;
transform-origin: var(--kb-popper-content-transform-origin);
&[data-expanded] {
animation: overlayShow 250ms ease-out;
}
/* Option elements (typically <li>) */
& [role="option"] {
@apply px-2 py-4 rounded-sm flex items-center gap-1 flex-shrink-0;
&[data-highlighted],
&:focus-visible {
@apply outline outline-1 outline-inv-2;
} }
/* Option elements (typically <li>) */ &:hover {
& [role="option"] { @apply bg-def-2;
@apply px-1 py-2 rounded-sm flex items-center gap-1 flex-shrink-0 ;
&[data-highlighted],
&:focus-visible {
@apply outline outline-1 outline-inv-2
}
&:hover {
@apply bg-def-2;
}
&:active {
@apply bg-def-4;
}
} }
& [role="listbox"] { &:active {
&:focus-visible { @apply bg-def-4;
@apply outline-none;
}
} }
}
& [role="listbox"] {
&:focus-visible {
@apply outline-none;
}
}
} }
@keyframes overlayShow { @keyframes overlayShow {
from { from {
opacity: 0; opacity: 0;

View File

@@ -1,4 +1,4 @@
import { Select as KSelect } from "@kobalte/core/select"; import { Select as KSelect, SelectPortalProps } from "@kobalte/core/select";
import Icon from "../Icon/Icon"; import Icon from "../Icon/Icon";
import { Orienter } from "../Form/Orienter"; import { Orienter } from "../Form/Orienter";
import { Label, LabelProps } from "../Form/Label"; import { Label, LabelProps } from "../Form/Label";
@@ -28,6 +28,8 @@ export type SelectProps = {
// Custom props // Custom props
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical";
label?: Omit<LabelProps, "labelComponent" | "descriptionComponent">; label?: Omit<LabelProps, "labelComponent" | "descriptionComponent">;
portalProps?: Partial<SelectPortalProps>;
zIndex?: number;
} & ( } & (
| { | {
// Sync options // Sync options
@@ -48,6 +50,8 @@ export const Select = (props: SelectProps) => {
["placeholder", "ref", "onInput", "onChange", "onBlur"], ["placeholder", "ref", "onInput", "onChange", "onBlur"],
); );
const zIndex = () => props.zIndex ?? 40;
const [getValue, setValue] = createSignal<Option>(); const [getValue, setValue] = createSignal<Option>();
const [resolvedOptions, setResolvedOptions] = createSignal<Option[]>([]); const [resolvedOptions, setResolvedOptions] = createSignal<Option[]>([]);
@@ -78,6 +82,7 @@ export const Select = (props: SelectProps) => {
return ( return (
<KSelect <KSelect
{...root} {...root}
fitViewport={true}
options={options()} options={options()}
sameWidth={true} sameWidth={true}
gutter={0} gutter={0}
@@ -164,8 +169,11 @@ export const Select = (props: SelectProps) => {
</KSelect.Icon> </KSelect.Icon>
</KSelect.Trigger> </KSelect.Trigger>
</Orienter> </Orienter>
<KSelect.Portal> <KSelect.Portal {...props.portalProps}>
<KSelect.Content class={styles.options_content}> <KSelect.Content
class={styles.options_content}
style={{ "--z-index": zIndex() }}
>
<KSelect.Listbox /> <KSelect.Listbox />
</KSelect.Content> </KSelect.Content>
</KSelect.Portal> </KSelect.Portal>

View File

@@ -142,18 +142,12 @@ export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult => {
export type MachineFlashOptions = SuccessData<"get_machine_flash_options">; export type MachineFlashOptions = SuccessData<"get_machine_flash_options">;
export type MachineFlashOptionsQuery = UseQueryResult<MachineFlashOptions>; export type MachineFlashOptionsQuery = UseQueryResult<MachineFlashOptions>;
export const useMachineFlashOptions = ( export const useMachineFlashOptions = (): MachineFlashOptionsQuery => {
clanURI: string,
): MachineFlashOptionsQuery => {
const client = useApiClient(); const client = useApiClient();
return useQuery<MachineFlashOptions>(() => ({ return useQuery<MachineFlashOptions>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "machine_flash_options"], queryKey: ["clans", "machine_flash_options"],
queryFn: async () => { queryFn: async () => {
const call = client.fetch("get_machine_flash_options", { const call = client.fetch("get_machine_flash_options", {});
flake: {
identifier: clanURI,
},
});
const result = await call.result; const result = await call.result;
if (result.status === "error") { if (result.status === "error") {
@@ -188,3 +182,43 @@ export const useSystemStorageOptions = (): SystemStorageOptionsQuery => {
}, },
})); }));
}; };
export type MachineHardwareSummary =
SuccessData<"get_machine_hardware_summary">;
export type MachineHardwareSummaryQuery =
UseQueryResult<MachineHardwareSummary>;
export const useMachineHardwareSummary = (
clanUri: string,
machineName: string,
): MachineHardwareSummaryQuery => {
const client = useApiClient();
return useQuery<MachineHardwareSummary>(() => ({
queryKey: [
"clans",
encodeBase64(clanUri),
"machines",
machineName,
"hardware_summary",
],
queryFn: async () => {
const call = client.fetch("get_machine_hardware_summary", {
machine: {
flake: {
identifier: clanUri,
},
name: machineName,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return result.data;
},
}));
};

View File

@@ -1,8 +1,10 @@
import { RouteSectionProps, useNavigate } from "@solidjs/router"; import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { SidebarPane } from "@/src/components/Sidebar/SidebarPane"; import { SidebarPane } from "@/src/components/Sidebar/SidebarPane";
import { navigateToClan, useClanURI, useMachineName } from "@/src/hooks/clan"; import { navigateToClan, useClanURI, useMachineName } from "@/src/hooks/clan";
import { Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import { SectionGeneral } from "./SectionGeneral"; import { SectionGeneral } from "./SectionGeneral";
import { InstallModal } from "@/src/workflows/Install/install";
import { Button } from "@/src/components/Button/Button";
export const Machine = (props: RouteSectionProps) => { export const Machine = (props: RouteSectionProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -13,10 +15,30 @@ export const Machine = (props: RouteSectionProps) => {
navigateToClan(navigate, clanURI); navigateToClan(navigate, clanURI);
}; };
const machineName = useMachineName(); const [showInstall, setShowModal] = createSignal(false);
let container: Node;
return ( return (
<Show when={useMachineName()} keyed> <Show when={useMachineName()} keyed>
<Button
hierarchy="primary"
onClick={() => setShowModal(true)}
class="absolute top-0 right-0 m-4"
>
Install me!
</Button>
<Show when={showInstall()}>
<div
class="absolute top-0 left-0 w-full h-full flex justify-center items-center z-50 bg-white/90"
ref={(el) => (container = el)}
>
<InstallModal
machineName={useMachineName()}
mount={container!}
onClose={() => setShowModal(false)}
/>
</div>
</Show>
<SidebarPane title={useMachineName()} onClose={onClose}> <SidebarPane title={useMachineName()} onClose={onClose}>
<SectionGeneral /> <SectionGeneral />
</SidebarPane> </SidebarPane>

View File

@@ -1,6 +1,7 @@
import { Modal } from "@/src/components/Modal/Modal"; import { Modal } from "@/src/components/Modal/Modal";
import { import {
createStepper, createStepper,
getStepStore,
StepperProvider, StepperProvider,
useStepper, useStepper,
} from "@/src/hooks/stepper"; } from "@/src/hooks/stepper";
@@ -10,6 +11,7 @@ import { Dynamic } from "solid-js/web";
import { initialSteps } from "./steps/Initial"; import { initialSteps } from "./steps/Initial";
import { createInstallerSteps } from "./steps/createInstaller"; import { createInstallerSteps } from "./steps/createInstaller";
import { installSteps } from "./steps/installSteps"; import { installSteps } from "./steps/installSteps";
import { ApiCall } from "@/src/hooks/api";
interface InstallForm extends FieldValues { interface InstallForm extends FieldValues {
data_from_step_1: string; data_from_step_1: string;
@@ -41,6 +43,8 @@ const InstallStepper = () => {
export interface InstallModalProps { export interface InstallModalProps {
machineName: string; machineName: string;
initialStep?: InstallSteps[number]["id"]; initialStep?: InstallSteps[number]["id"];
mount?: Node;
onClose?: () => void;
} }
const steps = [ const steps = [
@@ -52,10 +56,13 @@ const steps = [
export type InstallSteps = typeof steps; export type InstallSteps = typeof steps;
export interface InstallStoreType { export interface InstallStoreType {
flash: { flash: {
language: string;
keymap: string;
ssh_file: string; ssh_file: string;
device: string; device: string;
progress: ApiCall<"run_machine_flash">;
};
install: {
targetHost: string;
machineName: string;
}; };
} }
@@ -76,13 +83,18 @@ export const InstallModal = (props: InstallModalProps) => {
</Show> </Show>
); );
}; };
const [store, set] = getStepStore<InstallStoreType>(stepper);
set("install", { machineName: props.machineName });
return ( return (
<StepperProvider stepper={stepper}> <StepperProvider stepper={stepper}>
<Modal <Modal
mount={props.mount}
title="Install machine" title="Install machine"
onClose={() => { onClose={() => {
console.log("Install aborted"); console.log("Install modal closed");
props.onClose?.();
}} }}
// @ts-expect-error some steps might not have // @ts-expect-error some steps might not have
metaHeader={stepper.currentStep()?.title ? <MetaHeader /> : undefined} metaHeader={stepper.currentStep()?.title ? <MetaHeader /> : undefined}

View File

@@ -20,8 +20,9 @@ import {
useMachineFlashOptions, useMachineFlashOptions,
useSystemStorageOptions, useSystemStorageOptions,
} from "@/src/hooks/queries"; } from "@/src/hooks/queries";
import { useClanURI } from "@/src/hooks/clan";
import { useApiClient } from "@/src/hooks/ApiClient"; import { useApiClient } from "@/src/hooks/ApiClient";
import { createEffect, onMount } from "solid-js";
import { create } from "storybook/internal/theming";
const Prose = () => ( const Prose = () => (
<StepLayout <StepLayout
@@ -94,26 +95,25 @@ const ConfigureImageSchema = v.object({
v.string("Please select a key."), v.string("Please select a key."),
v.nonEmpty("Please select a key."), v.nonEmpty("Please select a key."),
), ),
language: v.pipe(v.string(), v.nonEmpty("Please choose a language.")),
keymap: v.pipe(v.string(), v.nonEmpty("Please select a keyboard layout.")),
}); });
type ConfigureImageForm = v.InferInput<typeof ConfigureImageSchema>; type ConfigureImageForm = v.InferInput<typeof ConfigureImageSchema>;
const ConfigureImage = () => { const ConfigureImage = () => {
const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
const [formStore, { Form, Field }] = createForm<ConfigureImageForm>({ const [formStore, { Form, Field }] = createForm<ConfigureImageForm>({
validate: valiForm(ConfigureImageSchema), validate: valiForm(ConfigureImageSchema),
initialValues: {
ssh_key: store.flash?.ssh_file,
},
}); });
const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
const handleSubmit: SubmitHandler<ConfigureImageForm> = (values, event) => { const handleSubmit: SubmitHandler<ConfigureImageForm> = (values, event) => {
// Push values to the store // Push values to the store
set("flash", (s) => ({ set("flash", (s) => ({
...s, ...s,
language: values.language,
keymap: values.keymap,
ssh_file: values.ssh_key, ssh_file: values.ssh_key,
})); }));
@@ -124,8 +124,9 @@ const ConfigureImage = () => {
const onSelectFile = async () => { const onSelectFile = async () => {
const req = client.fetch("get_system_file", { const req = client.fetch("get_system_file", {
file_request: { file_request: {
mode: "select_folder", mode: "get_system_file",
title: "Select a folder for you new Clan", title: "Select a folder for you new Clan",
initial_folder: "~/.ssh",
}, },
}); });
@@ -144,14 +145,20 @@ const ConfigureImage = () => {
throw new Error("No data returned from api call"); throw new Error("No data returned from api call");
}; };
const currClan = useClanURI(); const optionsQuery = useMachineFlashOptions();
const optionsQuery = useMachineFlashOptions(currClan);
let content: Node;
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<StepLayout <StepLayout
body={ body={
<div class="flex flex-col gap-2"> <div
class="flex flex-col gap-2"
ref={(el) => {
content = el;
}}
>
<Fieldset> <Fieldset>
<Field name="ssh_key"> <Field name="ssh_key">
{(field, input) => ( {(field, input) => (
@@ -172,66 +179,6 @@ const ConfigureImage = () => {
)} )}
</Field> </Field>
</Fieldset> </Fieldset>
<Fieldset>
<Field name="language">
{(field, props) => (
<Select
{...props}
value={field.value}
error={field.error}
required
label={{
label: "Language",
description: "Select your preferred language",
}}
getOptions={async () => {
if (!optionsQuery.data) {
await optionsQuery.refetch();
}
return (optionsQuery.data?.languages ?? []).map(
(lang) => ({
// TODO: Pretty label ?
value: lang,
label: lang,
}),
);
}}
placeholder="Language"
name={field.name}
/>
)}
</Field>
<Field name="keymap">
{(field, props) => (
<Select
{...props}
value={field.value}
error={field.error}
required
label={{
label: "Keymap",
description: "Select your keyboard layout",
}}
getOptions={async () => {
if (!optionsQuery.data) {
await optionsQuery.refetch();
}
return (optionsQuery.data?.keymaps ?? []).map(
(keymap) => ({
// TODO: Pretty label ?
value: keymap,
label: keymap,
}),
);
}}
placeholder="Keymap"
name={field.name}
/>
)}
</Field>
</Fieldset>
</div> </div>
} }
footer={ footer={
@@ -256,11 +203,14 @@ type ChooseDiskForm = v.InferInput<typeof ChooseDiskSchema>;
const ChooseDisk = () => { const ChooseDisk = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
const [formStore, { Form, Field }] = createForm<ChooseDiskForm>({ const [formStore, { Form, Field }] = createForm<ChooseDiskForm>({
validate: valiForm(ChooseDiskSchema), validate: valiForm(ChooseDiskSchema),
initialValues: {
disk: store.flash?.device,
},
}); });
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
const client = useApiClient(); const client = useApiClient();
const systemStorageQuery = useSystemStorageOptions(); const systemStorageQuery = useSystemStorageOptions();
@@ -273,8 +223,6 @@ const ChooseDisk = () => {
})); }));
const call = client.fetch("run_machine_flash", { const call = client.fetch("run_machine_flash", {
system_config: { system_config: {
keymap: store.flash.keymap,
language: store.flash.language,
ssh_keys_path: [store.flash.ssh_file], ssh_keys_path: [store.flash.ssh_file],
}, },
disks: [ disks: [
@@ -284,13 +232,15 @@ const ChooseDisk = () => {
}, },
], ],
}); });
// TOOD: Pass the "call" Promise to the "progress step"
set("flash", "progress", call);
console.log("Flashing", store.flash); console.log("Flashing", store.flash);
// Here you would typically trigger the disk selection process
stepSignal.next(); stepSignal.next();
}; };
const stripId = (s: string) => s.split("-")[1] ?? s;
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<StepLayout <StepLayout
@@ -300,6 +250,7 @@ const ChooseDisk = () => {
<Field name="disk"> <Field name="disk">
{(field, props) => ( {(field, props) => (
<Select <Select
zIndex={100}
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
@@ -312,11 +263,12 @@ const ChooseDisk = () => {
if (!systemStorageQuery.data) { if (!systemStorageQuery.data) {
await systemStorageQuery.refetch(); await systemStorageQuery.refetch();
} }
console.log(systemStorageQuery.data);
return (systemStorageQuery.data?.blockdevices ?? []).map( return (systemStorageQuery.data?.blockdevices ?? []).map(
(dev) => ({ (dev) => ({
value: dev.path, value: dev.path,
label: dev.name, label: stripId(dev.id_link),
}), }),
); );
}} }}
@@ -346,6 +298,17 @@ const ChooseDisk = () => {
}; };
const FlashProgress = () => { const FlashProgress = () => {
const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
onMount(async () => {
const result = await store.flash.progress.result;
if (result.status == "success") {
console.log("Flashing Success");
}
stepSignal.next();
});
return ( return (
<div class="flex h-60 w-full flex-col items-center justify-end bg-inv-4"> <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"> <div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">

View File

@@ -8,8 +8,8 @@ import {
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { Fieldset } from "@/src/components/Form/Fieldset"; import { Fieldset } from "@/src/components/Form/Fieldset";
import * as v from "valibot"; import * as v from "valibot";
import { useStepper } from "@/src/hooks/stepper"; import { getStepStore, useStepper } from "@/src/hooks/stepper";
import { InstallSteps } from "../install"; import { InstallSteps, InstallStoreType } from "../install";
import { TextInput } from "@/src/components/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { Alert } from "@/src/components/Alert/Alert"; import { Alert } from "@/src/components/Alert/Alert";
import { createSignal, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
@@ -19,6 +19,9 @@ import { Button } from "@/src/components/Button/Button";
import { Select } from "@/src/components/Select/Select"; import { Select } from "@/src/components/Select/Select";
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar"; import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
import Icon from "@/src/components/Icon/Icon"; import Icon from "@/src/components/Icon/Icon";
import { useMachineHardwareSummary } from "@/src/hooks/queries";
import { useClanURI } from "@/src/hooks/clan";
import { useApiClient } from "@/src/hooks/ApiClient";
export const InstallHeader = (props: { machineName: string }) => { export const InstallHeader = (props: { machineName: string }) => {
return ( return (
@@ -38,16 +41,24 @@ const ConfigureAdressSchema = v.object({
type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>; type ConfigureAdressForm = v.InferInput<typeof ConfigureAdressSchema>;
const ConfigureAddress = () => { const ConfigureAddress = () => {
const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal);
const [formStore, { Form, Field }] = createForm<ConfigureAdressForm>({ const [formStore, { Form, Field }] = createForm<ConfigureAdressForm>({
validate: valiForm(ConfigureAdressSchema), validate: valiForm(ConfigureAdressSchema),
initialValues: {
targetHost: store.install?.targetHost,
},
}); });
const stepSignal = useStepper<InstallSteps>();
// TODO: push values to the parent form Store // TODO: push values to the parent form Store
const handleSubmit: SubmitHandler<ConfigureAdressForm> = (values, event) => { const handleSubmit: SubmitHandler<ConfigureAdressForm> = (values, event) => {
console.log("ISO creation submitted", values); console.log("targetHost set", values);
set("install", (s) => ({ ...s, targetHost: values.targetHost }));
// Here you would typically trigger the ISO creation process // Here you would typically trigger the ISO creation process
stepSignal.next(); stepSignal.next();
console.log("Shit doesnt work", values);
}; };
return ( return (
@@ -91,12 +102,41 @@ const ConfigureAddress = () => {
const CheckHardware = () => { const CheckHardware = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
// TODO: Hook this up with api const [store, get] = getStepStore<InstallStoreType>(stepSignal);
const [report, setReport] = createSignal<boolean>(true); const hardwareQuery = useMachineHardwareSummary(
useClanURI(),
store.install.machineName,
);
const handleNext = () => { const handleNext = () => {
stepSignal.next(); stepSignal.next();
}; };
const clanUri = useClanURI();
const client = useApiClient();
const handleUpdateSummary = async () => {
// TODO: Debounce
const call = client.fetch("run_machine_hardware_info", {
target_host: {
address: store.install.targetHost,
command_prefix: "D0 YOU SEE ME LEAKING?",
},
opts: {
backend: "nixos-facter",
machine: {
flake: {
identifier: clanUri,
},
name: store.install.machineName,
},
},
});
await call.result;
hardwareQuery.refetch();
};
const reportExists = () => hardwareQuery?.data?.hardware_config !== "none";
return ( return (
<StepLayout <StepLayout
@@ -107,17 +147,28 @@ const CheckHardware = () => {
<Typography hierarchy="label" size="xs" weight="bold"> <Typography hierarchy="label" size="xs" weight="bold">
Hardware Report Hardware Report
</Typography> </Typography>
<Button hierarchy="secondary" startIcon="Report"> <Button
hierarchy="secondary"
startIcon="Report"
onClick={handleUpdateSummary}
>
Update hardware report Update hardware report
</Button> </Button>
</Orienter> </Orienter>
<Divider orientation="horizontal" /> <Divider orientation="horizontal" />
<Show when={report()}> <Show when={hardwareQuery.isLoading}>Loading...</Show>
<Alert <Show when={hardwareQuery.data}>
icon="Checkmark" {(d) => (
type="info" <Alert
title="Hardware report exists" icon="Checkmark"
/> type={reportExists() ? "warning" : "info"}
title={
reportExists()
? "Hardware report exists"
: "Hardware report not found"
}
/>
)}
</Show> </Show>
</Fieldset> </Fieldset>
</div> </div>
@@ -125,7 +176,11 @@ const CheckHardware = () => {
footer={ footer={
<div class="flex justify-between"> <div class="flex justify-between">
<BackButton /> <BackButton />
<NextButton type="button" onClick={handleNext}> <NextButton
type="button"
onClick={handleNext}
disabled={!reportExists()}
>
Next Next
</NextButton> </NextButton>
</div> </div>

View File

@@ -92,6 +92,10 @@
# Retrieve python API Typescript types # Retrieve python API Typescript types
python api.py > $out/API.json python api.py > $out/API.json
json2ts --input $out/API.json > $out/API.ts json2ts --input $out/API.json > $out/API.ts
# Substitute '{}' with 'Record<string, never>' because typescript is like that
# It treats it not as the type of an empty object, but as non-nullish.
# Should be fixed in json2ts: https://github.com/bcherny/json-schema-to-typescript/issues/557
sed -i -e 's/{}/Record<string, never>/g' $out/API.ts
# Retrieve python API Typescript types # Retrieve python API Typescript types
# delete the reserved tags from typechecking because the conversion library doesn't support them # delete the reserved tags from typechecking because the conversion library doesn't support them