1import { type Accessor, createMemo, For, type JSX, type Resource, Show } from "solid-js";
2import Unplug from "lucide-solid/icons/unplug";
3import { reason, unconnected } from "../api.ts";
4
5/** The latest value that loaded, kept through later failures; reading an errored resource directly throws. */
6export function lastGood<T>(resource: Resource<T>): Accessor<T | undefined> {
7 return createMemo<T | undefined>((previous) => (resource.state === "errored" ? previous : resource.latest));
8}
9
10/** Placeholder rows of staggered widths, for a list or table. */
11export function SkeletonRows(props: { count: number }) {
12 return <For each={Array(props.count)}>{(_, i) => <div class="skeleton row-skeleton" style={{ width: `${35 + ((i() * 37) % 50)}%` }} />}</For>;
13}
14
15/**
16 * A skeleton, then `children` with the latest value, which stays up through a refetch and, with a note and a retry
17 * above it, through a failed one. Only a failure before anything loaded replaces the content with the error, and a
18 * source that isn't connected yet replaces it with the server's note instead, since retrying can't help.
19 */
20export function Loaded<T>(props: {
21 data: Resource<T>;
22 /** What failed to load, e.g. "pool status", read as "Couldn't load pool status." */
23 what: string;
24 retry: () => void;
25 /** Placeholder shaped like the content, built from `.skeleton` blocks; defaults to one block. */
26 skeleton?: JSX.Element;
27 children: (value: Accessor<NonNullable<T>>) => JSX.Element;
28}) {
29 const value = lastGood(props.data);
30 /** The last failure, kept through a refetch until one succeeds, so polling doesn't flash the skeleton. */
31 const error = createMemo<unknown>((previous) =>
32 props.data.state === "errored" ? props.data.error : props.data.state === "ready" ? undefined : previous);
33 const failure = () => error() !== undefined && reason(error());
34 return (
35 <Show when={value()} fallback={
36 <Show when={failure()} fallback={
37 <>
38 <span class="sr-only">Loading {props.what}…</span>
39 {props.skeleton ?? <div class="skeleton" style={{ height: "120px" }} />}
40 </>
41 }>
42 {(text) => unconnected(error()) ? (
43 <div class="empty unconnected" role="status">
44 <Unplug aria-hidden="true" />
45 <p>{text()}</p>
46 </div>
47 ) : (
48 <div class="empty failed" role="alert">
49 <p><span class="error">Couldn't load {props.what}.</span> {text()}</p>
50 <button class="button" onClick={props.retry}>retry</button>
51 </div>
52 )}
53 </Show>
54 }>
55 {(latest) => (
56 <>
57 <Show when={failure()}>
58 {(text) => (
59 <p class="stale-note" role="status">
60 <span class="error">Couldn't refresh {props.what}.</span> {text()}
61 <button class="button small" onClick={props.retry}>retry</button>
62 </p>
63 )}
64 </Show>
65 {props.children(latest)}
66 </>
67 )}
68 </Show>
69 );
70}