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:
@@ -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;
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user