Clan-app: refine create machine workflow via query operation
This commit is contained in:
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user