127 lines
4.7 KiB
TypeScript
127 lines
4.7 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { Database, Search } from 'lucide-react';
|
|
|
|
import {
|
|
Badge,
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
Input,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
listDatasets,
|
|
VISIBILITY_LABEL,
|
|
} from '@ump/shared';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
/**
|
|
* Clinical data repository — every imaging dataset on the platform (admin scope=all).
|
|
* Read-only inventory for milestone 1; per-dataset drill-down lands in a later slice.
|
|
*/
|
|
export function DatasetsPage() {
|
|
const [q, setQ] = useState('');
|
|
const { data, isLoading, isError } = useQuery({
|
|
queryKey: ['imagehub', 'datasets', 'all'],
|
|
queryFn: () => listDatasets({ scope: 'all' }),
|
|
});
|
|
|
|
const filtered = useMemo(() => {
|
|
const needle = q.trim().toLowerCase();
|
|
if (!needle) return data ?? [];
|
|
return (data ?? []).filter(
|
|
(d) =>
|
|
d.name.toLowerCase().includes(needle) ||
|
|
(d.ownerEmail ?? '').toLowerCase().includes(needle) ||
|
|
d.modalityTags.some((t) => t.toLowerCase().includes(needle)),
|
|
);
|
|
}, [data, q]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="font-serif text-2xl font-semibold text-foreground">Kho dữ liệu lâm sàng</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Toàn bộ bộ dữ liệu hình ảnh trên hệ thống của các nhà nghiên cứu.
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-3 space-y-0">
|
|
<CardTitle className="text-base">
|
|
{isLoading ? 'Đang tải…' : `${filtered.length} bộ dữ liệu`}
|
|
</CardTitle>
|
|
<div className="relative w-64 max-w-full">
|
|
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Tìm theo tên, chủ sở hữu, nhãn…"
|
|
className="pl-8"
|
|
/>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isError && <p className="text-sm text-destructive">Không tải được kho dữ liệu.</p>}
|
|
{!isError && !isLoading && filtered.length === 0 && (
|
|
<div className="py-12 text-center">
|
|
<Database className="mx-auto mb-3 h-10 w-10 text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground">Chưa có bộ dữ liệu nào.</p>
|
|
</div>
|
|
)}
|
|
{filtered.length > 0 && (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Tên bộ dữ liệu</TableHead>
|
|
<TableHead>Chủ sở hữu</TableHead>
|
|
<TableHead>Hiển thị</TableHead>
|
|
<TableHead className="text-right">Tệp</TableHead>
|
|
<TableHead className="text-right">Phiên bản</TableHead>
|
|
<TableHead>Cập nhật</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filtered.map((d) => (
|
|
<TableRow key={d.id}>
|
|
<TableCell className="font-medium">
|
|
<div className="flex items-center gap-2">
|
|
<Database className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<div>
|
|
<div>{d.name}</div>
|
|
{d.modalityTags.length > 0 && (
|
|
<div className="mt-0.5 flex flex-wrap gap-1">
|
|
{d.modalityTags.map((t) => (
|
|
<Badge key={t} variant="secondary" className="text-[10px]">
|
|
{t}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">{d.ownerEmail ?? '—'}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{VISIBILITY_LABEL[d.visibility]}</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-right">{d.fileCount}</TableCell>
|
|
<TableCell className="text-right">{d.versionCount}</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{d.updatedAt ? new Date(d.updatedAt).toLocaleDateString('vi-VN') : '—'}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|