treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
This commit is contained in:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -28,21 +28,28 @@
|
|||
// loader is `text` (for unrelated shadow-DOM components' CSS-as-string
|
||||
// needs), and that loader is global per call, not per-module.
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
||||
import { useState, useEffect, useCallback, useRef } from "preact/hooks";
|
||||
|
||||
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
|
||||
// `rename_all`) — see that enum's own doc comment for why it's kept in
|
||||
// an exhaustive match on the Rust side; this union is this file's
|
||||
// equivalent contract.
|
||||
type NodeState = 'Pending' | 'Running' | 'Finishing' | 'Done' | 'Failed' | 'Cancelled' | 'Skipped';
|
||||
type NodeState =
|
||||
| "Pending"
|
||||
| "Running"
|
||||
| "Finishing"
|
||||
| "Done"
|
||||
| "Failed"
|
||||
| "Cancelled"
|
||||
| "Skipped";
|
||||
|
||||
type TerminalState = 'Done' | 'Failed' | 'Cancelled' | 'Skipped';
|
||||
type TerminalState = "Done" | "Failed" | "Cancelled" | "Skipped";
|
||||
|
||||
// Mirrors `hive_jobq_wire::GraphDep` — externally tagged on `kind`,
|
||||
// values are the Rust variant names verbatim.
|
||||
type GraphDep =
|
||||
| { kind: 'Node'; id: number; accepts: TerminalState[] }
|
||||
| { kind: 'Resource'; name: string; count: number };
|
||||
| { kind: "Node"; id: number; accepts: TerminalState[] }
|
||||
| { kind: "Resource"; name: string; count: number };
|
||||
|
||||
interface NodePayload {
|
||||
label: string;
|
||||
|
|
@ -69,13 +76,13 @@ interface TreeNode extends GraphNode {
|
|||
}
|
||||
|
||||
const STATE_GLYPH: Record<NodeState, string> = {
|
||||
Pending: '⏸',
|
||||
Running: '▶',
|
||||
Finishing: '◐',
|
||||
Done: '✔',
|
||||
Failed: '✖',
|
||||
Cancelled: '⊘',
|
||||
Skipped: '·',
|
||||
Pending: "⏸",
|
||||
Running: "▶",
|
||||
Finishing: "◐",
|
||||
Done: "✔",
|
||||
Failed: "✖",
|
||||
Cancelled: "⊘",
|
||||
Skipped: "·",
|
||||
};
|
||||
|
||||
// Declaration order doubles as render order for the filter checkboxes —
|
||||
|
|
@ -84,12 +91,16 @@ const STATE_GLYPH: Record<NodeState, string> = {
|
|||
const ALL_STATES = Object.keys(STATE_GLYPH) as NodeState[];
|
||||
|
||||
// Product call: "default selection filters out skipped and done."
|
||||
const DEFAULT_HIDDEN_STATES = new Set<NodeState>(['Done', 'Skipped']);
|
||||
const DEFAULT_HIDDEN_STATES = new Set<NodeState>(["Done", "Skipped"]);
|
||||
|
||||
// Non-terminal states a cancel button makes sense on. Finishing is
|
||||
// included — "own logic done, children still running" is still a subtree
|
||||
// worth stopping early.
|
||||
const CANCELLABLE_STATES = new Set<NodeState>(['Pending', 'Running', 'Finishing']);
|
||||
const CANCELLABLE_STATES = new Set<NodeState>([
|
||||
"Pending",
|
||||
"Running",
|
||||
"Finishing",
|
||||
]);
|
||||
|
||||
// Build a parent/child tree from the flat wire array. `parent` (structural
|
||||
// grouping) defines tree shape. Sibling order follows array order, which
|
||||
|
|
@ -117,7 +128,9 @@ function buildTree(nodes: GraphNode[]): TreeNode[] {
|
|||
}
|
||||
for (const n of byId.values()) {
|
||||
n._waitsOn = (n.deps || [])
|
||||
.filter((d): d is Extract<GraphDep, { kind: 'Node' }> => d.kind === 'Node')
|
||||
.filter(
|
||||
(d): d is Extract<GraphDep, { kind: "Node" }> => d.kind === "Node",
|
||||
)
|
||||
.map((d) => byId.get(d.id))
|
||||
.filter((dep): dep is TreeNode => dep != null)
|
||||
.map((dep) => dep.payload.label);
|
||||
|
|
@ -131,17 +144,19 @@ function buildTree(nodes: GraphNode[]): TreeNode[] {
|
|||
// single stringified row rather than silently dropping it).
|
||||
function DataList({ data }: { data: unknown }) {
|
||||
if (data == null) return null;
|
||||
const isPlainObject = typeof data === 'object' && !Array.isArray(data);
|
||||
const isPlainObject = typeof data === "object" && !Array.isArray(data);
|
||||
const entries: [string, unknown][] = isPlainObject
|
||||
? Object.entries(data as Record<string, unknown>)
|
||||
: [['data', data]];
|
||||
: [["data", data]];
|
||||
if (!entries.length) return null;
|
||||
return (
|
||||
<dl class="jg-data">
|
||||
{entries.map(([k, v]) => (
|
||||
<>
|
||||
<dt key={k + '-dt'}>{k}</dt>
|
||||
<dd key={k + '-dd'}>{typeof v === 'string' ? v : JSON.stringify(v)}</dd>
|
||||
<dt key={k + "-dt"}>{k}</dt>
|
||||
<dd key={k + "-dd"}>
|
||||
{typeof v === "string" ? v : JSON.stringify(v)}
|
||||
</dd>
|
||||
</>
|
||||
))}
|
||||
</dl>
|
||||
|
|
@ -157,7 +172,7 @@ function NodeView({
|
|||
cancellable: boolean;
|
||||
onCancel?: (id: number) => void;
|
||||
}) {
|
||||
const glyph = STATE_GLYPH[n.state] || '?';
|
||||
const glyph = STATE_GLYPH[n.state] || "?";
|
||||
const showCancel = cancellable && CANCELLABLE_STATES.has(n.state);
|
||||
// Flash the state glyph on a genuine state change (Pending → Running,
|
||||
// etc.), not on mount — `prevState` starts at the node's own initial
|
||||
|
|
@ -176,27 +191,41 @@ function NodeView({
|
|||
return (
|
||||
<div class="jg-node">
|
||||
<div class="jg-row">
|
||||
<span class={'jg-state jg-state-' + n.state.toLowerCase() + (flashing ? ' jg-state-flash' : '')}
|
||||
title={n.state + (n.error ? ' — ' + n.error : '')}
|
||||
onAnimationEnd={() => setFlashing(false)}>
|
||||
<span
|
||||
class={
|
||||
"jg-state jg-state-" +
|
||||
n.state.toLowerCase() +
|
||||
(flashing ? " jg-state-flash" : "")
|
||||
}
|
||||
title={n.state + (n.error ? " — " + n.error : "")}
|
||||
onAnimationEnd={() => setFlashing(false)}
|
||||
>
|
||||
{glyph}
|
||||
</span>
|
||||
{' '}
|
||||
</span>{" "}
|
||||
<span class="jg-label">{n.payload.label}</span>
|
||||
{showCancel && (
|
||||
<button type="button" class="jg-cancel-btn" title={'cancel ' + n.payload.label}
|
||||
onClick={() => onCancel && onCancel(n.id)}>
|
||||
<button
|
||||
type="button"
|
||||
class="jg-cancel-btn"
|
||||
title={"cancel " + n.payload.label}
|
||||
onClick={() => onCancel && onCancel(n.id)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{n._waitsOn && n._waitsOn.length > 0 && (
|
||||
<div class="jg-waits-on">waits on: {n._waitsOn.join(', ')}</div>
|
||||
<div class="jg-waits-on">waits on: {n._waitsOn.join(", ")}</div>
|
||||
)}
|
||||
<DataList data={n.payload.data} />
|
||||
{n.error && <pre class="jg-error">{n.error}</pre>}
|
||||
{n._children.map((child) => (
|
||||
<NodeView key={child.id} n={child} cancellable={cancellable} onCancel={onCancel} />
|
||||
<NodeView
|
||||
key={child.id}
|
||||
n={child}
|
||||
cancellable={cancellable}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -212,12 +241,20 @@ function FilterBar({
|
|||
return (
|
||||
<div class="jg-filter">
|
||||
{ALL_STATES.map((state) => {
|
||||
const id = 'jg-filter-' + state.toLowerCase();
|
||||
const id = "jg-filter-" + state.toLowerCase();
|
||||
return (
|
||||
<label key={state} for={id} class={'jg-filter-label jg-state-' + state.toLowerCase()}>
|
||||
<input type="checkbox" id={id} checked={selectedStates.has(state)}
|
||||
onChange={() => onToggle(state)} />
|
||||
{' '}{STATE_GLYPH[state] + ' ' + state}
|
||||
<label
|
||||
key={state}
|
||||
for={id}
|
||||
class={"jg-filter-label jg-state-" + state.toLowerCase()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={selectedStates.has(state)}
|
||||
onChange={() => onToggle(state)}
|
||||
/>{" "}
|
||||
{STATE_GLYPH[state] + " " + state}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
|
@ -229,11 +266,14 @@ function FilterBar({
|
|||
// param — omitted entirely when every state is checked, so the
|
||||
// unfiltered default case sends the exact same request as before this
|
||||
// filter existed.
|
||||
function fetchUrl(endpoint: string | undefined, selectedStates: Set<NodeState>): string | null {
|
||||
function fetchUrl(
|
||||
endpoint: string | undefined,
|
||||
selectedStates: Set<NodeState>,
|
||||
): string | null {
|
||||
if (!endpoint) return null;
|
||||
if (selectedStates.size >= ALL_STATES.length) return endpoint;
|
||||
const url = new URL(endpoint, window.location.origin);
|
||||
url.searchParams.set('states', Array.from(selectedStates).join(','));
|
||||
url.searchParams.set("states", Array.from(selectedStates).join(","));
|
||||
return url.pathname + url.search;
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +289,13 @@ export interface JobqGraphProps {
|
|||
// change identity so the effect below re-runs, giving a host an
|
||||
// explicit "refetch now" lever (bump it and re-render) without an
|
||||
// imperative ref into this component.
|
||||
export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, refreshToken = 0 }: JobqGraphProps) {
|
||||
export function JobqGraph({
|
||||
endpoint,
|
||||
cancellable = false,
|
||||
onUpdate,
|
||||
onCancel,
|
||||
refreshToken = 0,
|
||||
}: JobqGraphProps) {
|
||||
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
|
||||
() => new Set(ALL_STATES.filter((s) => !DEFAULT_HIDDEN_STATES.has(s))),
|
||||
);
|
||||
|
|
@ -259,7 +305,8 @@ export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, r
|
|||
const toggleState = useCallback((state: NodeState) => {
|
||||
setSelectedStates((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(state)) next.delete(state); else next.add(state);
|
||||
if (next.has(state)) next.delete(state);
|
||||
else next.add(state);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
|
@ -271,7 +318,7 @@ export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, r
|
|||
(async () => {
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error('http ' + r.status);
|
||||
if (!r.ok) throw new Error("http " + r.status);
|
||||
const data = (await r.json()) as GraphNode[];
|
||||
if (cancelled) return;
|
||||
setNodes(data);
|
||||
|
|
@ -282,11 +329,13 @@ export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, r
|
|||
setError(String(err));
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- selectedStates is a Set;
|
||||
// its *contents* are what should retrigger the fetch, not its identity, and the
|
||||
// string form below already changes identity exactly when contents do.
|
||||
}, [endpoint, Array.from(selectedStates).sort().join(','), refreshToken]);
|
||||
}, [endpoint, Array.from(selectedStates).sort().join(","), refreshToken]);
|
||||
|
||||
return (
|
||||
<div class="jg-root">
|
||||
|
|
@ -300,7 +349,12 @@ export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, r
|
|||
<p class="jg-empty">empty</p>
|
||||
) : (
|
||||
buildTree(nodes).map((root) => (
|
||||
<NodeView key={root.id} n={root} cancellable={cancellable} onCancel={onCancel} />
|
||||
<NodeView
|
||||
key={root.id}
|
||||
n={root}
|
||||
cancellable={cancellable}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,10 +65,10 @@
|
|||
animation: none;
|
||||
}
|
||||
}
|
||||
:root[data-motion='reduce'] .jg-node {
|
||||
:root[data-motion="reduce"] .jg-node {
|
||||
animation: none;
|
||||
}
|
||||
:root[data-motion='allow'] .jg-node {
|
||||
:root[data-motion="allow"] .jg-node {
|
||||
animation: jg-node-enter 160ms ease;
|
||||
}
|
||||
|
||||
|
|
@ -93,13 +93,30 @@
|
|||
min-width: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
.jg-state-pending { color: var(--muted); }
|
||||
.jg-state-running { color: var(--cyan); }
|
||||
.jg-state-finishing { color: var(--cyan); opacity: 0.75; }
|
||||
.jg-state-done { color: var(--green); }
|
||||
.jg-state-failed { color: var(--red); }
|
||||
.jg-state-cancelled { color: var(--muted); text-decoration: line-through; }
|
||||
.jg-state-skipped { color: var(--muted); opacity: 0.5; }
|
||||
.jg-state-pending {
|
||||
color: var(--muted);
|
||||
}
|
||||
.jg-state-running {
|
||||
color: var(--cyan);
|
||||
}
|
||||
.jg-state-finishing {
|
||||
color: var(--cyan);
|
||||
opacity: 0.75;
|
||||
}
|
||||
.jg-state-done {
|
||||
color: var(--green);
|
||||
}
|
||||
.jg-state-failed {
|
||||
color: var(--red);
|
||||
}
|
||||
.jg-state-cancelled {
|
||||
color: var(--muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.jg-state-skipped {
|
||||
color: var(--muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Short-lived pulse applied by `NodeView` (JobqGraph.tsx) exactly when
|
||||
a node's own `state` value changes on an existing DOM node — a mount
|
||||
|
|
@ -123,10 +140,10 @@
|
|||
animation: none;
|
||||
}
|
||||
}
|
||||
:root[data-motion='reduce'] .jg-state-flash {
|
||||
:root[data-motion="reduce"] .jg-state-flash {
|
||||
animation: none;
|
||||
}
|
||||
:root[data-motion='allow'] .jg-state-flash {
|
||||
:root[data-motion="allow"] .jg-state-flash {
|
||||
animation: jg-state-flash 350ms ease;
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +166,9 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
.jg-cancel-btn:hover,
|
||||
.jg-cancel-btn:focus-visible {
|
||||
|
|
@ -166,8 +185,13 @@
|
|||
grid-template-columns: auto 1fr;
|
||||
gap: 0 0.5em;
|
||||
}
|
||||
.jg-data dt { font-weight: 600; }
|
||||
.jg-data dd { margin: 0; word-break: break-word; }
|
||||
.jg-data dt {
|
||||
font-weight: 600;
|
||||
}
|
||||
.jg-data dd {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.jg-waits-on {
|
||||
margin: 0.1em 0 0 1.6em;
|
||||
|
|
|
|||
Loading…
Reference in a new issue