From bf79581cc91f49556a83b1a9e91262fcb757894f Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 06:11:38 -0400 Subject: [PATCH] refactor(decnet_web/Credentials): extract CredsTable + ReuseTable + SortTh --- .../src/components/Credentials/CredsTable.tsx | 114 ++++++++++++++++++ .../src/components/Credentials/ReuseTable.tsx | 84 +++++++++++++ .../src/components/Credentials/SortTh.tsx | 22 ++++ 3 files changed, 220 insertions(+) create mode 100644 decnet_web/src/components/Credentials/CredsTable.tsx create mode 100644 decnet_web/src/components/Credentials/ReuseTable.tsx create mode 100644 decnet_web/src/components/Credentials/SortTh.tsx diff --git a/decnet_web/src/components/Credentials/CredsTable.tsx b/decnet_web/src/components/Credentials/CredsTable.tsx new file mode 100644 index 00000000..e72b4749 --- /dev/null +++ b/decnet_web/src/components/Credentials/CredsTable.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { ChevronRight as ChevR, Target } from '../../icons'; +import EmptyState from '../EmptyState/EmptyState'; +import SortTh from './SortTh'; +import { reuseKey, truncHash } from './helpers'; +import type { + CredentialEntry, ReuseMapEntry, SortDir, +} from './types'; + +interface Props { + rows: CredentialEntry[]; + reuseMap: Map; + loading: boolean; + sortCol: string; + sortDir: SortDir; + onSort: (col: string) => void; + onSelectCred: (c: CredentialEntry) => void; + onSelectAttacker: (ip: string) => void; + onOpenReuse: (key: string) => void; +} + +const CredsTable: React.FC = ({ + rows, reuseMap, loading, sortCol, sortDir, onSort, + onSelectCred, onSelectAttacker, onOpenReuse, +}) => ( + + + + LAST SEEN + DECKY + SVC + ATTACKER + PRINCIPAL + + KIND + HITS + + + + + + {rows.length > 0 ? rows.map((c) => { + const isPlain = c.secret_kind === 'plaintext'; + const secretText = isPlain + ? (c.secret_printable ?? '—') + : truncHash(c.secret_sha256, 16); + const key = reuseKey(c.secret_sha256, c.secret_kind, c.principal); + const reuseHit = reuseMap.get(key); + return ( + onSelectCred(c)}> + + + + + + + + + + + + ); + }) : ( + + + + )} + +
SECRETREUSE
+ {new Date(c.last_seen).toLocaleTimeString()} + {c.decky_name}{c.service} + { e.stopPropagation(); onSelectAttacker(c.attacker_ip); }} + > + {c.attacker_ip} + + + {c.principal ?? } + + + {secretText} + + + + {c.secret_kind.toUpperCase()} + + + {c.attempt_count} + + {reuseHit ? ( + { e.stopPropagation(); onOpenReuse(key); }} + > + ×{reuseHit.target_count} + + ) : ( + + )} + + +
+ +
+); + +export default CredsTable; diff --git a/decnet_web/src/components/Credentials/ReuseTable.tsx b/decnet_web/src/components/Credentials/ReuseTable.tsx new file mode 100644 index 00000000..4862c4b0 --- /dev/null +++ b/decnet_web/src/components/Credentials/ReuseTable.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { ChevronRight as ChevR, Target } from '../../icons'; +import EmptyState from '../EmptyState/EmptyState'; +import SortTh from './SortTh'; +import type { CredentialReuseRow, SortDir } from './types'; + +interface Props { + rows: CredentialReuseRow[]; + loading: boolean; + sortCol: string; + sortDir: SortDir; + onSort: (col: string) => void; + onSelect: (r: CredentialReuseRow) => void; +} + +const ReuseTable: React.FC = ({ + rows, loading, sortCol, sortDir, onSort, onSelect, +}) => ( + + + + LAST SEEN + PRINCIPAL + KIND + TARGETS + ATTEMPTS + + + + + + + {rows.length > 0 ? rows.map((r) => { + const isPlain = r.secret_kind === 'plaintext'; + const moreDeckies = Math.max(0, r.deckies.length - 3); + const moreServices = Math.max(0, r.services.length - 3); + return ( + onSelect(r)}> + + + + + + + + + + ); + }) : ( + + + + )} + +
DECKIESSERVICES
+ {new Date(r.last_seen).toLocaleTimeString()} + + {r.principal ?? } + + + {r.secret_kind.toUpperCase()} + + {r.target_count}{r.attempt_count} + {r.deckies.slice(0, 3).map((d) => ( + {d} + ))} + {moreDeckies > 0 && +{moreDeckies}} + + {r.services.slice(0, 3).map((s) => ( + {s} + ))} + {moreServices > 0 && +{moreServices}} + + +
+ +
+); + +export default ReuseTable; diff --git a/decnet_web/src/components/Credentials/SortTh.tsx b/decnet_web/src/components/Credentials/SortTh.tsx new file mode 100644 index 00000000..caed3373 --- /dev/null +++ b/decnet_web/src/components/Credentials/SortTh.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import type { SortDir } from './types'; + +interface Props { + col: string; + activeCol: string; + dir: SortDir; + onSort: (col: string) => void; + children: React.ReactNode; +} + +const SortTh: React.FC = ({ col, activeCol, dir, onSort, children }) => ( + onSort(col)} + > + {children} + {activeCol === col ? (dir === 'asc' ? ' ▲' : ' ▼') : ''} + +); + +export default SortTh;