Clan-app: Add loading animations & improve async data handling

This commit is contained in:
Johannes Kirschbauer
2024-08-06 22:29:11 +02:00
parent ec16059abc
commit e69d6b22f0
10 changed files with 203 additions and 140 deletions

View File

@@ -1,86 +1,67 @@
import { createSignal, Match, Show, Switch } from "solid-js";
import { ErrorData, pyApi, SuccessData } from "../api";
import { Accessor, createEffect, Show } from "solid-js";
import { SuccessData } from "../api";
import { Menu } from "./Menu";
import { setRoute } from "../App";
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
interface MachineListItemProps {
name: string;
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) => {
const { name, info } = props;
const { name, info, nixOnly } = props;
return (
<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">
<span class="material-icons content-center text-5xl">
<span
class="material-icons content-center text-5xl"
classList={{
"text-neutral-500": nixOnly,
}}
>
devices_other
</span>
</figure>
<div class="card-body flex-row justify-between">
<div class="card-body flex-row justify-between ">
<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">
<Show when={info}>{(d) => d()?.description}</Show>
</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>
<button class="btn btn-ghost">
<span class="material-icons">more_vert</span>
</button>
<Menu
popoverid={`menu-${props.name}`}
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>

View 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>
);
};