Files
DECNET/decnet_web/src/components/Credentials/ReuseTable.tsx
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

86 lines
3.2 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
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;