refactor(decnet_web/Credentials): extract CredsTable + ReuseTable + SortTh
This commit is contained in:
114
decnet_web/src/components/Credentials/CredsTable.tsx
Normal file
114
decnet_web/src/components/Credentials/CredsTable.tsx
Normal file
@@ -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<string, ReuseMapEntry>;
|
||||
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<Props> = ({
|
||||
rows, reuseMap, loading, sortCol, sortDir, onSort,
|
||||
onSelectCred, onSelectAttacker, onOpenReuse,
|
||||
}) => (
|
||||
<table className="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortTh col="seen" activeCol={sortCol} dir={sortDir} onSort={onSort}>LAST SEEN</SortTh>
|
||||
<SortTh col="decky" activeCol={sortCol} dir={sortDir} onSort={onSort}>DECKY</SortTh>
|
||||
<SortTh col="svc" activeCol={sortCol} dir={sortDir} onSort={onSort}>SVC</SortTh>
|
||||
<SortTh col="attacker" activeCol={sortCol} dir={sortDir} onSort={onSort}>ATTACKER</SortTh>
|
||||
<SortTh col="principal" activeCol={sortCol} dir={sortDir} onSort={onSort}>PRINCIPAL</SortTh>
|
||||
<th>SECRET</th>
|
||||
<SortTh col="kind" activeCol={sortCol} dir={sortDir} onSort={onSort}>KIND</SortTh>
|
||||
<SortTh col="hits" activeCol={sortCol} dir={sortDir} onSort={onSort}>HITS</SortTh>
|
||||
<th>REUSE</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={c.id} className="clickable" onClick={() => onSelectCred(c)}>
|
||||
<td className="dim" style={{ fontSize: '0.72rem', whiteSpace: 'nowrap' }}>
|
||||
{new Date(c.last_seen).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="violet-accent">{c.decky_name}</td>
|
||||
<td><span className="chip dim-chip">{c.service}</span></td>
|
||||
<td>
|
||||
<span
|
||||
className="matrix-text attacker-link"
|
||||
onClick={(e) => { e.stopPropagation(); onSelectAttacker(c.attacker_ip); }}
|
||||
>
|
||||
{c.attacker_ip}
|
||||
</span>
|
||||
</td>
|
||||
<td className="principal-cell">
|
||||
{c.principal ?? <span className="dim">—</span>}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`secret-cell${isPlain ? '' : ' hashed'}`} title={secretText}>
|
||||
{secretText}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`chip ${isPlain ? 'matrix' : 'violet'}`}>
|
||||
{c.secret_kind.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="attempt-pill">{c.attempt_count}</span>
|
||||
</td>
|
||||
<td>
|
||||
{reuseHit ? (
|
||||
<span
|
||||
className="attempt-pill"
|
||||
style={{ cursor: 'pointer', color: 'var(--violet)' }}
|
||||
title="Open reuse finding"
|
||||
onClick={(e) => { e.stopPropagation(); onOpenReuse(key); }}
|
||||
>
|
||||
×{reuseHit.target_count}
|
||||
</span>
|
||||
) : (
|
||||
<span className="dim">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', opacity: 0.4 }}>
|
||||
<ChevR size={14} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}) : (
|
||||
<tr>
|
||||
<td colSpan={10}>
|
||||
<EmptyState
|
||||
icon={Target}
|
||||
title={loading ? 'RETRIEVING CREDENTIALS…' : 'NO CREDENTIALS YET'}
|
||||
hint={loading ? undefined : 'captured auth attempts will land here'}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
export default CredsTable;
|
||||
84
decnet_web/src/components/Credentials/ReuseTable.tsx
Normal file
84
decnet_web/src/components/Credentials/ReuseTable.tsx
Normal file
@@ -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<Props> = ({
|
||||
rows, loading, sortCol, sortDir, onSort, onSelect,
|
||||
}) => (
|
||||
<table className="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortTh col="seen" activeCol={sortCol} dir={sortDir} onSort={onSort}>LAST SEEN</SortTh>
|
||||
<SortTh col="principal" activeCol={sortCol} dir={sortDir} onSort={onSort}>PRINCIPAL</SortTh>
|
||||
<SortTh col="kind" activeCol={sortCol} dir={sortDir} onSort={onSort}>KIND</SortTh>
|
||||
<SortTh col="targets" activeCol={sortCol} dir={sortDir} onSort={onSort}>TARGETS</SortTh>
|
||||
<SortTh col="attempts" activeCol={sortCol} dir={sortDir} onSort={onSort}>ATTEMPTS</SortTh>
|
||||
<th>DECKIES</th>
|
||||
<th>SERVICES</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={r.id} className="clickable" onClick={() => onSelect(r)}>
|
||||
<td className="dim" style={{ fontSize: '0.72rem', whiteSpace: 'nowrap' }}>
|
||||
{new Date(r.last_seen).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="principal-cell">
|
||||
{r.principal ?? <span className="dim">—</span>}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`chip ${isPlain ? 'matrix' : 'violet'}`}>
|
||||
{r.secret_kind.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td><span className="attempt-pill">{r.target_count}</span></td>
|
||||
<td><span className="attempt-pill">{r.attempt_count}</span></td>
|
||||
<td>
|
||||
{r.deckies.slice(0, 3).map((d) => (
|
||||
<span key={d} className="chip dim-chip" style={{ marginRight: 4 }}>{d}</span>
|
||||
))}
|
||||
{moreDeckies > 0 && <span className="dim">+{moreDeckies}</span>}
|
||||
</td>
|
||||
<td>
|
||||
{r.services.slice(0, 3).map((s) => (
|
||||
<span key={s} className="chip dim-chip" style={{ marginRight: 4 }}>{s}</span>
|
||||
))}
|
||||
{moreServices > 0 && <span className="dim">+{moreServices}</span>}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', opacity: 0.4 }}>
|
||||
<ChevR size={14} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}) : (
|
||||
<tr>
|
||||
<td colSpan={8}>
|
||||
<EmptyState
|
||||
icon={Target}
|
||||
title={loading ? 'RETRIEVING REUSE…' : 'NO REUSE FINDINGS YET'}
|
||||
hint={loading ? undefined : 'a credential captured on ≥2 deckies will land here'}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
export default ReuseTable;
|
||||
22
decnet_web/src/components/Credentials/SortTh.tsx
Normal file
22
decnet_web/src/components/Credentials/SortTh.tsx
Normal file
@@ -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<Props> = ({ col, activeCol, dir, onSort, children }) => (
|
||||
<th
|
||||
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
|
||||
onClick={() => onSort(col)}
|
||||
>
|
||||
{children}
|
||||
{activeCol === col ? (dir === 'asc' ? ' ▲' : ' ▼') : ''}
|
||||
</th>
|
||||
);
|
||||
|
||||
export default SortTh;
|
||||
Reference in New Issue
Block a user