import { CircleAlert } from "lucide-react";
import type { ReactNode } from "react";
import { displayUsername, formatDate } from "../lib/format";
import type { AccountUsername, AuditLogRow } from "../types";
type Tone = "neutral" | "good" | "danger" | "warn";
export function PageFrame({
title,
eyebrow,
children,
actions
}: {
title: string;
eyebrow?: string;
children: ReactNode;
actions?: ReactNode;
}) {
return (
{eyebrow &&
{eyebrow}
}
{title}
{actions &&
{actions}
}
{children}
);
}
export function QueryPanel({ children }: { children: ReactNode }) {
return {children}
;
}
export function SplitLayout({ main, side }: { main: ReactNode; side: ReactNode }) {
return (
);
}
export function SectionHead({ title, text, action }: { title: string; text?: string; action?: ReactNode }) {
return (
);
}
export function Alert({ children }: { children: ReactNode }) {
return {children}
;
}
export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: Tone }) {
return {children};
}
export function StatusItem({ label, value, tone }: { label: string; value: string; tone: "neutral" | "good" | "warn" }) {
return (
{label}
{value}
);
}
export function Metric({
label,
value,
tone = "neutral",
mono = false,
loading = false
}: {
label: string;
value: string;
tone?: Tone;
mono?: boolean;
loading?: boolean;
}) {
return (
{label}
{loading ? : value}
);
}
export function Summary({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
return (
{label}
{value}
);
}
export function AuditTable({ rows }: { rows: AuditLogRow[] }) {
return (
| {"ID"} | {"Command ID"} | {"Action"} | {"Actor"} | {"Status"} | {"Dry-run"} | {"Reason"} | {"Time"} |
{rows.map((row) => (
| {row.ID} |
{row.CommandID} |
{row.Action} |
{row.Actor} |
{row.Status} |
{row.DryRun ? "Yes" : "No"} |
{row.Reason} |
{formatDate(row.CreatedAt)} |
))}
{rows.length === 0 && }
);
}
export function EmptyRow({ colSpan }: { colSpan: number }) {
return | {"No results"} |
;
}
// Placeholder rows for a table that has nothing yet *because it is still
// loading* -- distinct from EmptyRow, which asserts the query genuinely
// returned nothing. Showing "No results" during the first fetch reads as a
// wrong answer rather than a pending one.
export function LoadingRow({ colSpan, rows = 3 }: { colSpan: number; rows?: number }) {
return (
<>
{Array.from({ length: rows }, (_, row) => (
{Array.from({ length: colSpan }, (_unused, cell) => (
|
|
))}
))}
>
);
}
export function LoadingSurface({ label }: { label: string }) {
return ;
}
export function JsonBlock({ value }: { value: string }) {
return {value || "{}"};
}
// UsernameCell renders a peer's editable username with its collectible usernames
// branching off underneath, in the order clients project them.
//
// An inactive collectible is shown rather than hidden: the peer still owns it, it
// just does not resolve publicly, and an operator looking for "where did that name
// go" needs to see it. It is marked instead of dropped.
// Pass an empty username to render the branch on its own, which is what the
// detail header does: it already shows the editable slot on the line above.
export function UsernameCell({ username, collectibles }: { username?: string; collectibles?: AccountUsername[] | null }) {
const main = displayUsername(username ?? "");
const branch = collectibles ?? [];
if (branch.length === 0) {
return <>{main || "-"}>;
}
return (
<>
{main}
{branch.map((item) => (
-
{displayUsername(item.Username)}
{!item.Active && {"inactive"}}
))}
>
);
}