You've already forked DataMate
- 将文件获取逻辑从 fetchDataset 函数中分离到独立的 useEffect 钩子 - 添加 dataset.id 依赖以确保在数据集加载后获取文件 - 修复初始加载时可能发生的文件获取时机问题 - 改进组件渲染性能通过更精确的依赖跟踪 - 保持原有功能不变但提升代码可维护性
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { Breadcrumb, App, Tabs, Table, Tag } from "antd";
|
|
import {
|
|
ReloadOutlined,
|
|
DownloadOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
PlusOutlined,
|
|
} from "@ant-design/icons";
|
|
import DetailHeader from "@/components/DetailHeader";
|
|
import { mapDataset, datasetTypeMap } from "../dataset.const";
|
|
import type { Dataset } from "@/pages/DataManagement/dataset.model";
|
|
import { Link, useNavigate, useParams } from "react-router";
|
|
import { useFilesOperation } from "./useFilesOperation";
|
|
import {
|
|
createDatasetTagUsingPost,
|
|
deleteDatasetByIdUsingDelete,
|
|
downloadDatasetUsingGet,
|
|
queryDatasetByIdUsingGet,
|
|
queryDatasetsUsingGet,
|
|
queryDatasetTagsUsingGet,
|
|
querySimilarDatasetsUsingGet,
|
|
updateDatasetByIdUsingPut,
|
|
} from "../dataset.api";
|
|
import DataQuality from "./components/DataQuality";
|
|
import DataLineageFlow from "./components/DataLineageFlow";
|
|
import Overview from "./components/Overview";
|
|
import { Activity, Clock, File, FileType } from "lucide-react";
|
|
import EditDataset from "../Create/EditDataset";
|
|
import ImportConfiguration from "./components/ImportConfiguration";
|
|
import CardView from "@/components/CardView";
|
|
|
|
const SIMILAR_DATASET_LIMIT = 4;
|
|
const SIMILAR_TAGS_PREVIEW_LIMIT = 3;
|
|
|
|
export default function DatasetDetail() {
|
|
const { id } = useParams(); // 获取动态路由参数
|
|
const navigate = useNavigate();
|
|
const [activeTab, setActiveTab] = useState("overview");
|
|
const { message } = App.useApp();
|
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
|
|
|
const [dataset, setDataset] = useState<Dataset>({} as Dataset);
|
|
const [parentDataset, setParentDataset] = useState<Dataset | null>(null);
|
|
const [childDatasets, setChildDatasets] = useState<Dataset[]>([]);
|
|
const [childDatasetsLoading, setChildDatasetsLoading] = useState(false);
|
|
const [similarDatasets, setSimilarDatasets] = useState<Dataset[]>([]);
|
|
const [similarDatasetsLoading, setSimilarDatasetsLoading] = useState(false);
|
|
const [similarTagNames, setSimilarTagNames] = useState<string[]>([]);
|
|
const similarRequestRef = useRef(0);
|
|
const filesOperation = useFilesOperation(dataset);
|
|
|
|
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
|
const normalizeTagNames = (
|
|
tags?: Array<string | { name?: string | null } | null>
|
|
) => {
|
|
if (!tags || tags.length === 0) {
|
|
return [];
|
|
}
|
|
const names = tags
|
|
.map((tag) => (typeof tag === "string" ? tag : tag?.name))
|
|
.filter((name): name is string => !!name && name.trim().length > 0)
|
|
.map((name) => name.trim());
|
|
return Array.from(new Set(names));
|
|
};
|
|
const fetchSimilarDatasets = async (currentDataset: Dataset) => {
|
|
const requestId = similarRequestRef.current + 1;
|
|
similarRequestRef.current = requestId;
|
|
if (!currentDataset?.id) {
|
|
setSimilarDatasets([]);
|
|
setSimilarTagNames([]);
|
|
setSimilarDatasetsLoading(false);
|
|
return;
|
|
}
|
|
const tagNames = normalizeTagNames(
|
|
currentDataset.tags as Array<string | { name?: string }>
|
|
);
|
|
setSimilarTagNames(tagNames);
|
|
setSimilarDatasets([]);
|
|
if (tagNames.length === 0) {
|
|
setSimilarDatasetsLoading(false);
|
|
return;
|
|
}
|
|
setSimilarDatasetsLoading(true);
|
|
try {
|
|
const { data } = await querySimilarDatasetsUsingGet(currentDataset.id, {
|
|
limit: SIMILAR_DATASET_LIMIT,
|
|
});
|
|
if (similarRequestRef.current !== requestId) {
|
|
return;
|
|
}
|
|
const list = Array.isArray(data) ? data : [];
|
|
setSimilarDatasets(list.map((item) => mapDataset(item)));
|
|
} catch (error) {
|
|
console.error("Failed to fetch similar datasets:", error);
|
|
} finally {
|
|
if (similarRequestRef.current === requestId) {
|
|
setSimilarDatasetsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const similarTagsSummary = useMemo(() => {
|
|
if (!similarTagNames || similarTagNames.length === 0) {
|
|
return "";
|
|
}
|
|
const visibleTags = similarTagNames.slice(0, SIMILAR_TAGS_PREVIEW_LIMIT);
|
|
const hiddenCount = similarTagNames.length - visibleTags.length;
|
|
if (hiddenCount > 0) {
|
|
return `${visibleTags.join("、")} 等 ${similarTagNames.length} 个`;
|
|
}
|
|
return visibleTags.join("、");
|
|
}, [similarTagNames]);
|
|
|
|
const navigateItems = useMemo(() => {
|
|
const items = [
|
|
{
|
|
title: <Link to="/data/management">数据管理</Link>,
|
|
},
|
|
];
|
|
if (parentDataset) {
|
|
items.push({
|
|
title: (
|
|
<Link to={`/data/management/detail/${parentDataset.id}`}>
|
|
{parentDataset.name}
|
|
</Link>
|
|
),
|
|
});
|
|
}
|
|
items.push({
|
|
title: dataset.name || "数据集详情",
|
|
});
|
|
return items;
|
|
}, [dataset, parentDataset]);
|
|
const tabList = useMemo(() => {
|
|
const items = [
|
|
{
|
|
key: "overview",
|
|
label: "概览",
|
|
},
|
|
];
|
|
if (!dataset?.parentDatasetId) {
|
|
items.push({
|
|
key: "children",
|
|
label: "关联数据集",
|
|
});
|
|
}
|
|
return items;
|
|
}, [dataset?.parentDatasetId]);
|
|
const handleCreateChildDataset = () => {
|
|
if (!dataset?.id) {
|
|
return;
|
|
}
|
|
navigate("/data/management/create", {
|
|
state: { parentDatasetId: dataset.id },
|
|
});
|
|
};
|
|
const fetchChildDatasets = async (parentId?: string) => {
|
|
if (!parentId) {
|
|
setChildDatasets([]);
|
|
return;
|
|
}
|
|
setChildDatasetsLoading(true);
|
|
try {
|
|
const { data: res } = await queryDatasetsUsingGet({
|
|
parentDatasetId: parentId,
|
|
page: 1,
|
|
size: 1000,
|
|
});
|
|
const list = res?.content || res?.data || [];
|
|
setChildDatasets(list.map((item) => mapDataset(item)));
|
|
} finally {
|
|
setChildDatasetsLoading(false);
|
|
}
|
|
};
|
|
const fetchDataset = async () => {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
const { data } = await queryDatasetByIdUsingGet(id);
|
|
const mapped = mapDataset(data);
|
|
setDataset(mapped);
|
|
fetchSimilarDatasets(mapped);
|
|
if (data?.parentDatasetId) {
|
|
const { data: parentData } = await queryDatasetByIdUsingGet(
|
|
data.parentDatasetId
|
|
);
|
|
setParentDataset(mapDataset(parentData));
|
|
setChildDatasets([]);
|
|
} else {
|
|
setParentDataset(null);
|
|
await fetchChildDatasets(data?.id);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
fetchDataset();
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (dataset?.id) {
|
|
filesOperation.fetchFiles("", 1, 10); // 从根目录开始,第一页
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dataset?.id]);
|
|
useEffect(() => {
|
|
if (dataset?.parentDatasetId && activeTab === "children") {
|
|
setActiveTab("overview");
|
|
}
|
|
}, [activeTab, dataset?.parentDatasetId]);
|
|
|
|
const handleRefresh = async (showMessage = true, prefixOverride?: string) => {
|
|
fetchDataset();
|
|
// 刷新当前目录,保持在当前页
|
|
const targetPrefix =
|
|
prefixOverride !== undefined
|
|
? prefixOverride
|
|
: filesOperation.pagination.prefix;
|
|
filesOperation.fetchFiles(
|
|
targetPrefix,
|
|
filesOperation.pagination.current,
|
|
filesOperation.pagination.pageSize
|
|
);
|
|
if (showMessage) message.success({ content: "数据刷新成功" });
|
|
};
|
|
|
|
const handleDownload = async () => {
|
|
await downloadDatasetUsingGet(dataset.id);
|
|
message.success("文件下载成功");
|
|
};
|
|
|
|
const handleDeleteDataset = async () => {
|
|
await deleteDatasetByIdUsingDelete(dataset.id);
|
|
navigate("/data/management");
|
|
message.success("数据集删除成功");
|
|
};
|
|
|
|
useEffect(() => {
|
|
const refreshData = (e: Event) => {
|
|
const custom = e as CustomEvent<{ prefix?: string }>;
|
|
const prefixOverride = custom.detail?.prefix;
|
|
handleRefresh(false, prefixOverride);
|
|
};
|
|
window.addEventListener("update:dataset", refreshData as EventListener);
|
|
return () => {
|
|
window.removeEventListener(
|
|
"update:dataset",
|
|
refreshData as EventListener
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
// 基本信息描述项
|
|
const statistics = [
|
|
{
|
|
icon: <File className="text-blue-400 w-4 h-4" />,
|
|
key: "file",
|
|
value: dataset?.fileCount || 0,
|
|
},
|
|
{
|
|
icon: <Activity className="text-blue-400 w-4 h-4" />,
|
|
key: "size",
|
|
value: dataset?.size || "0 B",
|
|
},
|
|
{
|
|
icon: <FileType className="text-blue-400 w-4 h-4" />,
|
|
key: "type",
|
|
value:
|
|
datasetTypeMap[dataset?.datasetType as keyof typeof datasetTypeMap]
|
|
?.label ||
|
|
dataset?.type ||
|
|
"未知",
|
|
},
|
|
{
|
|
icon: <Clock className="text-blue-400 w-4 h-4" />,
|
|
key: "time",
|
|
value: dataset?.updatedAt,
|
|
},
|
|
];
|
|
|
|
// 数据集操作列表
|
|
const operations = [
|
|
...(dataset?.id && !dataset.parentDatasetId
|
|
? [
|
|
{
|
|
key: "create-child",
|
|
label: "创建关联数据集",
|
|
icon: <PlusOutlined />,
|
|
onClick: handleCreateChildDataset,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
key: "edit",
|
|
label: "编辑",
|
|
icon: <EditOutlined />,
|
|
onClick: () => {
|
|
setShowEditDialog(true);
|
|
},
|
|
},
|
|
|
|
{
|
|
key: "export",
|
|
label: "导出",
|
|
icon: <DownloadOutlined />,
|
|
// isDropdown: true,
|
|
// items: [
|
|
// { key: "alpaca", label: "Alpaca 格式", icon: <FileTextOutlined /> },
|
|
// { key: "jsonl", label: "JSONL 格式", icon: <DatabaseOutlined /> },
|
|
// { key: "csv", label: "CSV 格式", icon: <FileTextOutlined /> },
|
|
// { key: "coco", label: "COCO 格式", icon: <FileImageOutlined /> },
|
|
// ],
|
|
onClick: () => handleDownload(),
|
|
},
|
|
{
|
|
key: "refresh",
|
|
label: "刷新",
|
|
icon: <ReloadOutlined />,
|
|
onClick: handleRefresh,
|
|
},
|
|
{
|
|
key: "delete",
|
|
label: "删除",
|
|
danger: true,
|
|
confirm: {
|
|
title: "确认删除该数据集?",
|
|
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
|
okText: "删除",
|
|
cancelText: "取消",
|
|
okType: "danger",
|
|
},
|
|
icon: <DeleteOutlined />,
|
|
onClick: handleDeleteDataset,
|
|
},
|
|
];
|
|
const childColumns = [
|
|
{
|
|
title: "名称",
|
|
dataIndex: "name",
|
|
key: "name",
|
|
render: (_: string, record: Dataset) => (
|
|
<Link to={`/data/management/detail/${record.id}`}>{record.name}</Link>
|
|
),
|
|
},
|
|
{
|
|
title: "类型",
|
|
dataIndex: "datasetType",
|
|
key: "datasetType",
|
|
width: 120,
|
|
render: (value: string) => datasetTypeMap[value]?.label || "未知",
|
|
},
|
|
{
|
|
title: "状态",
|
|
dataIndex: "status",
|
|
key: "status",
|
|
width: 120,
|
|
render: (status) =>
|
|
status ? <Tag color={status.color}>{status.label}</Tag> : "-",
|
|
},
|
|
{
|
|
title: "文件数",
|
|
dataIndex: "fileCount",
|
|
key: "fileCount",
|
|
width: 120,
|
|
render: (value?: number) => value ?? 0,
|
|
},
|
|
{
|
|
title: "大小",
|
|
dataIndex: "size",
|
|
key: "size",
|
|
width: 140,
|
|
render: (value?: string) => value || "0 B",
|
|
},
|
|
{
|
|
title: "更新时间",
|
|
dataIndex: "updatedAt",
|
|
key: "updatedAt",
|
|
width: 180,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="h-full flex flex-col gap-4 overflow-hidden">
|
|
<Breadcrumb items={navigateItems} />
|
|
{/* Header */}
|
|
<DetailHeader
|
|
data={dataset}
|
|
statistics={statistics}
|
|
operations={operations}
|
|
tagConfig={{
|
|
showAdd: true,
|
|
tags: dataset.tags || [],
|
|
onFetchTags: async () => {
|
|
const res = await queryDatasetTagsUsingGet({
|
|
page: 0,
|
|
pageSize: 1000,
|
|
});
|
|
return res.data || [];
|
|
},
|
|
onCreateAndTag: async (tagName) => {
|
|
const res = await createDatasetTagUsingPost({ name: tagName });
|
|
if (res.data) {
|
|
await updateDatasetByIdUsingPut(dataset.id, {
|
|
tags: [...dataset.tags.map((tag) => tag.name), res.data.name],
|
|
});
|
|
handleRefresh();
|
|
}
|
|
},
|
|
onAddTag: async (tag) => {
|
|
const res = await updateDatasetByIdUsingPut(dataset.id, {
|
|
tags: [...dataset.tags.map((tag) => tag.name), tag],
|
|
});
|
|
if (res.data) {
|
|
handleRefresh();
|
|
}
|
|
},
|
|
}}
|
|
/>
|
|
<div className="flex-1 overflow-auto">
|
|
<div className="p-6 pt-2 bg-white rounded-md shadow mb-4">
|
|
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
|
<div className="">
|
|
{activeTab === "overview" && (
|
|
<Overview
|
|
dataset={dataset}
|
|
filesOperation={filesOperation}
|
|
fetchDataset={fetchDataset}
|
|
onUpload={() => setShowUploadDialog(true)}
|
|
/>
|
|
)}
|
|
{activeTab === "children" && (
|
|
<div className="pt-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h2 className="text-base font-semibold">关联数据集</h2>
|
|
<span className="text-xs text-gray-500">
|
|
共 {childDatasets.length} 个
|
|
</span>
|
|
</div>
|
|
<Table
|
|
rowKey="id"
|
|
columns={childColumns}
|
|
dataSource={childDatasets}
|
|
loading={childDatasetsLoading}
|
|
pagination={false}
|
|
locale={{ emptyText: "暂无关联数据集" }}
|
|
/>
|
|
</div>
|
|
)}
|
|
{activeTab === "lineage" && <DataLineageFlow dataset={dataset} />}
|
|
{activeTab === "quality" && <DataQuality />}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 相似数据集 */}
|
|
<div className="bg-white rounded-md shadow p-6 mb-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h2 className="text-base font-semibold">相似数据集</h2>
|
|
{similarTagsSummary && (
|
|
<span className="text-xs text-gray-500">
|
|
匹配标签:{similarTagsSummary}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<CardView
|
|
data={similarDatasets}
|
|
loading={similarDatasetsLoading}
|
|
operations={[]}
|
|
pagination={{
|
|
current: 1,
|
|
pageSize: similarDatasets.length || 10,
|
|
total: similarDatasets.length || 0,
|
|
style: { display: "none" },
|
|
}}
|
|
onView={(item) => {
|
|
navigate(`/data/management/detail/${item.id}`);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<ImportConfiguration
|
|
data={dataset}
|
|
open={showUploadDialog}
|
|
onClose={() => setShowUploadDialog(false)}
|
|
prefix={filesOperation.pagination.prefix}
|
|
updateEvent="update:dataset"
|
|
/>
|
|
<EditDataset
|
|
data={dataset}
|
|
open={showEditDialog}
|
|
onClose={() => setShowEditDialog(false)}
|
|
onRefresh={handleRefresh}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|