1a974265de
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.
135 lines
4.7 KiB
TypeScript
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);
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|