ui-2d: Fix build errors

This commit is contained in:
Qubasa
2025-07-02 15:59:50 +07:00
parent 672db4a33f
commit 8e00363584
21 changed files with 220 additions and 365 deletions

View File

@@ -1,84 +0,0 @@
{
"name": "@clan/ui",
"version": "0.0.1",
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "npm run check && npm run test && vite build && npm run convert-html",
"convert-html": "node gtk.webview.js",
"serve": "vite preview",
"check": "tsc --noEmit --skipLibCheck && eslint ./src --fix",
"knip": "knip --fix",
"test": "vitest run --project unit --typecheck",
"storybook": "storybook",
"storybook-build": "storybook build",
"storybook-dev": "storybook dev -p 6006",
"test-storybook": "vitest run --project storybook",
"test-storybook-update-snapshots": "vitest run --project storybook --update",
"test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'npx http-server storybook-static --port 6006 --silent' 'npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
},
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.3.0",
"@kachurun/storybook-solid": "^9.0.11",
"@kachurun/storybook-solid-vite": "^9.0.11",
"@storybook/addon-a11y": "^9.0.8",
"@storybook/addon-docs": "^9.0.8",
"@storybook/addon-links": "^9.0.8",
"@storybook/addon-onboarding": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@tailwindcss/typography": "^0.5.13",
"@types/json-schema": "^7.0.15",
"@types/node": "^22.15.19",
"@vitest/browser": "^3.2.3",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
"concurrently": "^9.1.2",
"eslint": "^9.27.0",
"eslint-plugin-tailwindcss": "^3.17.0",
"jsdom": "^26.1.0",
"knip": "^5.61.2",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"tailwindcss": "^4.0.0",
"typescript": "^5.4.5",
"typescript-eslint": "^8.32.1",
"vite": "^7.0.0",
"vite-plugin-solid": "^2.8.2",
"vite-plugin-solid-svg": "^0.8.1",
"vitest": "^3.2.3"
},
"dependencies": {
"@floating-ui/dom": "^1.6.8",
"@kobalte/core": "^0.13.10",
"@kobalte/tailwindcss": "^0.9.0",
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"corvu": "^0.7.1",
"nanoid": "^5.0.7",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.25.4",
"@esbuild/darwin-x64": "^0.25.4",
"@esbuild/linux-arm64": "^0.25.4",
"@esbuild/linux-x64": "^0.25.4"
},
"overrides": {
"vite": {
"rollup": "npm:@rollup/wasm-node@^4.34.9"
},
"@rollup/rollup-darwin-x64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-linux-x64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-darwin-arm64": "npm:@rollup/wasm-node@^4.34.9",
"@rollup/rollup-linux-arm64": "npm:@rollup/wasm-node@^4.34.9"
}
}

View File

@@ -0,0 +1 @@
../ui/package.json

View File

@@ -19,7 +19,6 @@ import {
} from "@/src/components/inputBase"; } from "@/src/components/inputBase";
import { FieldLayout } from "./layout"; import { FieldLayout } from "./layout";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { useContext } from "corvu/dialog";
interface Option { interface Option {
value: string; value: string;
@@ -51,9 +50,6 @@ interface SelectInputpProps {
} }
export function SelectInput(props: SelectInputpProps) { export function SelectInput(props: SelectInputpProps) {
const dialogContext = (dialogContextId?: string) =>
useContext(dialogContextId);
const _id = createUniqueId(); const _id = createUniqueId();
const [reference, setReference] = createSignal<HTMLElement>(); const [reference, setReference] = createSignal<HTMLElement>();

View File

@@ -23,9 +23,37 @@ export type SuccessQuery<T extends OperationNames> = Extract<
>; >;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"]; export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
function isMachine(obj: unknown): obj is Machine {
return (
!!obj &&
typeof obj === "object" &&
typeof (obj as any).name === "string" &&
typeof (obj as any).flake === "object" &&
typeof (obj as any).flake.identifier === "string"
);
}
// Machine type with flake for API calls
interface Machine {
name: string;
flake: {
identifier: string;
};
}
interface BackendOpts {
logging?: { group: string | Machine };
}
interface BackendReturnType<K extends OperationNames> {
result: OperationResponse<K>;
metadata: Record<string, any>;
}
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
// if window[method] does not exist, throw an error // if window[method] does not exist, throw an error
if (!(method in window)) { if (!(method in window)) {
@@ -33,19 +61,30 @@ const _callApi = <K extends OperationNames>(
// return a rejected promise // return a rejected promise
return { return {
promise: Promise.resolve({ promise: Promise.resolve({
status: "error", status: "error",
errors: [ errors: [
{ {
message: `Method ${method} not found on window object`, message: `Method ${method} not found on window object`,
code: "method_not_found", code: "method_not_found",
}, },
], ],
op_key: "noop", op_key: "noop",
}), }),
op_key: "noop", op_key: "noop",
}; };
} }
let metadata: BackendOpts | undefined = undefined;
if (backendOpts != undefined) {
metadata = { ...backendOpts };
let group = backendOpts?.logging?.group;
if (group != undefined && isMachine(group)) {
metadata = {
logging: { group: group.flake.identifier + "#" + group.name },
};
}
}
const promise = ( const promise = (
window as unknown as Record< window as unknown as Record<
OperationNames, OperationNames,
@@ -105,10 +144,11 @@ const handleCancel = async <K extends OperationNames>(
export const callApi = <K extends OperationNames>( export const callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args); console.log("Calling API", method, args, backendOpts);
const { promise, op_key } = _callApi(method, args); const { promise, op_key } = _callApi(method, args, backendOpts);
promise.catch((error) => { promise.catch((error) => {
toast.custom( toast.custom(
(t) => ( (t) => (
@@ -146,13 +186,14 @@ export const callApi = <K extends OperationNames>(
console.log("Not printing toast because operation was cancelled"); console.log("Not printing toast because operation was cancelled");
} }
if (response.status === "error" && !cancelled) { const result = response;
if (result.status === "error" && !cancelled) {
toast.remove(toastId); toast.remove(toastId);
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
t={t} t={t}
message={"Error: " + response.errors[0].message} message={"Error: " + result.errors[0].message}
/> />
), ),
{ {
@@ -162,7 +203,8 @@ export const callApi = <K extends OperationNames>(
} else { } else {
toast.remove(toastId); toast.remove(toastId);
} }
return response; return result;
}); });
return { promise: new_promise, op_key: op_key }; return { promise: new_promise, op_key: op_key };
}; };

View File

@@ -61,7 +61,7 @@ export const ApiTester = () => {
return await callApi( return await callApi(
values.endpoint as keyof API, values.endpoint as keyof API,
JSON.parse(values.payload || "{}"), JSON.parse(values.payload || "{}"),
); ).promise;
}, },
staleTime: Infinity, staleTime: Infinity,
enabled: false, enabled: false,

View File

@@ -27,5 +27,5 @@
} }
.button--dark-active:active { .button--dark-active:active {
@apply active:border-secondary-900 active:shadow-button-primary-active; @apply active:border-secondary-900;
} }

View File

@@ -7,5 +7,5 @@
} }
.button--ghost-active:active { .button--ghost-active:active {
@apply active:bg-secondary-200 active:text-secondary-900 active:shadow-button-primary-active; @apply active:bg-secondary-200 active:text-secondary-900;
} }

View File

@@ -27,7 +27,7 @@
} }
.button--light-active:active { .button--light-active:active {
@apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900 active:shadow-button-primary-active; @apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900;
box-shadow: inset 2px 2px theme(backgroundColor.secondary.300); box-shadow: inset 2px 2px theme(backgroundColor.secondary.300);

View File

@@ -17,7 +17,7 @@ const defaultRemoteData: RemoteData = {
private_key: undefined, private_key: undefined,
password: "", password: "",
forward_agent: true, forward_agent: true,
host_key_check: 0, host_key_check: "strict",
verbose_ssh: false, verbose_ssh: false,
ssh_options: {}, ssh_options: {},
tor_socks: false, tor_socks: false,
@@ -32,7 +32,7 @@ const sampleRemoteData: RemoteData = {
private_key: undefined, private_key: undefined,
password: "", password: "",
forward_agent: true, forward_agent: true,
host_key_check: 1, host_key_check: "ask",
verbose_ssh: false, verbose_ssh: false,
ssh_options: { ssh_options: {
StrictHostKeyChecking: "no", StrictHostKeyChecking: "no",
@@ -238,7 +238,7 @@ const advancedRemoteData: RemoteData = {
private_key: undefined, private_key: undefined,
password: "", password: "",
forward_agent: false, forward_agent: false,
host_key_check: 2, host_key_check: "none",
verbose_ssh: true, verbose_ssh: true,
ssh_options: { ssh_options: {
ConnectTimeout: "10", ConnectTimeout: "10",

View File

@@ -11,13 +11,6 @@ import { Loader } from "@/src/components/v2/Loader/Loader";
import { Button } from "@/src/components/v2/Button/Button"; import { Button } from "@/src/components/v2/Button/Button";
import Accordion from "@/src/components/accordion"; import Accordion from "@/src/components/accordion";
// Define the HostKeyCheck enum values with proper API mapping
export enum HostKeyCheck {
ASK = 0,
TOFU = 1,
IGNORE = 2,
}
// Export the API types for use in other components // Export the API types for use in other components
export type { RemoteData, Machine, RemoteDataSource }; export type { RemoteData, Machine, RemoteDataSource };
@@ -185,40 +178,6 @@ export function RemoteForm(props: RemoteFormProps) {
const [formData, setFormData] = createSignal<RemoteData | null>(null); const [formData, setFormData] = createSignal<RemoteData | null>(null);
const [isSaving, setIsSaving] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false);
const hostKeyCheckOptions = [
{ value: "ASK", label: "Ask" },
{ value: "TOFU", label: "TOFU (Trust On First Use)" },
{ value: "IGNORE", label: "Ignore" },
];
// Helper function to convert enum name to numeric value
const getHostKeyCheckValue = (name: string): number => {
switch (name) {
case "ASK":
return HostKeyCheck.ASK;
case "TOFU":
return HostKeyCheck.TOFU;
case "IGNORE":
return HostKeyCheck.IGNORE;
default:
return HostKeyCheck.ASK;
}
};
// Helper function to convert numeric value to enum name
const getHostKeyCheckName = (value: number | undefined): string => {
switch (value) {
case HostKeyCheck.ASK:
return "ASK";
case HostKeyCheck.TOFU:
return "TOFU";
case HostKeyCheck.IGNORE:
return "IGNORE";
default:
return "ASK";
}
};
// Query host data when machine is provided // Query host data when machine is provided
const hostQuery = useQuery(() => ({ const hostQuery = useQuery(() => ({
queryKey: [ queryKey: [
@@ -241,11 +200,15 @@ export function RemoteForm(props: RemoteFormProps) {
}); });
} }
const result = await callApi("get_host", { const result = await callApi(
name: props.machine.name, "get_host",
flake: props.machine.flake, {
field: props.field || "targetHost", name: props.machine.name,
}).promise; flake: props.machine.flake,
field: props.field || "targetHost",
},
{logging: { group: { name: props.machine.name, flake: props.machine.flake } },},
).promise;
if (result.status === "error") if (result.status === "error")
throw new Error("Failed to fetch host data"); throw new Error("Failed to fetch host data");
@@ -372,16 +335,13 @@ export function RemoteForm(props: RemoteFormProps) {
<SelectInput <SelectInput
label="Host Key Check" label="Host Key Check"
value={getHostKeyCheckName(formData()?.host_key_check)} value={formData()?.host_key_check || "ask"}
options={hostKeyCheckOptions} options={[
selectProps={{ { value: "ask", label: "Ask" },
onInput: (e) => { value: "none", label: "None" },
updateFormData({ { value: "strict", label: "Strict" },
host_key_check: getHostKeyCheckValue( { value: "tofu", label: "Trust on First Use" },
e.currentTarget.value, ]}
) as 0 | 1 | 2 | 3,
}),
}}
disabled={computedDisabled} disabled={computedDisabled}
helperText="How to handle host key verification" helperText="How to handle host key verification"
/> />

View File

@@ -0,0 +1,39 @@
import { JSX, Show } from "solid-js";
interface SimpleModalProps {
open: boolean;
onClose: () => void;
title?: string;
children: JSX.Element;
}
export const SimpleModal = (props: SimpleModalProps) => {
return (
<Show when={props.open}>
<div class="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div class="fixed inset-0 bg-black/50" onClick={props.onClose} />
{/* Modal Content */}
<div class="relative mx-4 w-full max-w-md rounded-lg bg-white shadow-lg">
{/* Header */}
<Show when={props.title}>
<div class="flex items-center justify-between border-b p-4">
<h3 class="text-lg font-semibold">{props.title}</h3>
<button
type="button"
class="text-gray-400 hover:text-gray-600"
onClick={props.onClose}
>
×
</button>
</div>
</Show>
{/* Body */}
<div>{props.children}</div>
</div>
</div>
</Show>
);
};

View File

@@ -125,7 +125,7 @@ export const InputLabel = (props: InputLabelProps) => {
weight="bold" weight="bold"
class="inline-flex gap-1 align-middle !fg-def-1" class="inline-flex gap-1 align-middle !fg-def-1"
classList={{ classList={{
[cx("!fg-semantic-1")]: !!props.error, [cx("!text-red-600")]: !!props.error,
}} }}
aria-invalid={props.error} aria-invalid={props.error}
> >
@@ -185,7 +185,7 @@ export const InputError = (props: InputErrorProps) => {
// @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now // @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now
size="xxs" size="xxs"
weight="medium" weight="medium"
class={cx("col-span-full px-1 !fg-semantic-4", typoClasses)} class={cx("col-span-full px-1 !text-red-500", typoClasses)}
{...rest} {...rest}
> >
{props.error} {props.error}

View File

@@ -47,11 +47,14 @@ export const MachineListItem = (props: MachineListItemProps) => {
); );
return; return;
} }
const target_host = await callApi("get_host", { const target_host = await callApi(
field: "targetHost", "get_host",
flake: { identifier: active_clan }, {
name: name, field: "targetHost",
}).promise; flake: { identifier: active_clan },
name: name,
}, {logging: { group: { name, flake: { identifier: active_clan } } }}
).promise;
if (target_host.status == "error") { if (target_host.status == "error") {
console.error("No target host found for the machine"); console.error("No target host found for the machine");
@@ -79,7 +82,6 @@ export const MachineListItem = (props: MachineListItemProps) => {
}, },
no_reboot: true, no_reboot: true,
debug: true, debug: true,
nix_options: [],
password: null, password: null,
}, },
target_host: target_host.data!.data, target_host: target_host.data!.data,
@@ -108,6 +110,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
field: "targetHost", field: "targetHost",
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, {
logging: { group: { name, flake: { identifier: active_clan } } },
}).promise; }).promise;
if (target_host.status == "error") { if (target_host.status == "error") {
@@ -129,7 +133,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
field: "buildHost", field: "buildHost",
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}).promise; }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise;
if (build_host.status == "error") { if (build_host.status == "error") {
console.error("No target host found for the machine"); console.error("No target host found for the machine");
@@ -150,7 +154,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
}, },
target_host: target_host.data!.data, target_host: target_host.data!.data,
build_host: build_host.data?.data || null, build_host: build_host.data?.data || null,
}).promise; }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise;
setUpdating(false); setUpdating(false);
}; };

View File

@@ -1,134 +0,0 @@
import Dialog from "corvu/dialog";
import { createSignal, JSX } from "solid-js";
import { Button } from "../Button/Button";
import Icon from "../icon";
import cx from "classnames";
interface ModalProps {
open: boolean | undefined;
handleClose: () => void;
title: string;
children: JSX.Element;
class?: string;
}
export const Modal = (props: ModalProps) => {
const [dragging, setDragging] = createSignal(false);
const [startOffset, setStartOffset] = createSignal({ x: 0, y: 0 });
let dialogRef: HTMLDivElement;
const handleMouseDown = (e: MouseEvent) => {
setDragging(true);
const rect = dialogRef.getBoundingClientRect();
setStartOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
};
const handleMouseMove = (e: MouseEvent) => {
if (dragging()) {
let newTop = e.clientY - startOffset().y;
let newLeft = e.clientX - startOffset().x;
if (newTop < 0) {
newTop = 0;
}
if (newLeft < 0) {
newLeft = 0;
}
dialogRef.style.top = `${newTop}px`;
dialogRef.style.left = `${newLeft}px`;
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
}
};
const handleMouseUp = () => setDragging(false);
return (
<Dialog open={props.open} trapFocus={true}>
<Dialog.Portal>
<Dialog.Overlay
class="fixed inset-0 z-50 bg-black/50"
onMouseMove={handleMouseMove}
/>
<Dialog.Content
class={cx(
"overflow-hidden absolute left-1/3 top-1/3 z-50 min-w-[560px] rounded-md border border-def-4 focus-visible:outline-none",
props.class,
)}
classList={{
"!cursor-grabbing": dragging(),
[cx("scale-[101%] transition-transform")]: dragging(),
}}
ref={(el) => {
dialogRef = el;
}}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseDown={(e: MouseEvent) => {
e.stopPropagation(); // Prevent backdrop drag conflict
}}
onClick={(e: MouseEvent) => e.stopPropagation()} // Prevent backdrop click closing
>
<Dialog.Label
as="div"
class="flex w-full justify-center border-b-2 px-4 py-2 align-middle bg-def-3 border-def-4"
onMouseDown={handleMouseDown}
>
<div
class="flex w-full cursor-move flex-col gap-px py-1 "
classList={{
"!cursor-grabbing": dragging(),
}}
>
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
</div>
<span class="mx-2 select-none whitespace-nowrap">
{props.title}
</span>
<div
class="flex w-full cursor-move flex-col gap-px py-1 "
classList={{
"!cursor-grabbing": dragging(),
}}
>
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
<hr class="h-px w-full border-none bg-secondary-300" />
</div>
<div class="absolute right-1 top-2 pl-1 bg-def-3">
<Button
onMouseDown={(e) => e.stopPropagation()}
tabIndex={-1}
class="size-4"
variant="ghost"
onClick={() => props.handleClose()}
size="s"
startIcon={<Icon icon={"Close"} />}
/>
</div>
</Dialog.Label>
<Dialog.Description
class="flex max-h-[90vh] flex-col overflow-y-hidden bg-def-1"
as="div"
>
{props.children}
</Dialog.Description>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};

View File

@@ -1,4 +1,4 @@
@import "material-icons/iconfont/filled.css"; /* @import "material-icons/iconfont/filled.css"; */
/* List of icons: https://marella.me/material-icons/demo/ */ /* List of icons: https://marella.me/material-icons/demo/ */
/* @import url(./components/Typography/css/typography.css); */ /* @import url(./components/Typography/css/typography.css); */

View File

@@ -19,10 +19,12 @@ import { createEffect, createSignal } from "solid-js"; // For, Show might not be
import toast from "solid-toast"; import toast from "solid-toast";
import { FieldLayout } from "@/src/Form/fields/layout"; import { FieldLayout } from "@/src/Form/fields/layout";
import { InputLabel } from "@/src/components/inputBase"; import { InputLabel } from "@/src/components/inputBase";
import { Modal } from "@/src/components/modal";
import Fieldset from "@/src/Form/fieldset"; // Still used for other fieldsets import Fieldset from "@/src/Form/fieldset"; // Still used for other fieldsets
import Accordion from "@/src/components/accordion"; import Accordion from "@/src/components/accordion";
import { SimpleModal } from "@/src/components/SimpleModal";
// Import the new generic component // Import the new generic component
import { import {
FileSelectorField, FileSelectorField,
@@ -192,12 +194,11 @@ export const Flash = () => {
return ( return (
<> <>
<Header title="Flash installer" /> <Header title="Flash installer" />
<Modal <SimpleModal
open={confirmOpen() || isFlashing()} open={confirmOpen() || isFlashing()}
handleClose={() => !isFlashing() && setConfirmOpen(false)} onClose={() => !isFlashing() && setConfirmOpen(false)}
title="Confirm" title="Confirm"
> >
{/* ... Modal content as before ... */}
<div class="flex flex-col gap-4 p-4"> <div class="flex flex-col gap-4 p-4">
<div class="flex flex-col justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2"> <div class="flex flex-col justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2">
<Typography <Typography
@@ -230,7 +231,7 @@ export const Flash = () => {
</Button> </Button>
</div> </div>
</div> </div>
</Modal> </SimpleModal>
<div class="w-full self-stretch p-8"> <div class="w-full self-stretch p-8">
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@@ -125,7 +125,6 @@ export function InstallMachine(props: InstallMachineProps) {
machine: { machine: {
name: props.name, name: props.name,
flake: { identifier: curr_uri }, flake: { identifier: curr_uri },
private_key: values.sshKey?.name,
}, },
}, },
target_host: targetHostResponse.data.data, target_host: targetHostResponse.data.data,

View File

@@ -55,7 +55,7 @@ export function MachineForm(props: MachineFormProps) {
...values.machine, ...values.machine,
tags: Array.from(values.machine.tags || detailed.machine.tags || []), tags: Array.from(values.machine.tags || detailed.machine.tags || []),
}, },
}).promise; } ).promise;
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: [ queryKey: [
@@ -80,7 +80,7 @@ export function MachineForm(props: MachineFormProps) {
const result = await callApi("get_generators_closure", { const result = await callApi("get_generators_closure", {
base_dir: base_dir, base_dir: base_dir,
machine_name: machine_name, machine_name: machine_name,
}).promise; }, {logging: {group: { name: machine_name, flake: {identifier: base_dir} }}}).promise;
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },
@@ -118,7 +118,8 @@ export function MachineForm(props: MachineFormProps) {
flake: { flake: {
identifier: curr_uri, identifier: curr_uri,
}, },
}).promise; }, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }}
).promise;
if (target.status === "error") { if (target.status === "error") {
toast.error("Failed to get target host"); toast.error("Failed to get target host");
@@ -143,7 +144,7 @@ export function MachineForm(props: MachineFormProps) {
...target_host, ...target_host,
}, },
build_host: null, build_host: null,
}).promise.finally(() => { }, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }}).promise.finally(() => {
setIsUpdating(false); setIsUpdating(false);
}); });
}; };

View File

@@ -44,7 +44,7 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
command_prefix: "sudo", command_prefix: "sudo",
port: 22, port: 22,
forward_agent: false, forward_agent: false,
host_key_check: 1, // 0 = ASK host_key_check: "ask", // 0 = ASK
verbose_ssh: false, verbose_ssh: false,
ssh_options: {}, ssh_options: {},
tor_socks: false, tor_socks: false,

View File

@@ -153,10 +153,10 @@ export const VarsStep = (props: VarsStepProps) => {
base_dir: props.dir, base_dir: props.dir,
machine_name: props.machine_id, machine_name: props.machine_id,
full_closure: props.fullClosure, full_closure: props.fullClosure,
}).promise; }, {logging: {group: { name: props.machine_id, flake: {identifier: props.dir} }}}).promise;
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },
})); }));
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => { const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {

View File

@@ -8,6 +8,7 @@ import Icon from "@/src/components/icon";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { makePersisted } from "@solid-primitives/storage"; import { makePersisted } from "@solid-primitives/storage";
import { useClanContext } from "@/src/contexts/clan"; import { useClanContext } from "@/src/contexts/clan";
import { debug } from "console";
type MachinesModel = Extract< type MachinesModel = Extract<
OperationResponse<"list_machines">, OperationResponse<"list_machines">,
@@ -38,6 +39,7 @@ export const MachineListView: Component = () => {
}, },
}).promise; }).promise;
console.log("response", response); console.log("response", response);
if (response.status === "error") { if (response.status === "error") {
console.error("Failed to fetch data"); console.error("Failed to fetch data");
} else { } else {

View File

@@ -1,26 +1,19 @@
import { API, Error as ApiError } from "@/api/API"; import { API } from "@/api/API";
import { Schema as Inventory } from "@/api/Inventory"; import { Schema as Inventory } from "@/api/Inventory";
import { toast } from "solid-toast"; import { toast } from "solid-toast";
import { import {
ErrorToastComponent, ErrorToastComponent,
CancelToastComponent, CancelToastComponent,
} from "@/src/components/toast"; } from "@/src/components/toast";
type OperationNames = keyof API; type OperationNames = keyof API;
type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
type ApiEnvelope<T> =
| {
status: "success";
data: T;
op_key: string;
}
| ApiError;
type Services = NonNullable<Inventory["services"]>; type Services = NonNullable<Inventory["services"]>;
type ServiceNames = keyof Services; type ServiceNames = keyof Services;
type ClanService<T extends ServiceNames> = Services[T];
type ClanServiceInstance<T extends ServiceNames> = NonNullable< export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
Services[T] Services[T]
>[string]; >[string];
@@ -28,51 +21,83 @@ export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>, OperationResponse<T>,
{ status: "success" } { status: "success" }
>; >;
type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"]; export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
type ErrorQuery<T extends OperationNames> = Extract< function isMachine(obj: unknown): obj is Machine {
OperationResponse<T>, return (
{ status: "error" } !!obj &&
>; typeof obj === "object" &&
type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"]; typeof (obj as Machine).name === "string" &&
typeof (obj as Machine).flake === "object" &&
type ClanOperations = Record<OperationNames, (str: string) => void>; typeof (obj as Machine).flake.identifier === "string"
);
interface GtkResponse<T> {
result: T;
op_key: string;
} }
// Machine type with flake for API calls
interface Machine {
name: string;
flake: {
identifier: string;
};
}
interface BackendOpts {
logging?: { group: string | Machine };
}
interface BackendReturnType<K extends OperationNames> {
result: OperationResponse<K>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata: Record<string, any>;
}
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { backendOpts?: BackendOpts,
): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
// if window[method] does not exist, throw an error // if window[method] does not exist, throw an error
if (!(method in window)) { if (!(method in window)) {
console.error(`Method ${method} not found on window object`); console.error(`Method ${method} not found on window object`);
// return a rejected promise // return a rejected promise
return { return {
promise: Promise.resolve({ promise: Promise.resolve({
status: "error", result: {
errors: [ status: "error",
{ errors: [
message: `Method ${method} not found on window object`, {
code: "method_not_found", message: `Method ${method} not found on window object`,
}, code: "method_not_found",
], },
op_key: "noop", ],
op_key: "noop",
},
metadata: {},
}), }),
op_key: "noop", op_key: "noop",
}; };
} }
let metadata: BackendOpts | undefined = undefined;
if (backendOpts != undefined) {
metadata = { ...backendOpts };
const group = backendOpts?.logging?.group;
if (group != undefined && isMachine(group)) {
metadata = {
logging: { group: group.flake.identifier + "#" + group.name },
};
}
}
const promise = ( const promise = (
window as unknown as Record< window as unknown as Record<
OperationNames, OperationNames,
( (
args: OperationArgs<OperationNames>, args: OperationArgs<OperationNames>,
) => Promise<OperationResponse<OperationNames>> metadata?: BackendOpts,
) => Promise<BackendReturnType<OperationNames>>
> >
)[method](args) as Promise<OperationResponse<K>>; )[method](args, metadata) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string; const op_key = (promise as any)._webviewMessageId as string;
@@ -82,7 +107,7 @@ const _callApi = <K extends OperationNames>(
const handleCancel = async <K extends OperationNames>( const handleCancel = async <K extends OperationNames>(
ops_key: string, ops_key: string,
orig_task: Promise<OperationResponse<K>>, orig_task: Promise<BackendReturnType<K>>,
) => { ) => {
console.log("Canceling operation: ", ops_key); console.log("Canceling operation: ", ops_key);
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
@@ -102,7 +127,7 @@ const handleCancel = async <K extends OperationNames>(
}); });
const resp = await promise; const resp = await promise;
if (resp.status === "error") { if (resp.result.status === "error") {
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
@@ -124,10 +149,11 @@ const handleCancel = async <K extends OperationNames>(
export const callApi = <K extends OperationNames>( export const callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args); console.log("Calling API", method, args);
const { promise, op_key } = _callApi(method, args); const { promise, op_key } = _callApi(method, args, backendOpts);
promise.catch((error) => { promise.catch((error) => {
toast.custom( toast.custom(
(t) => ( (t) => (
@@ -165,13 +191,14 @@ export const callApi = <K extends OperationNames>(
console.log("Not printing toast because operation was cancelled"); console.log("Not printing toast because operation was cancelled");
} }
if (response.status === "error" && !cancelled) { const result = response.result;
if (result.status === "error" && !cancelled) {
toast.remove(toastId); toast.remove(toastId);
toast.custom( toast.custom(
(t) => ( (t) => (
<ErrorToastComponent <ErrorToastComponent
t={t} t={t}
message={"Error: " + response.errors[0].message} message={"Error: " + result.errors[0].message}
/> />
), ),
{ {
@@ -181,7 +208,8 @@ export const callApi = <K extends OperationNames>(
} else { } else {
toast.remove(toastId); toast.remove(toastId);
} }
return response; return result;
}); });
return { promise: new_promise, op_key: op_key }; return { promise: new_promise, op_key: op_key };
}; };