Clan-app: Add loading animations & improve async data handling
This commit is contained in:
@@ -13,15 +13,13 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_inventory_machines(
|
def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
||||||
flake_url: str | Path, debug: bool = False
|
|
||||||
) -> dict[str, Machine]:
|
|
||||||
inventory = load_inventory_eval(flake_url)
|
inventory = load_inventory_eval(flake_url)
|
||||||
return inventory.machines
|
return inventory.machines
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_nixos_machines(flake_url: str | Path, debug: bool = False) -> list[str]:
|
def list_nixos_machines(flake_url: str | Path) -> list[str]:
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
[
|
[
|
||||||
f"{flake_url}#nixosConfigurations",
|
f"{flake_url}#nixosConfigurations",
|
||||||
@@ -42,7 +40,7 @@ def list_nixos_machines(flake_url: str | Path, debug: bool = False) -> list[str]
|
|||||||
|
|
||||||
def list_command(args: argparse.Namespace) -> None:
|
def list_command(args: argparse.Namespace) -> None:
|
||||||
flake_path = args.flake.path
|
flake_path = args.flake.path
|
||||||
for name in list_nixos_machines(flake_path, args.debug):
|
for name in list_nixos_machines(flake_path):
|
||||||
print(name)
|
print(name)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import { makePersisted } from "@solid-primitives/storage";
|
|||||||
|
|
||||||
// Some global state
|
// Some global state
|
||||||
const [route, setRoute] = createSignal<Route>("machines");
|
const [route, setRoute] = createSignal<Route>("machines");
|
||||||
createEffect(() => {
|
|
||||||
console.log(route());
|
|
||||||
});
|
|
||||||
|
|
||||||
export { route, setRoute };
|
export { route, setRoute };
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export const routes = {
|
|||||||
label: "diskConfig",
|
label: "diskConfig",
|
||||||
icon: "disk",
|
icon: "disk",
|
||||||
},
|
},
|
||||||
|
"machines/edit": {
|
||||||
|
child: CreateMachine,
|
||||||
|
label: "Edit Machine",
|
||||||
|
icon: "edit",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RouterProps {
|
interface RouterProps {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export const callApi = <K extends OperationNames>(
|
|||||||
return new Promise<OperationResponse<K>>((resolve) => {
|
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(method, "Received response: ", { response });
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}, id);
|
}, id);
|
||||||
|
|
||||||
@@ -136,7 +136,6 @@ const deserialize =
|
|||||||
(str: string) => {
|
(str: string) => {
|
||||||
try {
|
try {
|
||||||
const r = JSON.parse(str) as T;
|
const r = JSON.parse(str) as T;
|
||||||
console.log("Received: ", r);
|
|
||||||
fn(r);
|
fn(r);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error parsing JSON: ", e);
|
console.log("Error parsing JSON: ", e);
|
||||||
|
|||||||
@@ -1,86 +1,67 @@
|
|||||||
import { createSignal, Match, Show, Switch } from "solid-js";
|
import { Accessor, createEffect, Show } from "solid-js";
|
||||||
import { ErrorData, pyApi, SuccessData } from "../api";
|
import { SuccessData } from "../api";
|
||||||
|
import { Menu } from "./Menu";
|
||||||
|
import { setRoute } from "../App";
|
||||||
|
|
||||||
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
|
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
|
||||||
|
|
||||||
interface MachineListItemProps {
|
interface MachineListItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
info?: MachineDetails;
|
info?: MachineDetails;
|
||||||
|
nixOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HWInfo = Record<string, SuccessData<"show_machine_hardware_info">["data"]>;
|
|
||||||
type DeploymentInfo = Record<
|
|
||||||
string,
|
|
||||||
SuccessData<"show_machine_deployment_target">["data"]
|
|
||||||
>;
|
|
||||||
|
|
||||||
type MachineErrors = Record<string, ErrorData<"show_machine">["errors"]>;
|
|
||||||
|
|
||||||
const [hwInfo, setHwInfo] = createSignal<HWInfo>({});
|
|
||||||
|
|
||||||
const [deploymentInfo, setDeploymentInfo] = createSignal<DeploymentInfo>({});
|
|
||||||
|
|
||||||
const [errors, setErrors] = createSignal<MachineErrors>({});
|
|
||||||
|
|
||||||
// pyApi.show_machine_hardware_info.receive((r) => {
|
|
||||||
// const { op_key } = r;
|
|
||||||
// if (r.status === "error") {
|
|
||||||
// console.error(r.errors);
|
|
||||||
// if (op_key) {
|
|
||||||
// setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (op_key) {
|
|
||||||
// setHwInfo((d) => ({ ...d, [op_key]: r.data }));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// pyApi.show_machine_deployment_target.receive((r) => {
|
|
||||||
// const { op_key } = r;
|
|
||||||
// if (r.status === "error") {
|
|
||||||
// console.error(r.errors);
|
|
||||||
// if (op_key) {
|
|
||||||
// setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (op_key) {
|
|
||||||
// setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
export const MachineListItem = (props: MachineListItemProps) => {
|
export const MachineListItem = (props: MachineListItemProps) => {
|
||||||
const { name, info } = props;
|
const { name, info, nixOnly } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div class="card card-side m-2 bg-base-100 shadow-lg">
|
<div class="card card-side m-2 bg-base-200">
|
||||||
<figure class="pl-2">
|
<figure class="pl-2">
|
||||||
<span class="material-icons content-center text-5xl">
|
<span
|
||||||
|
class="material-icons content-center text-5xl"
|
||||||
|
classList={{
|
||||||
|
"text-neutral-500": nixOnly,
|
||||||
|
}}
|
||||||
|
>
|
||||||
devices_other
|
devices_other
|
||||||
</span>
|
</span>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body flex-row justify-between ">
|
<div class="card-body flex-row justify-between ">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h2 class="card-title">{name}</h2>
|
<h2
|
||||||
|
class="card-title"
|
||||||
|
classList={{
|
||||||
|
"text-neutral-500": nixOnly,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</h2>
|
||||||
<div class="text-slate-600">
|
<div class="text-slate-600">
|
||||||
<Show when={info}>{(d) => d()?.description}</Show>
|
<Show when={info}>{(d) => d()?.description}</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-wrap gap-4 py-2"></div>
|
<div class="flex flex-row flex-wrap gap-4 py-2"></div>
|
||||||
{/* Show only the first error at the bottom */}
|
|
||||||
<Show when={errors()[name]?.[0]}>
|
|
||||||
{(error) => (
|
|
||||||
<div class="badge badge-error py-4">
|
|
||||||
Error: {error().message}: {error().description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-ghost">
|
<Menu
|
||||||
<span class="material-icons">more_vert</span>
|
popoverid={`menu-${props.name}`}
|
||||||
</button>
|
label={<span class="material-icons">more_vert</span>}
|
||||||
|
>
|
||||||
|
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
setRoute("machines/edit");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>Deploy</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
pkgs/webview-ui/app/src/components/Menu.tsx
Normal file
84
pkgs/webview-ui/app/src/components/Menu.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { children, Component, createSignal, type JSX } from "solid-js";
|
||||||
|
import { useFloating } from "@/src/floating";
|
||||||
|
import {
|
||||||
|
autoUpdate,
|
||||||
|
flip,
|
||||||
|
hide,
|
||||||
|
offset,
|
||||||
|
Placement,
|
||||||
|
shift,
|
||||||
|
} from "@floating-ui/dom";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
/**
|
||||||
|
* Used by the html API to associate the popover with the dispatcher button
|
||||||
|
*/
|
||||||
|
popoverid: string;
|
||||||
|
|
||||||
|
label: JSX.Element;
|
||||||
|
|
||||||
|
children?: JSX.Element;
|
||||||
|
buttonProps?: JSX.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
buttonClass?: string;
|
||||||
|
/**
|
||||||
|
* @default "bottom"
|
||||||
|
*/
|
||||||
|
placement?: Placement;
|
||||||
|
}
|
||||||
|
export const Menu = (props: MenuProps) => {
|
||||||
|
const c = children(() => props.children);
|
||||||
|
const [reference, setReference] = createSignal<HTMLElement>();
|
||||||
|
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
|
// `position` is a reactive object.
|
||||||
|
const position = useFloating(reference, floating, {
|
||||||
|
placement: "bottom",
|
||||||
|
|
||||||
|
// pass options. Ensure the cleanup function is returned.
|
||||||
|
whileElementsMounted: (reference, floating, update) =>
|
||||||
|
autoUpdate(reference, floating, update, {
|
||||||
|
animationFrame: true,
|
||||||
|
}),
|
||||||
|
middleware: [
|
||||||
|
offset(5),
|
||||||
|
shift(),
|
||||||
|
flip(),
|
||||||
|
|
||||||
|
hide({
|
||||||
|
strategy: "referenceHidden",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
popovertarget={props.popoverid}
|
||||||
|
popovertargetaction="toggle"
|
||||||
|
ref={setReference}
|
||||||
|
class={cx(
|
||||||
|
"btn btn-ghost btn-outline join-item btn-sm",
|
||||||
|
props.buttonClass,
|
||||||
|
)}
|
||||||
|
{...props.buttonProps}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
popover="auto"
|
||||||
|
id={props.popoverid}
|
||||||
|
ref={setFloating}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
position: position.strategy,
|
||||||
|
top: `${position.y ?? 0}px`,
|
||||||
|
left: `${position.x ?? 0}px`,
|
||||||
|
}}
|
||||||
|
class="bg-transparent"
|
||||||
|
>
|
||||||
|
{c()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { activeURI, setRoute } from "../App";
|
import { activeURI, setRoute } from "../App";
|
||||||
import { callApi } from "../api";
|
import { callApi } from "../api";
|
||||||
import { Accessor, createEffect, Show } from "solid-js";
|
import { Accessor, Show } from "solid-js";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
clan_dir: Accessor<string | null>;
|
clan_dir: Accessor<string | null>;
|
||||||
@@ -34,7 +34,14 @@ export const Header = (props: HeaderProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Show when={!query.isFetching && query.data}>
|
<Show when={query.isLoading && !query.data}>
|
||||||
|
<div class="skeleton mx-4 size-11 rounded-full"></div>
|
||||||
|
<span class="flex flex-col gap-2">
|
||||||
|
<div class="skeleton h-3 w-32"></div>
|
||||||
|
<div class="skeleton h-3 w-40"></div>
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={query.data}>
|
||||||
{(meta) => (
|
{(meta) => (
|
||||||
<div class="tooltip tooltip-right" data-tip={activeURI()}>
|
<div class="tooltip tooltip-right" data-tip={activeURI()}>
|
||||||
<div class="avatar placeholder online mx-4">
|
<div class="avatar placeholder online mx-4">
|
||||||
@@ -46,7 +53,7 @@ export const Header = (props: HeaderProps) => {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<Show when={!query.isFetching && query.data}>
|
<Show when={query.data}>
|
||||||
{(meta) => [
|
{(meta) => [
|
||||||
<span class="text-primary">{meta().name}</span>,
|
<span class="text-primary">{meta().name}</span>,
|
||||||
<span class="text-neutral">{meta()?.description}</span>,
|
<span class="text-neutral">{meta()?.description}</span>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
|
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
|
||||||
import { activeURI, setRoute } from "@/src/App";
|
import { activeURI, setRoute } from "@/src/App";
|
||||||
import { createForm, required, reset } from "@modular-forms/solid";
|
import { createForm, required, reset } from "@modular-forms/solid";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
import { Match, Switch } from "solid-js";
|
import { Match, Switch } from "solid-js";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
|
|
||||||
@@ -23,9 +23,7 @@ export function CreateMachine() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { refetch: refetchMachines } = createQuery(() => ({
|
const queryClient = useQueryClient();
|
||||||
queryKey: [activeURI(), "list_inventory_machines"],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleSubmit = async (values: CreateMachineForm) => {
|
const handleSubmit = async (values: CreateMachineForm) => {
|
||||||
const active_dir = activeURI();
|
const active_dir = activeURI();
|
||||||
@@ -46,7 +44,10 @@ export function CreateMachine() {
|
|||||||
if (response.status === "success") {
|
if (response.status === "success") {
|
||||||
toast.success(`Successfully created ${values.machine.name}`);
|
toast.success(`Successfully created ${values.machine.name}`);
|
||||||
reset(formStore);
|
reset(formStore);
|
||||||
refetchMachines();
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [activeURI(), "list_machines"],
|
||||||
|
});
|
||||||
setRoute("machines");
|
setRoute("machines");
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -3,43 +3,28 @@ import { activeURI, setRoute } from "@/src/App";
|
|||||||
import { callApi, OperationResponse } from "@/src/api";
|
import { callApi, OperationResponse } 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 {
|
||||||
|
createQueries,
|
||||||
|
createQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/solid-query";
|
||||||
|
|
||||||
type MachinesModel = Extract<
|
type MachinesModel = Extract<
|
||||||
OperationResponse<"list_inventory_machines">,
|
OperationResponse<"list_inventory_machines">,
|
||||||
{ status: "success" }
|
{ status: "success" }
|
||||||
>["data"];
|
>["data"];
|
||||||
|
|
||||||
|
type ExtendedMachine = MachinesModel & {
|
||||||
|
nixOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const MachineListView: Component = () => {
|
export const MachineListView: Component = () => {
|
||||||
const {
|
const queryClient = useQueryClient();
|
||||||
data: nixosMachines,
|
|
||||||
isFetching: isLoadingNixos,
|
const inventoryQuery = createQuery<MachinesModel>(() => ({
|
||||||
refetch: refetchNixos,
|
queryKey: [activeURI(), "list_machines", "inventory"],
|
||||||
} = createQuery<string[]>(() => ({
|
placeholderData: {},
|
||||||
queryKey: [activeURI(), "list_nixos_machines"],
|
enabled: !!activeURI(),
|
||||||
queryFn: async () => {
|
|
||||||
const uri = activeURI();
|
|
||||||
if (uri) {
|
|
||||||
const response = await callApi("list_nixos_machines", {
|
|
||||||
flake_url: uri,
|
|
||||||
});
|
|
||||||
if (response.status === "error") {
|
|
||||||
toast.error("Failed to fetch data");
|
|
||||||
} else {
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
}));
|
|
||||||
const {
|
|
||||||
data: inventoryMachines,
|
|
||||||
isFetching: isLoadingInventory,
|
|
||||||
refetch: refetchInventory,
|
|
||||||
} = createQuery<MachinesModel>(() => ({
|
|
||||||
queryKey: [activeURI(), "list_inventory_machines"],
|
|
||||||
initialData: {},
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const uri = activeURI();
|
const uri = activeURI();
|
||||||
if (uri) {
|
if (uri) {
|
||||||
@@ -54,26 +39,43 @@ export const MachineListView: Component = () => {
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 5,
|
}));
|
||||||
|
|
||||||
|
const nixosQuery = createQuery<string[]>(() => ({
|
||||||
|
queryKey: [activeURI(), "list_machines", "nixos"],
|
||||||
|
enabled: !!activeURI(),
|
||||||
|
placeholderData: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
const uri = activeURI();
|
||||||
|
if (uri) {
|
||||||
|
const response = await callApi("list_nixos_machines", {
|
||||||
|
flake_url: uri,
|
||||||
|
});
|
||||||
|
if (response.status === "error") {
|
||||||
|
toast.error("Failed to fetch data");
|
||||||
|
} else {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
refetchInventory();
|
queryClient.invalidateQueries({
|
||||||
refetchNixos();
|
// Invalidates the cache for of all types of machine list at once
|
||||||
|
queryKey: [activeURI(), "list_machines"],
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const unpackedMachines = () => Object.entries(inventoryMachines);
|
const inventoryMachines = () => Object.entries(inventoryQuery.data || {});
|
||||||
const nixOnlyMachines = () =>
|
const nixOnlyMachines = () =>
|
||||||
nixosMachines?.filter(
|
nixosQuery.data?.filter(
|
||||||
(name) => !unpackedMachines().some(([key, machine]) => key === name),
|
(name) => !inventoryMachines().some(([key, machine]) => key === name),
|
||||||
);
|
);
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
console.log(nixOnlyMachines(), unpackedMachines());
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="max-w-screen-lg">
|
<div>
|
||||||
<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={() => refresh()}>
|
<button class="btn btn-ghost" onClick={() => refresh()}>
|
||||||
@@ -86,7 +88,7 @@ export const MachineListView: Component = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isLoadingInventory}>
|
<Match when={inventoryQuery.isLoading}>
|
||||||
{/* 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">
|
||||||
@@ -104,20 +106,20 @@ export const MachineListView: Component = () => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
when={
|
when={
|
||||||
!isLoadingInventory &&
|
!inventoryQuery.isLoading &&
|
||||||
unpackedMachines().length === 0 &&
|
inventoryMachines().length === 0 &&
|
||||||
nixOnlyMachines()?.length === 0
|
nixOnlyMachines()?.length === 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
No machines found
|
No machines found
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!isLoadingInventory}>
|
<Match when={!inventoryQuery.isLoading}>
|
||||||
<ul>
|
<ul>
|
||||||
<For each={unpackedMachines()}>
|
<For each={inventoryMachines()}>
|
||||||
{([name, info]) => <MachineListItem name={name} info={info} />}
|
{([name, info]) => <MachineListItem name={name} info={info} />}
|
||||||
</For>
|
</For>
|
||||||
<For each={nixOnlyMachines()}>
|
<For each={nixOnlyMachines()}>
|
||||||
{(name) => <MachineListItem name={name} />}
|
{(name) => <MachineListItem name={name} nixOnly={true} />}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { callApi } from "@/src/api";
|
import { callApi } from "@/src/api";
|
||||||
import {
|
|
||||||
createForm,
|
|
||||||
required,
|
|
||||||
setValue,
|
|
||||||
SubmitHandler,
|
|
||||||
} from "@modular-forms/solid";
|
|
||||||
import {
|
import {
|
||||||
activeURI,
|
activeURI,
|
||||||
clanList,
|
clanList,
|
||||||
@@ -12,15 +6,7 @@ import {
|
|||||||
setClanList,
|
setClanList,
|
||||||
setRoute,
|
setRoute,
|
||||||
} from "@/src/App";
|
} from "@/src/App";
|
||||||
import {
|
import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
Match,
|
|
||||||
Setter,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js";
|
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { useFloating } from "@/src/floating";
|
import { useFloating } from "@/src/floating";
|
||||||
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||||
@@ -156,6 +142,9 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-title">{clan_dir}</div>
|
<div class="stat-title">{clan_dir}</div>
|
||||||
|
|
||||||
|
<Show when={details.isLoading}>
|
||||||
|
<div class="skeleton h-12 w-80" />
|
||||||
|
</Show>
|
||||||
<Show when={details.isSuccess}>
|
<Show when={details.isSuccess}>
|
||||||
<div class="stat-value">{details.data?.name}</div>
|
<div class="stat-value">{details.data?.name}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user