import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createNeCollection, deleteCollectionJob, fetchCollectionJob, fetchCollectionRuns, fetchEligibleNe, fetchNeCollections, pauseCollectionJob, startCollectionJob, retryFailedCollectionJob, collectionJobDownloadUrl, collectionRunDownloadUrl, } from "../services/api"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; import type { CollectionJobDetail, CollectionJobItem, EligibleNeItem } from "../types"; import { pageCount } from "../utils/display"; import { formatSystemTime } from "../utils/time"; const POLL_MS = 2000; const ELIGIBLE_PAGE_SIZE = 20; export function CollectPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const queryClient = useQueryClient(); const [commands, setCommands] = useState(""); const [title, setTitle] = useState(""); const [selectedMap, setSelectedMap] = useState>({}); const [pickSelectedIds, setPickSelectedIds] = useState([]); const [neKeyword, setNeKeyword] = useState(""); const [nePage, setNePage] = useState(1); const [jobPage, setJobPage] = useState(1); const [expandedJobId, setExpandedJobId] = useState(""); const selectedIds = useMemo(() => Object.keys(selectedMap), [selectedMap]); const selectedList = useMemo(() => Object.values(selectedMap), [selectedMap]); const eligibleQuery = useQuery({ queryKey: queryKeys.collectionEligibleNe(nePage, neKeyword), queryFn: () => fetchEligibleNe({ page: nePage, pageSize: ELIGIBLE_PAGE_SIZE, keyword: neKeyword }), staleTime: 5000, }); const jobsQuery = useQuery({ queryKey: queryKeys.neCollections(jobPage), queryFn: () => fetchNeCollections({ page: jobPage, pageSize: 20 }), staleTime: 1000, refetchInterval: (q) => { const items = q.state.data?.items ?? []; return items.some((j) => j.status === "running") ? POLL_MS : false; }, }); const detailQuery = useQuery({ queryKey: queryKeys.neCollectionDetail(expandedJobId), queryFn: () => fetchCollectionJob(expandedJobId), enabled: Boolean(expandedJobId), staleTime: 500, refetchInterval: (q) => (q.state.data?.job.status === "running" ? POLL_MS : false), }); const jobs = jobsQuery.data?.items ?? []; const jobActive = jobs.some((j) => j.status === "running"); const expandedJobRunning = Boolean(expandedJobId) && (jobs.find((j) => j.id === expandedJobId)?.status === "running" || detailQuery.data?.job.status === "running"); const autoPolling = jobActive || expandedJobRunning; const refreshAll = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll }), expandedJobId ? queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(expandedJobId) }) : Promise.resolve(), expandedJobId ? queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionRunsAll }) : Promise.resolve(), ]); }; const invalidateJobs = async (jobId?: string) => { await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll }); if (jobId) { await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(jobId) }); await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionRunsAll }); } }; const pauseMutation = useMutation({ mutationFn: pauseCollectionJob, onSuccess: async (job) => { showOk(t("collect.paused")); await invalidateJobs(job.id); }, onError: (err) => showError(String(err)), }); const startJobMutation = useMutation({ mutationFn: startCollectionJob, onSuccess: async (job) => { showOk(t("collect.started", { id: job.id })); setExpandedJobId(job.id); await invalidateJobs(job.id); }, onError: (err) => showError(String(err)), }); const retryFailedMutation = useMutation({ mutationFn: retryFailedCollectionJob, onSuccess: async (job) => { showOk(t("collect.retryFailedDone")); setExpandedJobId(job.id); await invalidateJobs(job.id); }, onError: (err) => showError(String(err)), }); const deleteMutation = useMutation({ mutationFn: deleteCollectionJob, onSuccess: async (_, jobId) => { showOk(t("collect.deleted")); if (expandedJobId === jobId) setExpandedJobId(""); await invalidateJobs(); }, onError: (err) => showError(String(err)), }); const createMutation = useMutation({ mutationFn: () => createNeCollection({ title: title.trim(), commands, ne_ids: selectedIds, }), onSuccess: async (job) => { showOk(t("collect.created", { id: job.id })); setExpandedJobId(job.id); await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionsAll }); await queryClient.invalidateQueries({ queryKey: queryKeys.neCollectionDetail(job.id) }); }, onError: (err) => showError(String(err)), }); const addNe = (row: EligibleNeItem) => { setSelectedMap((prev) => (prev[row.id] ? prev : { ...prev, [row.id]: row })); }; const addBatchNe = (rows: EligibleNeItem[]) => { if (rows.length === 0) return; setSelectedMap((prev) => { const next = { ...prev }; for (const row of rows) { if (!next[row.id]) next[row.id] = row; } return next; }); }; const removeNe = (id: string) => { setSelectedMap((prev) => { const next = { ...prev }; delete next[id]; return next; }); }; const clearSelected = () => setSelectedMap({}); const eligibleItems = eligibleQuery.data?.items ?? []; const selectablePickItems = eligibleItems.filter((row) => !selectedMap[row.id]); const allPickSelected = selectablePickItems.length > 0 && selectablePickItems.every((row) => pickSelectedIds.includes(row.id)); const batchPickCount = pickSelectedIds.length; const neTotal = eligibleQuery.data?.total ?? 0; const nePages = pageCount(neTotal, ELIGIBLE_PAGE_SIZE); const jobTotal = jobsQuery.data?.total ?? 0; const jobPages = pageCount(jobTotal, 20); const commandLines = useMemo( () => commands .split("\n") .map((l) => l.trim()) .filter((l) => l && !l.startsWith("#")).length, [commands], ); const actionPending = pauseMutation.isPending || startJobMutation.isPending || retryFailedMutation.isPending || deleteMutation.isPending; return (

{t("collect.create.title")}

{t("collect.create.hint")}