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 { ListClansModal } from "@/src/modals/ListClansModal/ListClansModal";
|
||||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||||
import { CubeConstruction } from "@/src/components/CubeConstruction/CubeConstruction";
|
import { CubeConstruction } from "@/src/components/CubeConstruction/CubeConstruction";
|
||||||
|
import * as api from "@/src/api";
|
||||||
|
|
||||||
type State = "welcome" | "setup" | "creating";
|
type State = "welcome" | "setup" | "creating";
|
||||||
|
|
||||||
@@ -200,56 +201,22 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
event,
|
event,
|
||||||
) => {
|
) => {
|
||||||
const path = `${directory}/${name}`;
|
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");
|
setState("creating");
|
||||||
|
try {
|
||||||
const resp = await req.result;
|
await api.clan.createClan({
|
||||||
|
name,
|
||||||
// Set up default services
|
path,
|
||||||
await client.fetch("create_service_instance", {
|
description,
|
||||||
flake: {
|
});
|
||||||
identifier: path,
|
} catch (err) {
|
||||||
},
|
setWelcomeError(String(err));
|
||||||
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);
|
|
||||||
setState("welcome");
|
setState("welcome");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status === "success") {
|
addClanURI(path);
|
||||||
addClanURI(path);
|
setActiveClanURI(path);
|
||||||
setActiveClanURI(path);
|
navigateToClan(navigate, path);
|
||||||
navigateToClan(navigate, path);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export interface InstallStoreType {
|
|||||||
machineName: string;
|
machineName: string;
|
||||||
mainDisk?: string;
|
mainDisk?: string;
|
||||||
// ...TODO Vars
|
// ...TODO Vars
|
||||||
progress: ApiCall<"run_machine_install" | "run_machine_update">;
|
progress: AbortController;
|
||||||
promptValues: PromptValues;
|
promptValues: PromptValues;
|
||||||
prepareStep: "disk" | "generators" | "install";
|
prepareStep: "disk" | "generators" | "install";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Button } from "@/src/components/Button/Button";
|
|||||||
import Icon from "@/src/components/Icon/Icon";
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||||
import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
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 { useClanURI } from "@/src/hooks/clan";
|
||||||
import { AlertProps } from "@/src/components/Alert/Alert";
|
import { AlertProps } from "@/src/components/Alert/Alert";
|
||||||
import usbLogo from "@/logos/usb-stick-min.png?url";
|
import usbLogo from "@/logos/usb-stick-min.png?url";
|
||||||
@@ -34,54 +34,43 @@ const UpdateStepper = (props: UpdateStepperProps) => {
|
|||||||
|
|
||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
|
|
||||||
const client = useApiClient();
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
console.log("Starting update for", store.install.machineName);
|
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");
|
console.error("No target host specified, API requires it");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = store.install.port
|
const port = store.install.port
|
||||||
? parseInt(store.install.port, 10)
|
? parseInt(store.install.port, 10)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const call = client.fetch("run_machine_update", {
|
const abortController = new AbortController();
|
||||||
machine: {
|
set("install", "progress", abortController);
|
||||||
flake: { identifier: clanURI },
|
try {
|
||||||
|
await api.clan.updateMachine({
|
||||||
|
clan: clanURI,
|
||||||
name: store.install.machineName,
|
name: store.install.machineName,
|
||||||
},
|
targetHost,
|
||||||
build_host: null,
|
|
||||||
target_host: {
|
|
||||||
address: store.install.targetHost,
|
|
||||||
port,
|
port,
|
||||||
password: store.install.password,
|
password: store.install.password,
|
||||||
ssh_options: {
|
signal: abortController.signal,
|
||||||
StrictHostKeyChecking: "no",
|
});
|
||||||
UserKnownHostsFile: "/dev/null",
|
} catch (err) {
|
||||||
},
|
if (abortController.signal.aborted) {
|
||||||
},
|
return;
|
||||||
});
|
}
|
||||||
// For cancel
|
|
||||||
set("install", "progress", call);
|
|
||||||
|
|
||||||
const result = await call.result;
|
|
||||||
|
|
||||||
if (result.status === "error") {
|
|
||||||
console.error("Update failed", result.errors);
|
|
||||||
setAlert(() => ({
|
setAlert(() => ({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Update failed",
|
title: "Update failed",
|
||||||
description: result.errors[0].message,
|
description: String(err),
|
||||||
}));
|
}));
|
||||||
stepSignal.previous();
|
stepSignal.previous();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "success") {
|
|
||||||
stepSignal.next();
|
stepSignal.next();
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,10 +114,7 @@ const UpdateProgress = () => {
|
|||||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
const progress = store.install.progress;
|
store.install.progress.abort();
|
||||||
if (progress) {
|
|
||||||
await progress.cancel();
|
|
||||||
}
|
|
||||||
stepSignal.previous();
|
stepSignal.previous();
|
||||||
};
|
};
|
||||||
const updateState =
|
const updateState =
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "@/src/hooks/queries";
|
} from "@/src/hooks/queries";
|
||||||
import { useClanURI } from "@/src/hooks/clan";
|
import { useClanURI } from "@/src/hooks/clan";
|
||||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
|
import * as api from "@/src/api";
|
||||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||||
import { Loader } from "@/src/components/Loader/Loader";
|
import { Loader } from "@/src/components/Loader/Loader";
|
||||||
import { Button as KButton } from "@kobalte/core/button";
|
import { Button as KButton } from "@kobalte/core/button";
|
||||||
@@ -731,33 +732,29 @@ const InstallSummary = () => {
|
|||||||
? parseInt(store.install.port, 10)
|
? parseInt(store.install.port, 10)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const runInstall = client.fetch("run_machine_install", {
|
const abortController = new AbortController();
|
||||||
opts: {
|
set("install", "progress", abortController);
|
||||||
machine: {
|
|
||||||
name: store.install.machineName,
|
try {
|
||||||
flake: {
|
await api.clan.installMachine({
|
||||||
identifier: clanUri,
|
clan: clanUri,
|
||||||
},
|
name: store.install.machineName,
|
||||||
},
|
targetHost: store.install.targetHost,
|
||||||
},
|
|
||||||
target_host: {
|
|
||||||
address: store.install.targetHost,
|
|
||||||
port,
|
port,
|
||||||
password: store.install.password,
|
password: store.install.password,
|
||||||
ssh_options: {
|
signal: abortController.signal,
|
||||||
StrictHostKeyChecking: "no",
|
});
|
||||||
UserKnownHostsFile: "/dev/null",
|
} catch (err) {
|
||||||
},
|
if (abortController.signal.aborted) {
|
||||||
},
|
return;
|
||||||
});
|
}
|
||||||
|
// TODO: show other errors
|
||||||
|
}
|
||||||
set("install", (s) => ({
|
set("install", (s) => ({
|
||||||
...s,
|
...s,
|
||||||
prepareStep: "install",
|
prepareStep: "install",
|
||||||
progress: runInstall,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await runInstall.result;
|
|
||||||
|
|
||||||
stepSignal.setActiveStep("install:done");
|
stepSignal.setActiveStep("install:done");
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -818,10 +815,7 @@ const InstallProgress = () => {
|
|||||||
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
const [store, get] = getStepStore<InstallStoreType>(stepSignal);
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
const progress = store.install.progress;
|
store.install.progress.abort();
|
||||||
if (progress) {
|
|
||||||
await progress.cancel();
|
|
||||||
}
|
|
||||||
stepSignal.previous();
|
stepSignal.previous();
|
||||||
};
|
};
|
||||||
const installState = useNotifyOrigin<ProcessMessage<unknown, InstallTopic>>(
|
const installState = useNotifyOrigin<ProcessMessage<unknown, InstallTopic>>(
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@api/clan/client": ["./src/api/clan/client-call"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function regenPythonApiOnFileChange() {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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: "./",
|
base: "./",
|
||||||
|
|||||||
Reference in New Issue
Block a user