feat(dashboard): GuruConnect v2 operator console (pass 1)
All checks were successful
All checks were successful
React + Vite + TypeScript SPA: scaffold, operations-terminal design system, Bearer-token auth, and the Machines view. - Design system: OKLCH-tinted dark theme (ink-slate + signal-cyan), Hanken Grotesk + JetBrains Mono, status-color language (online/offline/granted/pending/denied/not_required), motion with prefers-reduced-motion honored. - Auth: token in sessionStorage via ref (never React state), protected routes, 401 session teardown, admin-gated per-agent-key UI. - Machines view: data table (sticky header, keyboard-activated rows, skeleton loading, actionable empty/error states), non-blocking detail drawer, delete confirm, admin key management with copy-once reveal. - UI primitives: Modal (focus trap + inert + portal + dialogStack), Drawer, Table, Badge/StatusDot, toast, states. - Typed API client normalizing the two error-envelope shapes. Passed Code Review (no blockers), impeccable critique-and-polish, and local gates (tsc/lint/build green). Dev-only Vite proxy to :3002. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
dashboard/src/components/ui/Table.tsx
Normal file
95
dashboard/src/components/ui/Table.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { ReactNode } from "react";
|
||||
import "./table.css";
|
||||
|
||||
export interface Column<T> {
|
||||
/** Unique column key. */
|
||||
key: string;
|
||||
/** Header label. Omit for the status / actions rails. */
|
||||
header?: ReactNode;
|
||||
/** Cell renderer. */
|
||||
render: (row: T) => ReactNode;
|
||||
/** Extra class on the <td> (e.g. dt__status, dt__actions). */
|
||||
cellClass?: string;
|
||||
}
|
||||
|
||||
interface TableProps<T> {
|
||||
columns: Column<T>[];
|
||||
rows: T[];
|
||||
rowKey: (row: T) => string;
|
||||
/** Optional per-row activation (opens detail). Bound to click, Enter, Space. */
|
||||
onRowClick?: (row: T) => void;
|
||||
/** Accessible label for the row's primary activation, e.g. the hostname. */
|
||||
rowLabel?: (row: T) => string;
|
||||
/** Cap the staggered fade-in so large lists don't crawl in. */
|
||||
maxStaggerRows?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dense, console-style data table. Sticky header, hover highlight, hover-
|
||||
* revealed row actions, and a staggered fade-in on mount (capped so big lists
|
||||
* appear promptly). Column-driven so callers compose cells declaratively.
|
||||
*/
|
||||
export function Table<T>({
|
||||
columns,
|
||||
rows,
|
||||
rowKey,
|
||||
onRowClick,
|
||||
rowLabel,
|
||||
maxStaggerRows = 14,
|
||||
}: TableProps<T>) {
|
||||
return (
|
||||
<div className="dt-wrap">
|
||||
<table className="dt">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((c) => (
|
||||
<th key={c.key} className={c.cellClass}>
|
||||
{c.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => {
|
||||
const delay = i < maxStaggerRows ? `${i * 22}ms` : "0ms";
|
||||
return (
|
||||
<tr
|
||||
key={rowKey(row)}
|
||||
style={{
|
||||
animationDelay: delay,
|
||||
cursor: onRowClick ? "pointer" : undefined,
|
||||
}}
|
||||
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
aria-label={
|
||||
onRowClick && rowLabel
|
||||
? `Open detail for ${rowLabel(row)}`
|
||||
: undefined
|
||||
}
|
||||
onKeyDown={
|
||||
onRowClick
|
||||
? (e) => {
|
||||
// Activate on Enter or Space, the standard for a
|
||||
// button-like row. Space must not scroll the page.
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
e.preventDefault();
|
||||
onRowClick(row);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{columns.map((c) => (
|
||||
<td key={c.key} className={c.cellClass}>
|
||||
{c.render(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user