Clan-app: refine create machine workflow via query operation

This commit is contained in:
Johannes Kirschbauer
2024-08-06 14:22:24 +02:00
parent 52e2ba9801
commit 6158e82f43
4 changed files with 126 additions and 129 deletions

View File

@@ -31,6 +31,8 @@ def create_machine(flake: FlakeId, machine: Machine) -> None:
if machine.name in full_inventory.machines.keys(): if machine.name in full_inventory.machines.keys():
raise ClanError(f"Machine with the name {machine.name} already exists") raise ClanError(f"Machine with the name {machine.name} already exists")
print(f"Define machine {machine.name}", machine)
inventory.machines.update({machine.name: machine}) inventory.machines.update({machine.name: machine})
save_inventory(inventory, flake.path, f"Create machine {machine.name}") save_inventory(inventory, flake.path, f"Create machine {machine.name}")

View File

@@ -120,13 +120,10 @@ export const callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
) => { ) => {
return new Promise<OperationResponse<K>>((resolve, reject) => { return new Promise<OperationResponse<K>>((resolve) => {
const id = nanoid(); const id = nanoid();
pyApi[method].receive((response) => { pyApi[method].receive((response) => {
console.log("Received response: ", { response }); console.log("Received response: ", { response });
if (response.status === "error") {
reject(response);
}
resolve(response); resolve(response);
}, id); }, id);

View File

@@ -1,36 +1,69 @@
import { callApi, OperationArgs, pyApi } from "@/src/api"; import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
import { activeURI } from "@/src/App"; import { activeURI, setRoute } from "@/src/App";
import { createForm, required } from "@modular-forms/solid"; import { createForm, required, reset } from "@modular-forms/solid";
import { createQuery } from "@tanstack/solid-query";
import { Match, Switch } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
type CreateMachineForm = OperationArgs<"create_machine">; type CreateMachineForm = OperationArgs<"create_machine">;
export function CreateMachine() { export function CreateMachine() {
const [formStore, { Form, Field }] = createForm<CreateMachineForm>({}); const [formStore, { Form, Field }] = createForm<CreateMachineForm>({
initialValues: {
flake: {
loc: activeURI() || "",
},
machine: {
deploy: {
targetHost: "",
},
name: "",
description: "",
},
},
});
const { refetch: refetchMachines } = createQuery(() => ({
queryKey: [activeURI(), "list_inventory_machines"],
}));
const handleSubmit = async (values: CreateMachineForm) => { const handleSubmit = async (values: CreateMachineForm) => {
const active_dir = activeURI(); const active_dir = activeURI();
if (!active_dir) { if (!active_dir) {
toast.error("Open a clan to create the machine in"); toast.error("Open a clan to create the machine within");
return; return;
} }
callApi("create_machine", { console.log("submitting", values);
const response = await callApi("create_machine", {
...values,
flake: { flake: {
loc: active_dir, loc: active_dir,
}, },
machine: {
name: "jon",
deploy: {
targetHost: null,
},
},
}); });
console.log("submit", values);
if (response.status === "success") {
toast.success(`Successfully created ${values.machine.name}`);
reset(formStore);
refetchMachines();
setRoute("machines");
} else {
toast.error(
`Error: ${response.errors[0].message}. Machine ${values.machine.name} could not be created`,
);
}
}; };
return ( return (
<div class="px-1"> <div class="px-1">
Create new Machine Create new Machine
<button
onClick={() => {
reset(formStore);
}}
>
reset
</button>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Field <Field
name="machine.name" name="machine.name"
@@ -38,13 +71,20 @@ export function CreateMachine() {
> >
{(field, props) => ( {(field, props) => (
<> <>
<label class="input input-bordered flex items-center gap-2"> <label
class="input input-bordered flex items-center gap-2"
classList={{
"input-disabled": formStore.submitting,
}}
>
<input <input
{...props}
value={field.value}
type="text" type="text"
class="grow" class="grow"
placeholder="name" placeholder="name"
required required
{...props} disabled={formStore.submitting}
/> />
</label> </label>
<div class="label"> <div class="label">
@@ -60,8 +100,14 @@ export function CreateMachine() {
<Field name="machine.description"> <Field name="machine.description">
{(field, props) => ( {(field, props) => (
<> <>
<label class="input input-bordered flex items-center gap-2"> <label
class="input input-bordered flex items-center gap-2"
classList={{
"input-disabled": formStore.submitting,
}}
>
<input <input
value={String(field.value)}
type="text" type="text"
class="grow" class="grow"
placeholder="description" placeholder="description"
@@ -82,8 +128,14 @@ export function CreateMachine() {
<Field name="machine.deploy.targetHost"> <Field name="machine.deploy.targetHost">
{(field, props) => ( {(field, props) => (
<> <>
<label class="input input-bordered flex items-center gap-2"> <label
class="input input-bordered flex items-center gap-2"
classList={{
"input-disabled": formStore.submitting,
}}
>
<input <input
value={String(field.value)}
type="text" type="text"
class="grow" class="grow"
placeholder="root@flash-installer.local" placeholder="root@flash-installer.local"
@@ -115,8 +167,24 @@ export function CreateMachine() {
</> </>
)} )}
</Field> </Field>
<button class="btn btn-error float-right" type="submit"> <button
<span class="material-icons">add</span>Create class="btn btn-error float-right"
type="submit"
classList={{
"btn-disabled": formStore.submitting,
}}
>
<Switch
fallback={
<>
<span class="loading loading-spinner loading-sm"></span>Creating
</>
}
>
<Match when={!formStore.submitting}>
<span class="material-icons">add</span>Create
</Match>
</Switch>
</button> </button>
</Form> </Form>
</div> </div>

View File

@@ -1,49 +1,20 @@
import { import { type Component, createEffect, For, Match, Switch } from "solid-js";
type Component, import { activeURI, setRoute } from "@/src/App";
createEffect, import { callApi, OperationResponse } from "@/src/api";
createSignal,
For,
Match,
Show,
Switch,
} from "solid-js";
import { activeURI, route, setActiveURI, setRoute } from "@/src/App";
import { callApi, OperationResponse, pyApi } from "@/src/api";
import toast from "solid-toast"; import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem"; import { MachineListItem } from "@/src/components/MachineListItem";
import { createQuery } from "@tanstack/solid-query"; import { createQuery } from "@tanstack/solid-query";
// type FilesModel = Extract<
// OperationResponse<"get_directory">,
// { status: "success" }
// >["data"]["files"];
// type ServiceModel = Extract<
// OperationResponse<"show_mdns">,
// { status: "success" }
// >["data"]["services"];
type MachinesModel = Extract< type MachinesModel = Extract<
OperationResponse<"list_inventory_machines">, OperationResponse<"list_inventory_machines">,
{ status: "success" } { status: "success" }
>["data"]; >["data"];
// pyApi.open_file.receive((r) => {
// if (r.op_key === "open_clan") {
// console.log(r);
// if (r.status === "error") return console.error(r.errors);
// if (r.data) {
// setCurrClanURI(r.data);
// }
// }
// });
export const MachineListView: Component = () => { export const MachineListView: Component = () => {
const { const {
data: nixosMachines, data: nixosMachines,
isFetching, isFetching: isLoadingNixos,
isLoading, refetch: refetchNixos,
} = createQuery<string[]>(() => ({ } = createQuery<string[]>(() => ({
queryKey: [activeURI(), "list_nixos_machines"], queryKey: [activeURI(), "list_nixos_machines"],
queryFn: async () => { queryFn: async () => {
@@ -62,30 +33,36 @@ export const MachineListView: Component = () => {
}, },
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
})); }));
const {
data: inventoryMachines,
isFetching: isLoadingInventory,
refetch: refetchInventory,
} = createQuery<MachinesModel>(() => ({
queryKey: [activeURI(), "list_inventory_machines"],
initialData: {},
queryFn: async () => {
const uri = activeURI();
if (uri) {
const response = await callApi("list_inventory_machines", {
flake_url: uri,
});
if (response.status === "error") {
toast.error("Failed to fetch data");
} else {
return response.data;
}
}
return {};
},
staleTime: 1000 * 60 * 5,
}));
const [machines, setMachines] = createSignal<MachinesModel>({}); const refresh = async () => {
const [loading, setLoading] = createSignal<boolean>(false); refetchInventory();
refetchNixos();
const listMachines = async () => {
const uri = activeURI();
if (!uri) {
return;
}
setLoading(true);
const response = await callApi("list_inventory_machines", {
flake_url: uri,
});
setLoading(false);
if (response.status === "success") {
setMachines(response.data);
}
}; };
createEffect(() => { const unpackedMachines = () => Object.entries(inventoryMachines);
if (route() === "machines") listMachines();
});
const unpackedMachines = () => Object.entries(machines());
const nixOnlyMachines = () => const nixOnlyMachines = () =>
nixosMachines?.filter( nixosMachines?.filter(
(name) => !unpackedMachines().some(([key, machine]) => key === name), (name) => !unpackedMachines().some(([key, machine]) => key === name),
@@ -99,7 +76,7 @@ export const MachineListView: Component = () => {
<div class="max-w-screen-lg"> <div class="max-w-screen-lg">
<div class="tooltip tooltip-bottom" data-tip="Open Clan"></div> <div class="tooltip tooltip-bottom" data-tip="Open Clan"></div>
<div class="tooltip tooltip-bottom" data-tip="Refresh"> <div class="tooltip tooltip-bottom" data-tip="Refresh">
<button class="btn btn-ghost" onClick={() => listMachines()}> <button class="btn btn-ghost" onClick={() => refresh()}>
<span class="material-icons ">refresh</span> <span class="material-icons ">refresh</span>
</button> </button>
</div> </div>
@@ -108,55 +85,8 @@ export const MachineListView: Component = () => {
<span class="material-icons ">add</span> <span class="material-icons ">add</span>
</button> </button>
</div> </div>
{/* <Show when={services()}>
{(services) => (
<For each={Object.values(services())}>
{(service) => (
<div class="rounded-lg bg-white p-5 shadow-lg">
<h2 class="mb-2 text-xl font-semibold">{service.name}</h2>
<p>
<span class="font-bold">Interface:</span>
{service.interface}
</p>
<p>
<span class="font-bold">Protocol:</span>
{service.protocol}
</p>
<p>
<span class="font-bold">Name</span>
{service.name}
</p>
<p>
<span class="font-bold">Type:</span>
{service.type_}
</p>
<p>
<span class="font-bold">Domain:</span>
{service.domain}
</p>
<p>
<span class="font-bold">Host:</span>
{service.host}
</p>
<p>
<span class="font-bold">IP:</span>
{service.ip}
</p>
<p>
<span class="font-bold">Port:</span>
{service.port}
</p>
<p>
<span class="font-bold">TXT:</span>
{service.txt}
</p>
</div>
)}
</For>
)}
</Show> */}
<Switch> <Switch>
<Match when={loading()}> <Match when={isLoadingInventory}>
{/* Loading skeleton */} {/* Loading skeleton */}
<div> <div>
<div class="card card-side m-2 bg-base-100 shadow-lg"> <div class="card card-side m-2 bg-base-100 shadow-lg">
@@ -174,14 +104,14 @@ export const MachineListView: Component = () => {
</Match> </Match>
<Match <Match
when={ when={
!loading() && !isLoadingInventory &&
unpackedMachines().length === 0 && unpackedMachines().length === 0 &&
nixOnlyMachines()?.length === 0 nixOnlyMachines()?.length === 0
} }
> >
No machines found No machines found
</Match> </Match>
<Match when={!loading()}> <Match when={!isLoadingInventory}>
<ul> <ul>
<For each={unpackedMachines()}> <For each={unpackedMachines()}>
{([name, info]) => <MachineListItem name={name} info={info} />} {([name, info]) => <MachineListItem name={name} info={info} />}