Merge pull request 'Clan-app: refine create machine workflow via query operation' (#1853) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-08-06 14:11:58 +00:00
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():
raise ClanError(f"Machine with the name {machine.name} already exists")
print(f"Define machine {machine.name}", machine)
inventory.machines.update({machine.name: machine})
save_inventory(inventory, flake.path, f"Create machine {machine.name}")

View File

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

View File

@@ -1,36 +1,69 @@
import { callApi, OperationArgs, pyApi } from "@/src/api";
import { activeURI } from "@/src/App";
import { createForm, required } from "@modular-forms/solid";
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
import { activeURI, setRoute } from "@/src/App";
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";
type CreateMachineForm = OperationArgs<"create_machine">;
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 active_dir = activeURI();
if (!active_dir) {
toast.error("Open a clan to create the machine in");
toast.error("Open a clan to create the machine within");
return;
}
callApi("create_machine", {
console.log("submitting", values);
const response = await callApi("create_machine", {
...values,
flake: {
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 (
<div class="px-1">
Create new Machine
<button
onClick={() => {
reset(formStore);
}}
>
reset
</button>
<Form onSubmit={handleSubmit}>
<Field
name="machine.name"
@@ -38,13 +71,20 @@ export function CreateMachine() {
>
{(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
{...props}
value={field.value}
type="text"
class="grow"
placeholder="name"
required
{...props}
disabled={formStore.submitting}
/>
</label>
<div class="label">
@@ -60,8 +100,14 @@ export function CreateMachine() {
<Field name="machine.description">
{(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
value={String(field.value)}
type="text"
class="grow"
placeholder="description"
@@ -82,8 +128,14 @@ export function CreateMachine() {
<Field name="machine.deploy.targetHost">
{(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
value={String(field.value)}
type="text"
class="grow"
placeholder="root@flash-installer.local"
@@ -115,8 +167,24 @@ export function CreateMachine() {
</>
)}
</Field>
<button class="btn btn-error float-right" type="submit">
<span class="material-icons">add</span>Create
<button
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>
</Form>
</div>

View File

@@ -1,49 +1,20 @@
import {
type Component,
createEffect,
createSignal,
For,
Match,
Show,
Switch,
} from "solid-js";
import { activeURI, route, setActiveURI, setRoute } from "@/src/App";
import { callApi, OperationResponse, pyApi } from "@/src/api";
import { type Component, createEffect, For, Match, Switch } from "solid-js";
import { activeURI, setRoute } from "@/src/App";
import { callApi, OperationResponse } from "@/src/api";
import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem";
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<
OperationResponse<"list_inventory_machines">,
{ status: "success" }
>["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 = () => {
const {
data: nixosMachines,
isFetching,
isLoading,
isFetching: isLoadingNixos,
refetch: refetchNixos,
} = createQuery<string[]>(() => ({
queryKey: [activeURI(), "list_nixos_machines"],
queryFn: async () => {
@@ -62,30 +33,36 @@ export const MachineListView: Component = () => {
},
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 [loading, setLoading] = createSignal<boolean>(false);
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);
}
const refresh = async () => {
refetchInventory();
refetchNixos();
};
createEffect(() => {
if (route() === "machines") listMachines();
});
const unpackedMachines = () => Object.entries(machines());
const unpackedMachines = () => Object.entries(inventoryMachines);
const nixOnlyMachines = () =>
nixosMachines?.filter(
(name) => !unpackedMachines().some(([key, machine]) => key === name),
@@ -99,7 +76,7 @@ export const MachineListView: Component = () => {
<div class="max-w-screen-lg">
<div class="tooltip tooltip-bottom" data-tip="Open Clan"></div>
<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>
</button>
</div>
@@ -108,55 +85,8 @@ export const MachineListView: Component = () => {
<span class="material-icons ">add</span>
</button>
</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>
<Match when={loading()}>
<Match when={isLoadingInventory}>
{/* Loading skeleton */}
<div>
<div class="card card-side m-2 bg-base-100 shadow-lg">
@@ -174,14 +104,14 @@ export const MachineListView: Component = () => {
</Match>
<Match
when={
!loading() &&
!isLoadingInventory &&
unpackedMachines().length === 0 &&
nixOnlyMachines()?.length === 0
}
>
No machines found
</Match>
<Match when={!loading()}>
<Match when={!isLoadingInventory}>
<ul>
<For each={unpackedMachines()}>
{([name, info]) => <MachineListItem name={name} info={info} />}