feat(web): row-click inspector drawer for orchestrator events

Mirrors the CredentialsInspector pattern: clicking a row opens a
right-edge drawer with the full event payload pretty-printed and
copyable.  The table view truncates the src/dst id to 8 chars; the
drawer shows the full identifier plus a SOURCE chip
(TOPOLOGY / FLEET / SHARD) so operators can tell at a glance whether
the orchestrator hit a MazeNET decky, a unihost fleet decky, or a
SWARM shard.

Source detection is purely client-side based on id shape — bare UUID
→ topology, "local:*" → fleet, "<host>:*" → shard.  The server
already returns a normalized id from list_running_deckies; this
inspector just labels it.

Backdrop click closes via target===currentTarget guard (per the
React stop-propagation memory: never use stopPropagation on drawer
panels — it breaks native event delegation).

Live (in-flight stream) events use synthetic uuids prefixed "live-";
the drawer hides the EVENT UUID row and shows "LIVE EVENT" in the
header for those, since the server-side id won't exist until the
backend persists the row.
This commit is contained in:
2026-04-26 21:44:52 -04:00
parent 9650366d34
commit 674028d476
3 changed files with 360 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import {
} from '../icons';
import api from '../utils/api';
import EmptyState from './EmptyState/EmptyState';
import OrchestratorInspector from './OrchestratorInspector';
import { useOrchestratorStream, type OrchestratorStreamEvent } from './useOrchestratorStream';
import './Orchestrator.css';
@@ -51,6 +52,7 @@ const Orchestrator: React.FC = () => {
const [status, setStatus] = useState<StreamStatus>('connecting');
const [paused, setPaused] = useState(false);
const [now, setNow] = useState(Date.now());
const [selected, setSelected] = useState<OrchestratorEntry | null>(null);
const limit = 50;
const pausedRef = useRef(paused);
@@ -210,7 +212,11 @@ const Orchestrator: React.FC = () => {
const cls = !r.success ? 'fail' : fresh ? 'fresh' : '';
const kindCls = r.kind === 'traffic' || r.kind === 'file' ? r.kind : '';
return (
<tr key={r.uuid} className={cls}>
<tr
key={r.uuid}
className={`${cls} clickable`}
onClick={() => setSelected(r)}
>
<td className="dim">{timeAgo(r.ts)}</td>
<td>
<span className={`kind-chip ${kindCls}`}>{r.kind}</span>
@@ -244,6 +250,13 @@ const Orchestrator: React.FC = () => {
</table>
</div>
</div>
{selected && (
<OrchestratorInspector
event={selected}
onClose={() => setSelected(null)}
/>
)}
</div>
);
};