Files
model-platform/frontend/app/features/schedules/ArtifactList.tsx
T

161 lines
6.3 KiB
TypeScript

import { memo, useMemo, useState } from "react";
import { type ScheduleArtifact } from "../../services/api";
import Icon from "../../components/common/Icon";
import { shortHash } from "./utils";
// 包裹 memo 让父级 SchedulePage 在画布交互(setSelectedNodeId /
// setSelectedEdgeId / linkSourceId 等)重渲染时,artifact 卡片不会跟着
// 重建/重绑拖拽事件 —— 减少拖动过程中的事件重绑与列表抖动。
function ArtifactListImpl({
artifacts,
keyword,
onDrop,
onDoubleClick,
onContextMenu,
onKeywordChange,
}: {
artifacts: ScheduleArtifact[];
keyword: string;
onDrop: (artifact: ScheduleArtifact, x: number, y: number) => void;
onDoubleClick: (artifact: ScheduleArtifact) => void;
onContextMenu: (event: React.MouseEvent, artifact: ScheduleArtifact) => void;
onKeywordChange: (keyword: string) => void;
}) {
const filtered = keyword.trim()
? artifacts.filter((item) => (
item.script_name.toLowerCase().includes(keyword.trim().toLowerCase())
|| item.version_label.toLowerCase().includes(keyword.trim().toLowerCase())
))
: artifacts;
const [expandedScriptNames, setExpandedScriptNames] = useState<Set<string>>(new Set());
const groups = useMemo(() => {
const map = new Map<string, ScheduleArtifact[]>();
for (const item of filtered) {
const list = map.get(item.script_name);
if (list) list.push(item);
else map.set(item.script_name, [item]);
}
// 组内按 created_at desc,version_label desc 兜底
for (const list of map.values()) {
list.sort((a, b) => {
if (a.created_at !== b.created_at) return a.created_at < b.created_at ? 1 : -1;
return a.version_label < b.version_label ? 1 : -1;
});
}
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
}, [filtered]);
function toggle(scriptName: string) {
setExpandedScriptNames((prev) => {
const next = new Set(prev);
if (next.has(scriptName)) next.delete(scriptName);
else next.add(scriptName);
return next;
});
}
return (
<section className="schedule-panel schedule-artifacts-panel">
<div className="schedule-panel__title">
<div>
<strong>稳定版本脚本</strong>
<span className="schedule-panel__count">{artifacts.length}</span>
</div>
</div>
<label className="schedule-search">
<Icon name="search" size={15} />
<input
value={keyword}
placeholder="搜索稳定版本"
onChange={(event) => onKeywordChange(event.target.value)}
/>
</label>
<div className="artifact-list">
{groups.length === 0 ? (
<p className="schedule-empty">
暂无稳定版本,请先在"构建脚本"中发布
</p>
) : (
groups.map(([scriptName, items]) => {
const isExpanded = expandedScriptNames.has(scriptName);
const scriptType = items[0].script_type;
const iconName = scriptType === "notebook" ? "notebook" : "python";
return (
<div key={scriptName} className="artifact-group">
<button
type="button"
className="artifact-group__header"
aria-expanded={isExpanded}
onClick={() => toggle(scriptName)}
>
<span className="artifact-group__main">
<span className={`artifact-group__chevron${isExpanded ? " is-open" : ""}`}>
<Icon name="chevron" size={14} />
</span>
<span className={`artifact-group__icon artifact-group__icon--${scriptType}`}>
<Icon name={iconName} size={14} />
</span>
<span className="artifact-group__name" title={scriptName}>
{scriptName}
</span>
</span>
{items.length > 1 && (
<span className="artifact-group__count">{items.length}</span>
)}
</button>
{isExpanded && (
<div className="artifact-versions">
{items.map((artifact) => (
<div
className="artifact-version"
draggable
key={artifact.versions_id}
onDragStart={(event) => {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData(
"application/x-model-platform-version",
artifact.versions_id,
);
event.dataTransfer.setData("text/plain", artifact.versions_id);
}}
onContextMenu={(event) => onContextMenu(event, artifact)}
onDoubleClick={() => onDoubleClick(artifact)}
>
<span className="artifact-version__main">
<span className="artifact-version__handle">
<Icon name="grip-vertical" size={13} />
</span>
<span className="artifact-version__name">
{artifact.version_label}
</span>
<span className="artifact-version__commit">
{shortHash(artifact.content_hash)}
</span>
</span>
<span className="artifact-version__action">
<Icon name="external" size={12} />
</span>
</div>
))}
</div>
)}
</div>
);
})
)}
</div>
<p className="artifact-tip artifact-tip--footer">
<span>按住</span>
<span className="artifact-tip__grip">
<Icon name="grip-vertical" size={12} />
</span>
<span>拖拽卡片到画布即可绑定节点</span>
</p>
</section>
);
}
export const ArtifactList = memo(ArtifactListImpl);