108 lines
6.8 KiB
TypeScript
108 lines
6.8 KiB
TypeScript
import { Fragment, useState } from "react";
|
|
import type { ResourceKind, ResourcePage, ResourceRecord } from "../contracts";
|
|
import { Modal } from "./Modal";
|
|
import { StatusPill } from "./StatusPill";
|
|
|
|
export type Density = "comfortable" | "compact";
|
|
export type ColumnKey = "name" | "status" | "project" | "summary" | "risk" | "decision" | "actions";
|
|
|
|
const ALL_COLUMNS: Record<ResourceKind, ColumnKey[]> = {
|
|
projects: ["name", "status", "project", "summary"],
|
|
servers: ["name", "status", "project", "summary"],
|
|
tools: ["name", "status", "project", "summary", "risk", "actions"],
|
|
connectors: ["name", "status", "project", "summary"],
|
|
clients: ["name", "status", "project", "summary", "actions"],
|
|
policies: ["name", "status", "project", "summary"],
|
|
audit: ["name", "status", "project", "summary", "decision"],
|
|
};
|
|
|
|
export function columnsFor(kind: ResourceKind): ColumnKey[] { return ALL_COLUMNS[kind]; }
|
|
|
|
export function DataTable({
|
|
data, kind, density, visibleColumns, onClientAction, onConfig, onTest, navigate, busyClientId,
|
|
}: {
|
|
data: ResourcePage;
|
|
kind: ResourceKind;
|
|
density: Density;
|
|
visibleColumns: ColumnKey[];
|
|
onClientAction: (id: string, action: "disable" | "revoke") => void;
|
|
onConfig: (id: string) => void;
|
|
onTest: (row: ResourceRecord) => void;
|
|
navigate?: (next: string) => void;
|
|
busyClientId?: string | null;
|
|
}) {
|
|
const show = (column: ColumnKey) => visibleColumns.includes(column);
|
|
const [confirmingAction, setConfirmingAction] = useState<{ id: string; action: "disable" | "revoke"; name: string } | null>(null);
|
|
const actions = (row: ResourceRecord) => {
|
|
if (kind === "clients" && show("actions")) {
|
|
const disabled = busyClientId === row.id;
|
|
const resume = `/clients?onboard=${encodeURIComponent(row.id)}`;
|
|
const secondary = <><button type="button" className="button button-small button-secondary" onClick={() => setConfirmingAction({ id: row.id, action: "disable", name: row.name })} disabled={disabled || row.status !== "healthy"}>Disable</button><button type="button" className="button button-small button-danger" onClick={() => setConfirmingAction({ id: row.id, action: "revoke", name: row.name })} disabled={disabled || row.status === "unavailable"}>Revoke</button>{row.onboardingStage ? <a className="button button-small button-secondary" href={resume} onClick={navigate ? (event) => { event.preventDefault(); navigate(resume); } : undefined}>Onboarding</a> : null}</>;
|
|
return <div className="inline-actions client-actions"><button type="button" className="button button-small button-secondary" disabled={disabled} onClick={() => onConfig(row.id)}>{disabled ? "Working…" : "Config"}</button><div className="desktop-client-actions">{secondary}</div><details className="mobile-client-actions"><summary>More actions</summary><div>{secondary}</div></details></div>;
|
|
}
|
|
if (kind === "tools" && show("actions")) return <div className="inline-actions"><button className="button button-small button-secondary" onClick={() => onTest(row)}>Readiness</button></div>;
|
|
return null;
|
|
};
|
|
const name = (row: ResourceRecord) => {
|
|
const href = kind === "projects" ? `/projects/${encodeURIComponent(row.id)}` : kind === "connectors" ? `/connectors/${encodeURIComponent(row.id)}` : kind === "clients" ? row.onboardingStage ? `/clients?onboard=${encodeURIComponent(row.id)}` : `/clients?q=${encodeURIComponent(row.id)}` : null;
|
|
return href ? <a className="record-link" href={href} onClick={navigate ? (event) => { event.preventDefault(); navigate(href); } : undefined}><strong className="long-name">{row.name}</strong></a> : <strong className="long-name">{row.name}</strong>;
|
|
};
|
|
return (
|
|
<Fragment>
|
|
<div className={`table-wrap desktop-data-table table-kind-${kind} density-${density}`} role="region" tabIndex={0} aria-label={`${kind} records table`}>
|
|
<table>
|
|
<caption className="sr-only">{kind} records</caption>
|
|
<thead>
|
|
<tr>
|
|
{show("name") && <th>Name</th>}
|
|
{show("status") && <th>Status</th>}
|
|
{show("project") && <th>Project</th>}
|
|
{show("summary") && <th>Summary</th>}
|
|
{kind === "tools" && show("risk") && <th>Risk</th>}
|
|
{kind === "audit" && show("decision") && <th>Decision / trace</th>}
|
|
{(kind === "clients" || kind === "tools") && show("actions") && <th>Actions</th>}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.rows.map((row) => (
|
|
<tr key={row.id}>
|
|
{show("name") && <td>{name(row)}<small>{row.id}</small></td>}
|
|
{show("status") && <td><StatusPill value={row.status} /></td>}
|
|
{show("project") && <td>{row.project ?? "—"}</td>}
|
|
{show("summary") && <td>{row.summary}{row.onboardingStage ? <small>{row.onboardingStage}</small> : null}</td>}
|
|
{kind === "tools" && show("risk") && <td>{row.risk ?? "R0"}</td>}
|
|
{kind === "audit" && show("decision") && <td>{row.decision ?? "unknown"}{row.traceId && <small>{row.traceId}</small>}</td>}
|
|
{(kind === "clients" || kind === "tools") && show("actions") && <td>{actions(row)}</td>}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="mobile-record-list" aria-label={`${kind} records`}>
|
|
{data.rows.map((row) => <article className="mobile-record" key={row.id}><div className="mobile-record-heading"><div>{name(row)}<small>{row.id}</small></div>{show("status") && <StatusPill value={row.status} />}</div><dl>{show("project") && <><dt>Project</dt><dd>{row.project ?? "—"}</dd></>}{show("summary") && <><dt>Summary</dt><dd>{row.summary}{row.onboardingStage ? <small>{row.onboardingStage}</small> : null}</dd></>}{kind === "tools" && show("risk") && <><dt>Risk</dt><dd><span className="risk-label" title="Risk classification">{row.risk ?? "R0"}</span></dd></>}{kind === "audit" && show("decision") && <><dt>Decision / trace</dt><dd>{row.decision ?? "unknown"}{row.traceId && <small>{row.traceId}</small>}</dd></>}</dl>{actions(row)}</article>)}
|
|
</div>
|
|
<Modal
|
|
open={confirmingAction !== null}
|
|
title={`${confirmingAction?.action === "disable" ? "Disable" : "Revoke"} ${confirmingAction?.name}?`}
|
|
onClose={() => setConfirmingAction(null)}
|
|
>
|
|
<p>This action is logged and takes effect immediately.</p>
|
|
<div className="state-actions">
|
|
<button
|
|
className="button"
|
|
data-testid="confirm-action"
|
|
onClick={() => {
|
|
if (!confirmingAction) return;
|
|
onClientAction(confirmingAction.id, confirmingAction.action);
|
|
setConfirmingAction(null);
|
|
}}
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button className="button button-secondary" onClick={() => setConfirmingAction(null)}>Cancel</button>
|
|
</div>
|
|
</Modal>
|
|
</Fragment>
|
|
);
|
|
}
|