Merge pull request 'ui: new api call design' (#5319) from hgl-api into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5319
This commit is contained in:
69
pkgs/clan-app/ui/src/api/clan/client-call.ts
Normal file
69
pkgs/clan-app/ui/src/api/clan/client-call.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { API } from "@/api/API";
|
||||
|
||||
type Methods = keyof API;
|
||||
interface Header {
|
||||
logging?: { group_path: string[] };
|
||||
op_key?: string;
|
||||
}
|
||||
type Body<Method extends Methods> = API[Method]["arguments"];
|
||||
type Response<Method extends Methods> = API[Method]["return"];
|
||||
async function call<Method extends Methods>(
|
||||
method: Method,
|
||||
{
|
||||
body,
|
||||
header,
|
||||
signal,
|
||||
}: {
|
||||
body?: Body<Method>;
|
||||
header?: Header;
|
||||
signal?: AbortSignal;
|
||||
} = {},
|
||||
): Promise<Response<Method>> {
|
||||
const fn = (
|
||||
window as unknown as Record<
|
||||
Method,
|
||||
(args: { body?: Body<Method>; header?: Header }) => Promise<{
|
||||
body: Response<Method>;
|
||||
header: Record<string, unknown>;
|
||||
}>
|
||||
>
|
||||
)[method];
|
||||
if (typeof fn != "function") {
|
||||
throw new Error(`Cannot call clan non-existant method: ${method}`);
|
||||
}
|
||||
|
||||
const taskId = window.crypto.randomUUID();
|
||||
let isDone = false;
|
||||
signal?.addEventListener("abort", async () => {
|
||||
if (isDone) return;
|
||||
await call("delete_task", { body: { task_id: taskId } });
|
||||
});
|
||||
|
||||
const res = await fn({
|
||||
body,
|
||||
header: {
|
||||
...header,
|
||||
op_key: taskId,
|
||||
},
|
||||
});
|
||||
isDone = true;
|
||||
|
||||
if (res.body.status == "error") {
|
||||
const err = res.body.errors[0];
|
||||
throw new Error(`${err.message}: ${err.description}`);
|
||||
}
|
||||
|
||||
return res.body;
|
||||
}
|
||||
|
||||
export default {
|
||||
async get(url: Methods, { signal }: { signal?: AbortSignal }) {
|
||||
return await call(url, { signal });
|
||||
},
|
||||
async post<Method extends Methods>(
|
||||
url: Method,
|
||||
{ signal, body }: { signal?: AbortSignal; body: Body<Method> },
|
||||
) {
|
||||
return await call(url, { signal, body });
|
||||
},
|
||||
};
|
||||
13
pkgs/clan-app/ui/src/api/clan/client-fetch.ts
Normal file
13
pkgs/clan-app/ui/src/api/clan/client-fetch.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
async get(url: string, { signal }: { signal?: AbortSignal }) {
|
||||
const res = await fetch(url, { signal });
|
||||
return await res.json();
|
||||
},
|
||||
async post(
|
||||
url: string,
|
||||
{ signal, body }: { body?: unknown; signal?: AbortSignal },
|
||||
) {
|
||||
const res = await fetch(url, { signal, body: JSON.stringify(body) });
|
||||
return await res.json();
|
||||
},
|
||||
};
|
||||
125
pkgs/clan-app/ui/src/api/clan/index.ts
Normal file
125
pkgs/clan-app/ui/src/api/clan/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import client from "@api/clan/client";
|
||||
|
||||
// TODO: allow users to select a template
|
||||
export async function createClan({
|
||||
name,
|
||||
path,
|
||||
description,
|
||||
}: {
|
||||
name: string;
|
||||
path: string;
|
||||
description: string;
|
||||
}): Promise<void> {
|
||||
await client.post("create_clan", {
|
||||
body: {
|
||||
opts: {
|
||||
dest: path,
|
||||
template: "minimal",
|
||||
initial: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.post("create_service_instance", {
|
||||
body: {
|
||||
flake: {
|
||||
identifier: path,
|
||||
},
|
||||
module_ref: {
|
||||
name: "admin",
|
||||
input: "clan-core",
|
||||
},
|
||||
roles: {
|
||||
default: {
|
||||
tags: {
|
||||
all: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.post("create_secrets_user", {
|
||||
body: {
|
||||
flake_dir: path,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMachine({
|
||||
clan,
|
||||
name,
|
||||
targetHost,
|
||||
port,
|
||||
password,
|
||||
signal,
|
||||
}: {
|
||||
clan: string;
|
||||
name: string;
|
||||
targetHost: string;
|
||||
port?: number;
|
||||
password?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
await client.post("run_machine_update", {
|
||||
signal,
|
||||
body: {
|
||||
machine: {
|
||||
flake: { identifier: clan },
|
||||
name,
|
||||
},
|
||||
build_host: null,
|
||||
target_host: {
|
||||
address: targetHost,
|
||||
port,
|
||||
password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
UserKnownHostsFile: "/dev/null",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function installMachine({
|
||||
clan,
|
||||
name,
|
||||
targetHost,
|
||||
port,
|
||||
password,
|
||||
signal,
|
||||
}: {
|
||||
clan: string;
|
||||
name: string;
|
||||
targetHost: string;
|
||||
port?: number;
|
||||
password?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
await client.post("run_machine_install", {
|
||||
signal,
|
||||
body: {
|
||||
opts: {
|
||||
machine: {
|
||||
name,
|
||||
flake: {
|
||||
identifier: clan,
|
||||
},
|
||||
},
|
||||
},
|
||||
target_host: {
|
||||
address: targetHost,
|
||||
port,
|
||||
password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
UserKnownHostsFile: "/dev/null",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
1
pkgs/clan-app/ui/src/api/index.ts
Normal file
1
pkgs/clan-app/ui/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as clan from "./clan";
|
||||
@@ -38,6 +38,7 @@ import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal";
|
||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||
import { CubeConstruction } from "@/src/components/CubeConstruction/CubeConstruction";
|
||||
import * as api from "@/src/api";
|
||||
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
@@ -200,56 +201,22 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
event,
|
||||
) => {
|
||||
const path = `${directory}/${name}`;
|
||||
|
||||
const req = client.fetch("create_clan", {
|
||||
opts: {
|
||||
dest: path,
|
||||
// todo allow users to select a template
|
||||
template: "minimal",
|
||||
initial: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setState("creating");
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
// Set up default services
|
||||
await client.fetch("create_service_instance", {
|
||||
flake: {
|
||||
identifier: path,
|
||||
},
|
||||
module_ref: {
|
||||
name: "admin",
|
||||
input: "clan-core",
|
||||
},
|
||||
roles: {
|
||||
default: {
|
||||
tags: {
|
||||
all: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).result;
|
||||
|
||||
await client.fetch("create_secrets_user", {
|
||||
flake_dir: path,
|
||||
}).result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
setWelcomeError(resp.errors[0].message);
|
||||
try {
|
||||
await api.clan.createClan({
|
||||
name,
|
||||
path,
|
||||
description,
|
||||
});
|
||||
} catch (err) {
|
||||
setWelcomeError(String(err));
|
||||
setState("welcome");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.status === "success") {
|
||||
addClanURI(path);
|
||||
setActiveClanURI(path);
|
||||
navigateToClan(navigate, path);
|
||||
}
|
||||
addClanURI(path);
|
||||
setActiveClanURI(path);
|
||||
navigateToClan(navigate, path);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface InstallStoreType {
|
||||
machineName: string;
|
||||
mainDisk?: string;
|
||||
// ...TODO Vars
|
||||
progress: ApiCall<"run_machine_install" | "run_machine_update">;
|
||||
progress: AbortController;
|
||||
promptValues: PromptValues;
|
||||
prepareStep: "disk" | "generators" | "install";
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Button } from "@/src/components/Button/Button";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import * as api from "@/src/api";
|
||||
import { useClanURI } from "@/src/hooks/clan";
|
||||
import { AlertProps } from "@/src/components/Alert/Alert";
|
||||
import usbLogo from "@/logos/usb-stick-min.png?url";
|
||||
@@ -34,54 +34,43 @@ const UpdateStepper = (props: UpdateStepperProps) => {
|
||||
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const client = useApiClient();
|
||||
const handleUpdate = async () => {
|
||||
console.log("Starting update for", store.install.machineName);
|
||||
|
||||
if (!store.install.targetHost) {
|
||||
const targetHost = store.install.targetHost;
|
||||
if (!targetHost) {
|
||||
console.error("No target host specified, API requires it");
|
||||
return;
|
||||
}
|
||||
|
||||
const port = store.install.port
|
||||
? parseInt(store.install.port, 10)
|
||||
: undefined;
|
||||
|
||||
const call = client.fetch("run_machine_update", {
|
||||
machine: {
|
||||
flake: { identifier: clanURI },
|
||||
const abortController = new AbortController();
|
||||
set("install", "progress", abortController);
|
||||
try {
|
||||
await api.clan.updateMachine({
|
||||
clan: clanURI,
|
||||
name: store.install.machineName,
|
||||
},
|
||||
build_host: null,
|
||||
target_host: {
|
||||
address: store.install.targetHost,
|
||||
targetHost,
|
||||
port,
|
||||
password: store.install.password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
UserKnownHostsFile: "/dev/null",
|
||||
},
|
||||
},
|
||||
});
|
||||
// For cancel
|
||||
set("install", "progress", call);
|
||||
|
||||
const result = await call.result;
|
||||
|
||||
if (result.status === "error") {
|
||||
console.error("Update failed", result.errors);
|
||||
signal: abortController.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
setAlert(() => ({
|
||||
type: "error",
|
||||
title: "Update failed",
|
||||
description: result.errors[0].message,
|
||||
description: String(err),
|
||||
}));
|
||||
stepSignal.previous();
|
||||
return;
|
||||
}
|
||||
if (result.status === "success") {
|
||||
stepSignal.next();
|
||||
return;
|
||||
}
|
||||
|
||||
stepSignal.next();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -125,10 +114,7 @@ const UpdateProgress = () => {
|
||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
const handleCancel = async () => {
|
||||
const progress = store.install.progress;
|
||||
if (progress) {
|
||||
await progress.cancel();
|
||||
}
|
||||
store.install.progress.abort();
|
||||
stepSignal.previous();
|
||||
};
|
||||
const updateState =
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "@/src/hooks/queries";
|
||||
import { useClanURI } from "@/src/hooks/clan";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import * as api from "@/src/api";
|
||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||
import { Loader } from "@/src/components/Loader/Loader";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
@@ -731,33 +732,29 @@ const InstallSummary = () => {
|
||||
? parseInt(store.install.port, 10)
|
||||
: undefined;
|
||||
|
||||
const runInstall = client.fetch("run_machine_install", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: store.install.machineName,
|
||||
flake: {
|
||||
identifier: clanUri,
|
||||
},
|
||||
},
|
||||
},
|
||||
target_host: {
|
||||
address: store.install.targetHost,
|
||||
const abortController = new AbortController();
|
||||
set("install", "progress", abortController);
|
||||
|
||||
try {
|
||||
await api.clan.installMachine({
|
||||
clan: clanUri,
|
||||
name: store.install.machineName,
|
||||
targetHost: store.install.targetHost,
|
||||
port,
|
||||
password: store.install.password,
|
||||
ssh_options: {
|
||||
StrictHostKeyChecking: "no",
|
||||
UserKnownHostsFile: "/dev/null",
|
||||
},
|
||||
},
|
||||
});
|
||||
signal: abortController.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
// TODO: show other errors
|
||||
}
|
||||
set("install", (s) => ({
|
||||
...s,
|
||||
prepareStep: "install",
|
||||
progress: runInstall,
|
||||
}));
|
||||
|
||||
await runInstall.result;
|
||||
|
||||
stepSignal.setActiveStep("install:done");
|
||||
};
|
||||
return (
|
||||
@@ -818,10 +815,7 @@ const InstallProgress = () => {
|
||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||
|
||||
const handleCancel = async () => {
|
||||
const progress = store.install.progress;
|
||||
if (progress) {
|
||||
await progress.cancel();
|
||||
}
|
||||
store.install.progress.abort();
|
||||
stepSignal.previous();
|
||||
};
|
||||
const installState = useNotifyOrigin<ProcessMessage<unknown, InstallTopic>>(
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"allowJs": true,
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"@api/clan/client": ["./src/api/clan/client-call"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ function regenPythonApiOnFileChange() {
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
|
||||
"@": path.resolve(__dirname, "./"),
|
||||
// Different script can be used based on different env vars
|
||||
"@api/clan/client": path.resolve(__dirname, "./src/api/clan/client-call"),
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
|
||||
Reference in New Issue
Block a user