Files
Aegis/frontend/src/components/EvidenceList.tsx
T
kitos 1a974265de feat(evidence): inline preview for images and text/JSON files
Adds a View button (eye icon) on each evidence card for previewable file
types. Opens a full-screen modal:
- Images (png/jpg/gif/webp/svg/…): rendered directly via <img> tag
- JSON: fetched authenticated, pretty-printed in green mono
- Text/log/md/csv/xml/yaml/…: fetched authenticated, shown in <pre>

Non-previewable files only show the Download button as before.
Modal closes on Escape or backdrop click.
2026-05-28 13:49:35 +02:00

135 lines
4.7 KiB
TypeScript

import { useState } from "react";
import { FileIcon, Download, Copy, Check, Eye } from "lucide-react";
import EvidencePreviewModal, { getPreviewType } from "./EvidencePreviewModal";
interface Evidence {
id: string;
test_id: string;
file_name: string;
sha256_hash: string;
uploaded_by: string | null;
uploaded_at: string;
download_url?: string;
}
interface EvidenceListProps {
evidences: Evidence[];
onDownload: (evidenceId: string) => void;
}
export default function EvidenceList({ evidences, onDownload }: EvidenceListProps) {
const [copiedHash, setCopiedHash] = useState<string | null>(null);
const [preview, setPreview] = useState<Evidence | null>(null);
const formatDate = (dateStr: string) =>
new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
const copyHash = async (hash: string) => {
await navigator.clipboard.writeText(hash);
setCopiedHash(hash);
setTimeout(() => setCopiedHash(null), 2000);
};
if (evidences.length === 0) {
return (
<div className="rounded-lg border border-gray-800 bg-gray-800/30 p-6 text-center">
<FileIcon className="mx-auto h-10 w-10 text-gray-600" />
<p className="mt-2 text-sm text-gray-400">No evidence files uploaded yet</p>
</div>
);
}
return (
<>
<div className="space-y-3">
{evidences.map((evidence) => {
const previewType = getPreviewType(evidence.file_name);
const canPreview = previewType !== null;
return (
<div
key={evidence.id}
className="rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:bg-gray-800/50"
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="rounded-lg bg-gray-700 p-2">
<FileIcon className="h-5 w-5 text-gray-400" />
</div>
<div>
<p className="font-medium text-gray-200">{evidence.file_name}</p>
<p className="mt-0.5 text-xs text-gray-500">
Uploaded {formatDate(evidence.uploaded_at)}
</p>
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2">
{canPreview && (
<button
onClick={() => setPreview(evidence)}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
title="Preview"
>
<Eye className="h-4 w-4" />
View
</button>
)}
<button
onClick={() => onDownload(evidence.id)}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
>
<Download className="h-4 w-4" />
Download
</button>
</div>
</div>
{/* SHA256 Hash */}
<div className="mt-3 flex items-center gap-2">
<span className="text-xs font-medium text-gray-500">SHA256:</span>
<code className="flex-1 truncate rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-400">
{evidence.sha256_hash}
</code>
<button
onClick={() => copyHash(evidence.sha256_hash)}
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-300"
title="Copy hash"
>
{copiedHash === evidence.sha256_hash ? (
<Check className="h-4 w-4 text-green-400" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
);
})}
</div>
{/* Preview modal */}
{preview && (
<EvidencePreviewModal
evidenceId={preview.id}
fileName={preview.file_name}
previewType={getPreviewType(preview.file_name)}
downloadUrl={preview.download_url ?? `/api/v1/evidence/${preview.id}/file`}
onClose={() => setPreview(null)}
onDownload={() => {
onDownload(preview.id);
setPreview(null);
}}
/>
)}
</>
);
}