From 0a967b41725c092807f2644b7c6ac8ee2d42f34c Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 17 Sep 2026 17:06:25 +0800 Subject: [PATCH] Use modals and Excel-like metric sheets for biz_state batches. Co-authored-by: Cursor --- web/src/i18n/en.ts | 18 +- web/src/i18n/zh.ts | 16 +- web/src/index.css | 76 ++ web/src/pages/network/BizStatePage.tsx | 965 ++++++++++++++++--------- 4 files changed, 733 insertions(+), 342 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index cdbc42a..0fa406d 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -183,27 +183,39 @@ const en = { started: "Schedule started", paused: "Paused", collecting: "Collecting", + detail: "Details", profiles: "Monitor items", enable: "On", command: "Command template", params: "Bindings", discoverVrf: "Discover VRFs", saveBindings: "Save bindings", - cancel: "Cancel", + cancel: "Close", bindTitle: "Select VRF bindings", unbound: "Not bound", batches: "Batches", - viewBatch: "View", + viewBatch: "Open", export: "Export", noBatches: "No batches yet", batchDetail: "Batch detail", + batchWorkbook: "Batch workbook", + sheetCommands: "Commands", + sheetLldp: "LLDP neighbors", + sheetVrfRoute: "VRF route summary", + sheetFilterPh: "Filter this sheet…", + sheetEmpty: "No rows in this sheet", + filterColumn: "Column", + filterAllCols: "All columns", + listFilterPh: "Filter NE / IP / vendor / status", colNe: "NE", colStatus: "Status", colLast: "Last collect", colTime: "Time", colRows: "Rows", colActions: "Actions", - pickTask: "Select a task on the left", + colCommand: "Command", + colMessage: "Message", + pickTask: "Open details to manage a task", noNeedBind: "This item needs no parameter bindings", discoverFailed: "Discover failed", bindingsSaved: "Bindings saved", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 25b9592..1c6fba7 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -183,13 +183,14 @@ const zh = { started: "已启动周期采集", paused: "已暂停", collecting: "采集中", + detail: "详情", profiles: "监控项", enable: "启用", command: "命令模板", params: "参数关联", discoverVrf: "发现 VRF", saveBindings: "保存关联", - cancel: "取消", + cancel: "关闭", bindTitle: "选择 VRF 绑定", unbound: "未关联", batches: "采集批次", @@ -197,13 +198,24 @@ const zh = { export: "导出", noBatches: "尚无批次", batchDetail: "批次详情", + batchWorkbook: "批次工作簿", + sheetCommands: "命令", + sheetLldp: "LLDP 邻居", + sheetVrfRoute: "VRF 路由摘要", + sheetFilterPh: "筛选当前表…", + sheetEmpty: "本表无数据", + filterColumn: "筛选列", + filterAllCols: "全部列", + listFilterPh: "筛选网元 / IP / 厂商 / 状态", colNe: "网元", colStatus: "状态", colLast: "最近采集", colTime: "时间", colRows: "行数", colActions: "操作", - pickTask: "选择左侧任务查看详情", + colCommand: "命令", + colMessage: "说明", + pickTask: "打开详情查看任务", noNeedBind: "该监控项无需参数关联", discoverFailed: "发现失败", bindingsSaved: "参数关联已保存", diff --git a/web/src/index.css b/web/src/index.css index fb04e3e..1ab91dd 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -9917,6 +9917,82 @@ html.login-page--paused .login-page__flare { margin-bottom: 10px; } +/* biz_state batch workbook (Excel-like sheets) */ +.bs-workbook-modal .bs-workbook-body { + min-height: min(62vh, 640px); +} + +.bs-sheet-filter { + align-items: center; +} + +.bs-sheet-count { + white-space: nowrap; + font-size: 12px; +} + +.bs-sheet-table { + flex: 1 1 auto; + min-height: 280px; + max-height: min(48vh, 520px); +} + +.bs-sheet-tabs { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: flex-end; + margin-top: 4px; + padding: 6px 4px 0; + border-top: 1px solid rgba(148, 163, 184, 0.28); +} + +.bs-sheet-tab { + appearance: none; + border: 1px solid rgba(148, 163, 184, 0.35); + border-bottom: none; + border-radius: 6px 6px 0 0; + background: rgba(148, 163, 184, 0.12); + color: inherit; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + line-height: 1.2; +} + +.bs-sheet-tab:hover { + background: rgba(148, 163, 184, 0.22); +} + +.bs-sheet-tab.is-active { + background: var(--wb-surface, #fff); + border-color: rgba(59, 130, 246, 0.55); + font-weight: 600; + box-shadow: 0 -1px 0 rgba(59, 130, 246, 0.35) inset; +} + +.bs-sheet-tab__count { + margin-left: 6px; + opacity: 0.65; + font-variant-numeric: tabular-nums; +} + +.bs-bind-panel { + padding: 10px 12px; + border: 1px solid rgba(148, 163, 184, 0.25); + border-radius: 8px; +} + +.bs-bind-list { + max-height: 200px; + overflow: auto; +} + +.bs-bind-item { + display: block; + margin-bottom: 4px; +} + .nm-config-modal__body { flex: 1 1 auto; width: 100%; diff --git a/web/src/pages/network/BizStatePage.tsx b/web/src/pages/network/BizStatePage.tsx index da5cec8..49e1c21 100644 --- a/web/src/pages/network/BizStatePage.tsx +++ b/web/src/pages/network/BizStatePage.tsx @@ -1,6 +1,8 @@ -import { Button, Input } from "@heroui/react"; +import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; +import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useToast } from "../../hooks/useToast"; import { useI18n } from "../../i18n"; import { @@ -34,12 +36,7 @@ type TaskRow = { last_collect_ended_at?: string | null; }; -type Placeholder = { - name: string; - bind_mode?: string; - discover_profile_id?: string; -}; - +type Placeholder = { name: string }; type Profile = { profile_id: string; title: string; @@ -60,28 +57,127 @@ type BatchRow = { type Candidate = { value: string; label: string; rd?: string }; +type SheetCol = { key: string; header: string }; +type SheetDef = { + id: string; + title: string; + columns: SheetCol[]; + rows: Record[]; +}; + +type TaskTab = "profiles" | "batches"; + function fmtTime(v?: string | null) { if (!v) return "—"; return formatSystemTime(v) || v; } +function cellText(v: unknown): string { + if (v === null || v === undefined) return ""; + if (typeof v === "object") return JSON.stringify(v); + return String(v); +} + +function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] { + const sheets: SheetDef[] = []; + const cmds = (batch?.commands || []) as any[]; + if (cmds.length) { + sheets.push({ + id: "commands", + title: t("bizState.sheetCommands"), + columns: [ + { key: "raw_command", header: t("bizState.colCommand") }, + { key: "metric_id", header: "metric" }, + { key: "parse_status", header: t("bizState.colStatus") }, + { key: "row_count", header: t("bizState.colRows") }, + { key: "message", header: t("bizState.colMessage") }, + ], + rows: cmds.map((c) => ({ + raw_command: c.raw_command, + metric_id: c.metric_id, + parse_status: c.parse_status, + row_count: c.row_count, + message: c.message, + profile_id: c.profile_id, + })), + }); + } + const lldp = (batch?.lldp_neighbors || []) as any[]; + if (lldp.length) { + sheets.push({ + id: "lldp_neighbor", + title: t("bizState.sheetLldp"), + columns: [ + { key: "local_if", header: "local_if" }, + { key: "remote_sys", header: "remote_sys" }, + { key: "remote_if", header: "remote_if" }, + { key: "remote_ip", header: "remote_ip" }, + { key: "protocol", header: "protocol" }, + ], + rows: lldp, + }); + } + const vrf = (batch?.vrf_route_summary || []) as any[]; + if (vrf.length) { + sheets.push({ + id: "vrf_route_summary", + title: t("bizState.sheetVrfRoute"), + columns: [ + { key: "vrf", header: "vrf" }, + { key: "source", header: "source" }, + { key: "networks", header: "networks" }, + ], + rows: vrf, + }); + } + return sheets; +} + +function filterSheetRows( + rows: Record[], + columns: SheetCol[], + keyword: string, + columnKey: string, +): Record[] { + const kw = keyword.trim().toLowerCase(); + if (!kw) return rows; + return rows.filter((row) => { + const keys = columnKey ? [columnKey] : columns.map((c) => c.key); + return keys.some((k) => cellText(row[k]).toLowerCase().includes(kw)); + }); +} + export function BizStatePage() { const { t } = useI18n(); const { showOk, showError } = useToast(); + const [tasks, setTasks] = useState([]); - const [selectedId, setSelectedId] = useState(""); - const [detail, setDetail] = useState(null); - const [batches, setBatches] = useState([]); - const [batchDetail, setBatchDetail] = useState(null); - const [profiles, setProfiles] = useState([]); const [nes, setNes] = useState([]); const [neId, setNeId] = useState(""); const [busy, setBusy] = useState(false); + const [listKeyword, setListKeyword] = useState(""); + const debouncedListKw = useDebouncedValue(listKeyword, 250); + + // task modal + const [taskId, setTaskId] = useState(""); + const [detail, setDetail] = useState(null); + const [profiles, setProfiles] = useState([]); + const [batches, setBatches] = useState([]); + const [taskTab, setTaskTab] = useState("profiles"); + + // VRF bind (inside task modal) const [bindItemId, setBindItemId] = useState(""); const [candidates, setCandidates] = useState([]); const [selectedVrfs, setSelectedVrfs] = useState([]); const [discoverCmd, setDiscoverCmd] = useState(""); + // batch workbook modal + const [batchDetail, setBatchDetail] = useState(null); + const [sheetId, setSheetId] = useState(""); + const [sheetKeyword, setSheetKeyword] = useState(""); + const [sheetColumn, setSheetColumn] = useState(""); + const debouncedSheetKw = useDebouncedValue(sheetKeyword, 200); + const refreshTasks = useCallback(async () => { const res = await bizStateListTasks(); setTasks((res.items || []) as TaskRow[]); @@ -105,32 +201,73 @@ export function BizStatePage() { // eslint-disable-next-line react-hooks/exhaustive-deps -- mount once }, [refreshTasks]); + const filteredTasks = useMemo(() => { + const kw = debouncedListKw.trim().toLowerCase(); + if (!kw) return tasks; + return tasks.filter((row) => { + const blob = `${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.status} ${row.last_error}`.toLowerCase(); + return blob.includes(kw); + }); + }, [tasks, debouncedListKw]); + const collectProfiles = useMemo( () => profiles.filter((p) => (p.kind || "collect") === "collect"), [profiles], ); - const openTask = async (id: string) => { - setSelectedId(id); - setBatchDetail(null); + const sheets = useMemo( + () => (batchDetail ? buildBatchSheets(batchDetail, t) : []), + [batchDetail, t], + ); + + const activeSheet = useMemo(() => { + if (!sheets.length) return null; + return sheets.find((s) => s.id === sheetId) || sheets[0]; + }, [sheets, sheetId]); + + const filteredSheetRows = useMemo(() => { + if (!activeSheet) return []; + return filterSheetRows( + activeSheet.rows, + activeSheet.columns, + debouncedSheetKw, + sheetColumn, + ); + }, [activeSheet, debouncedSheetKw, sheetColumn]); + + const loadTask = async (id: string) => { + const task = await bizStateGetTask(id); + setDetail(task); + const b = await bizStateListBatches(id); + setBatches((b.items || []) as BatchRow[]); + const p = await bizStateListProfiles({ + vendor: task.vendor || "", + device_type: task.device_type || "", + }); + setProfiles((p.items || []) as Profile[]); + }; + + const openTask = async (id: string, tab: TaskTab = "profiles") => { + setTaskId(id); + setTaskTab(tab); setBindItemId(""); setCandidates([]); - setSelectedVrfs([]); + setBatchDetail(null); try { - const task = await bizStateGetTask(id); - setDetail(task); - const b = await bizStateListBatches(id); - setBatches((b.items || []) as BatchRow[]); - const p = await bizStateListProfiles({ - vendor: task.vendor || "", - device_type: task.device_type || "", - }); - setProfiles((p.items || []) as Profile[]); + await loadTask(id); } catch (e) { showError(formatErr(e)); + setTaskId(""); } }; + const closeTask = () => { + setTaskId(""); + setDetail(null); + setBindItemId(""); + setCandidates([]); + }; + const createTask = async () => { if (!neId) return; const ne = nes.find((n) => n.id === neId); @@ -146,9 +283,9 @@ export function BizStatePage() { device_type: ne.device_type, }); showOk(t("bizState.created")); - await refreshTasks(); - await openTask(String(task.id)); setNeId(""); + await refreshTasks(); + await openTask(String(task.id), "profiles"); } catch (e) { showError(t("bizState.createFailed") + ": " + formatErr(e)); } finally { @@ -157,12 +294,12 @@ export function BizStatePage() { }; const setStatus = async (status: string) => { - if (!selectedId) return; + if (!taskId) return; setBusy(true); try { - await bizStatePatchTask(selectedId, { status }); + await bizStatePatchTask(taskId, { status }); showOk(status === "running" ? t("bizState.started") : t("bizState.paused")); - await openTask(selectedId); + await loadTask(taskId); await refreshTasks(); } catch (e) { showError(formatErr(e)); @@ -172,23 +309,19 @@ export function BizStatePage() { }; const collectNow = async () => { - if (!selectedId) return; + if (!taskId) return; setBusy(true); try { - await bizStateCollectNow(selectedId); - await openTask(selectedId); - await refreshTasks(); + await bizStateCollectNow(taskId); for (let i = 0; i < 20; i++) { await new Promise((r) => setTimeout(r, 1500)); - const task = await bizStateGetTask(selectedId); + const task = await bizStateGetTask(taskId); setDetail(task); - if (!task.collect_running) { - const b = await bizStateListBatches(selectedId); - setBatches((b.items || []) as BatchRow[]); - break; - } + if (!task.collect_running) break; } + await loadTask(taskId); await refreshTasks(); + setTaskTab("batches"); } catch (e) { showError(formatErr(e)); } finally { @@ -196,26 +329,13 @@ export function BizStatePage() { } }; - const openBatch = async (batchId: string) => { - try { - const d = await bizStateGetBatch(batchId); - setBatchDetail(d); - } catch (e) { - showError(formatErr(e)); - } - }; - - const removeTask = async () => { - if (!selectedId) return; + const removeTask = async (id: string) => { if (!window.confirm(t("bizState.confirmDelete"))) return; setBusy(true); try { - await bizStateDeleteTask(selectedId); + await bizStateDeleteTask(id); showOk(t("bizState.deleted")); - setSelectedId(""); - setDetail(null); - setBatches([]); - setBatchDetail(null); + if (taskId === id) closeTask(); await refreshTasks(); } catch (e) { showError(formatErr(e)); @@ -225,7 +345,7 @@ export function BizStatePage() { }; const toggleProfileItem = async (profile: Profile, enable: boolean) => { - if (!selectedId || !detail) return; + if (!taskId || !detail) return; const items = [...(detail.items || [])]; const idx = items.findIndex((it: any) => it.source_profile_id === profile.profile_id); if (enable) { @@ -244,8 +364,8 @@ export function BizStatePage() { } setBusy(true); try { - await bizStatePatchTask(selectedId, { items }); - await openTask(selectedId); + await bizStatePatchTask(taskId, { items }); + await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { @@ -254,7 +374,7 @@ export function BizStatePage() { }; const startDiscover = async (item: any) => { - if (!selectedId || !detail) return; + if (!taskId) return; const prof = profiles.find((p) => p.profile_id === item.source_profile_id); const ph = (prof?.placeholders || [])[0]; if (!ph) { @@ -265,7 +385,7 @@ export function BizStatePage() { setBindItemId(item.id); try { const res = await bizStateDiscover({ - task_id: selectedId, + task_id: taskId, collect_profile_id: item.source_profile_id, placeholder: ph.name, }); @@ -289,19 +409,19 @@ export function BizStatePage() { }; const saveBindings = async () => { - if (!selectedId || !bindItemId) return; + if (!taskId || !bindItemId) return; const item = (detail?.items || []).find((it: any) => it.id === bindItemId); const prof = profiles.find((p) => p.profile_id === item?.source_profile_id); const phName = (prof?.placeholders || [])[0]?.name || "vrf"; setBusy(true); try { await bizStateSetBindings( - selectedId, + taskId, bindItemId, selectedVrfs.map((v) => ({ placeholder: phName, value: v })), ); showOk(t("bizState.bindingsSaved")); - await openTask(selectedId); + await loadTask(taskId); setBindItemId(""); setCandidates([]); } catch (e) { @@ -311,6 +431,26 @@ export function BizStatePage() { } }; + const openBatch = async (batchId: string) => { + try { + const d = await bizStateGetBatch(batchId); + setBatchDetail(d); + setSheetKeyword(""); + setSheetColumn(""); + const built = buildBatchSheets(d, t); + setSheetId(built[0]?.id || ""); + } catch (e) { + showError(formatErr(e)); + } + }; + + const closeBatch = () => { + setBatchDetail(null); + setSheetId(""); + setSheetKeyword(""); + setSheetColumn(""); + }; + const runningCount = tasks.filter((x) => x.status === "running").length; return ( @@ -348,295 +488,446 @@ export function BizStatePage() { -
-
- - - - - - +
+ setListKeyword(e.target.value)} + /> +
+ +
+
{t("bizState.colNe")}{t("bizState.colStatus")}{t("bizState.colLast")}
+ + + + + + + + + + {filteredTasks.map((row) => ( + + + + + - - - {tasks.map((row) => ( - void openTask(row.id)} - > - - - - - ))} - {!tasks.length ? ( - - - - ) : null} - -
{t("bizState.colNe")}{t("bizState.colStatus")}{t("bizState.colLast")}{t("bizState.colActions")}
+
{row.ne_name || row.ne_ip || "—"}
+
+ {row.vendor || "—"} · {row.ne_ip || "—"} +
+ {row.last_error ?
{row.last_error}
: null} +
+
+ {row.status} + {row.collect_running ? ( + {t("bizState.collecting")} + ) : null} +
+
{fmtTime(row.last_collect_ended_at)} +
+ + + {row.status !== "running" ? ( + + ) : ( + + )} + +
+
-
{row.ne_name || row.ne_ip || "—"}
-
{row.vendor || "—"}
-
-
- {row.status} - {row.collect_running ? ( - {t("bizState.collecting")} - ) : null} -
-
{fmtTime(row.last_collect_ended_at)}
-
{t("bizState.empty")}
-
+ ))} + {!filteredTasks.length ? ( + + +
{t("bizState.empty")}
+ + + ) : null} + + +
+
+ + {/* Task detail modal */} + + + + {detail?.ne_name || detail?.ne_ip || t("bizState.detail")} + {detail ? ` · ${detail.status}` : ""} + + + + + {detail ? ( +

+ {detail.vendor || "—"} · {detail.ne_ip || "—"} · interval {detail.interval_sec}s +

+ ) : null} + {detail?.last_error ?

{detail.last_error}

: null} + +
+ +
-
- {!detail ? ( -
{t("bizState.pickTask")}
- ) : ( - <> -
-

- {detail.ne_name || detail.ne_ip} · {detail.status} -

-
- - - - -
-
- {detail.last_error ?

{detail.last_error}

: null} - -

{t("bizState.profiles")}

-
- - - - - - - - - - - {collectProfiles.map((prof) => { - const it = (detail.items || []).find( - (x: any) => x.source_profile_id === prof.profile_id, - ); - const enabled = Boolean(it?.enabled); - const binds = it?.bindings || []; - const needsBind = (prof.placeholders || []).length > 0; - return ( - - - - - - - - ); - })} - -
{t("bizState.enable")}{t("bizState.profiles")}{t("bizState.command")}{t("bizState.params")} -
- void toggleProfileItem(prof, e.target.checked)} - /> - -
{prof.title}
- {prof.description ?
{prof.description}
: null} -
- {prof.command_template} - - {needsBind - ? binds.length - ? binds.map((b: any) => b.value).join(", ") - : t("bizState.unbound") - : "—"} - - {needsBind && enabled && it ? ( - - ) : null} -
-
- - {bindItemId && candidates.length ? ( -
-

{t("bizState.bindTitle")}

-

- {discoverCmd} · {selectedVrfs.length} -

-
- {candidates.map((c) => ( - - ))} -
-
- - -
-
- ) : null} - -

{t("bizState.batches")}

-
- - - - - - - - - - - {batches.map((b) => ( - - + {taskTab === "profiles" ? ( + <> +
+
{t("bizState.colTime")}{t("bizState.colStatus")}{t("bizState.colRows")}{t("bizState.colActions")}
{fmtTime(b.started_at)}
+ + + + + + + + + + {collectProfiles.map((prof) => { + const it = (detail?.items || []).find( + (x: any) => x.source_profile_id === prof.profile_id, + ); + const enabled = Boolean(it?.enabled); + const binds = it?.bindings || []; + const needsBind = (prof.placeholders || []).length > 0; + return ( + - + + + - ))} - {!batches.length ? ( - - - - ) : null} - -
{t("bizState.enable")}{t("bizState.profiles")}{t("bizState.command")}{t("bizState.params")} +
- {b.status} + void toggleProfileItem(prof, e.target.checked)} + /> {b.row_count} -
- +
{prof.title}
+ {prof.description ?
{prof.description}
: null} +
+ {prof.command_template} + + {needsBind + ? binds.length + ? binds.map((b: any) => b.value).join(", ") + : t("bizState.unbound") + : "—"} + + {needsBind && enabled && it ? ( - + ) : null}
-
{t("bizState.noBatches")}
-
-
+ ); + })} + + +
- {batchDetail ? ( -
-

- {t("bizState.batchDetail")} {batchDetail.id} -

-

- {batchDetail.command_count} · {batchDetail.row_count} · {batchDetail.status} -

- {(batchDetail.lldp_neighbors || []).length ? ( -
- - - - - - - - - - - {(batchDetail.lldp_neighbors || []).slice(0, 200).map((n: any, i: number) => ( - - - - - - - ))} - -
local_ifremote_sysremote_ifremote_ip
{n.local_if}{n.remote_sys}{n.remote_if}{n.remote_ip}
-
- ) : null} - {(batchDetail.vrf_route_summary || []).length ? ( -
- - - - - - - - - - {(batchDetail.vrf_route_summary || []).map((r: any, i: number) => ( - - - - - - ))} - -
vrfsourcenetworks
{r.vrf}{r.source}{r.networks}
-
- ) : null} + {bindItemId && candidates.length ? ( +
+

{t("bizState.bindTitle")}

+

+ {discoverCmd} · {selectedVrfs.length} +

+
+ {candidates.map((c) => ( + + ))}
- ) : null} - - )} +
+ + +
+
+ ) : null} + + ) : ( +
+ + + + + + + + + + + {batches.map((b) => ( + + + + + + + ))} + {!batches.length ? ( + + + + ) : null} + +
{t("bizState.colTime")}{t("bizState.colStatus")}{t("bizState.colRows")}{t("bizState.colActions")}
{fmtTime(b.started_at)} + {b.status} + + {b.row_count} + / {b.command_count} cmd + +
+ + +
+
+
{t("bizState.noBatches")}
+
+
+ )} + + + + + + + + + + + {/* Batch workbook: one sheet per monitor metric */} + + + + {t("bizState.batchWorkbook")} · {batchDetail?.id?.slice(0, 8)}… + + + + + {batchDetail ? ( +

+ {batchDetail.status} · {batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "} + {fmtTime(batchDetail.started_at)} +

+ ) : null} + + {activeSheet ? ( + <> +
+ setSheetColumn(e.target.value)} + aria-label={t("bizState.filterColumn")} + > + + {activeSheet.columns.map((c) => ( + + ))} + + setSheetKeyword(e.target.value)} + /> + + {filteredSheetRows.length}/{activeSheet.rows.length} + +
+ +
+ + + + {activeSheet.columns.map((c) => ( + + ))} + + + + {filteredSheetRows.slice(0, 2000).map((row, i) => ( + + {activeSheet.columns.map((c) => ( + + ))} + + ))} + {!filteredSheetRows.length ? ( + + + + ) : null} + +
{c.header}
+ {c.key === "parse_status" ? ( + + {cellText(row[c.key]) || "—"} + + ) : ( + cellText(row[c.key]) || "—" + )} +
+
{t("bizState.sheetEmpty")}
+
+
+ + ) : ( +
{t("bizState.sheetEmpty")}
+ )} + +
+ {sheets.map((s) => ( + + ))} + {!sheets.length ? {t("bizState.sheetEmpty")} : null}
-
- +
+ + {batchDetail ? ( + + ) : null} + + +
); }