mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Fix ZTE BGP RR *i/RD parsing and add batch collect detail UX.
Decouple heavy CLI parse via ParsePool, keep ECMP/multi-RD keys, and expose per-command CLI lines vs DB rows with raw.txt export. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4c5738e48e
commit
05357e971e
31 changed files with 1952 additions and 269 deletions
|
|
@ -266,14 +266,19 @@ const en = {
|
|||
deselectAllVrfs: "Deselect all",
|
||||
batches: "Batches",
|
||||
viewBatch: "Open",
|
||||
batchDetail: "Detail",
|
||||
export: "Export",
|
||||
exportCommands: "Export collect commands",
|
||||
exportCommandsOk: "Collect commands exported",
|
||||
exportRawLog: "Export as text",
|
||||
exportRawLogOk: "Log exported",
|
||||
noBatches: "No batches yet",
|
||||
batchDetail: "Batch detail",
|
||||
batchCollectDetail: "Collect detail",
|
||||
batchWorkbook: "Batch workbook",
|
||||
batchId: "Batch ID",
|
||||
copyBatchId: "Copy batch ID",
|
||||
batchMessageEmpty: "No message or error",
|
||||
batchCmdSummary: "{{ok}} ok / {{fail}} failed / {{other}} other · {{total}} commands",
|
||||
sheetCommands: "Commands",
|
||||
sheetLldp: "LLDP neighbors",
|
||||
sheetVrfRoute: "VRF route summary",
|
||||
|
|
@ -292,6 +297,8 @@ const en = {
|
|||
viewRawLog: "Raw log",
|
||||
rawLogTitle: "Raw collect log",
|
||||
rawLogEmpty: "No raw output",
|
||||
rawLogStats: "{{lines}} lines collected · {{rows}} rows stored",
|
||||
openWorkbook: "Open workbook",
|
||||
filterColumn: "Column",
|
||||
filterAllCols: "All columns",
|
||||
listFilterPh: "Filter NE / IP / vendor / status",
|
||||
|
|
@ -306,7 +313,8 @@ const en = {
|
|||
colStarted: "Started",
|
||||
colEnded: "Ended",
|
||||
colTime: "Time",
|
||||
colRows: "Rows",
|
||||
colRows: "DB rows",
|
||||
colRawLines: "CLI lines",
|
||||
colActions: "Actions",
|
||||
colCommand: "Command",
|
||||
colMessage: "Message",
|
||||
|
|
|
|||
|
|
@ -266,14 +266,19 @@ const zh = {
|
|||
deselectAllVrfs: "取消全选",
|
||||
batches: "采集批次",
|
||||
viewBatch: "查看",
|
||||
batchDetail: "详情",
|
||||
export: "导出",
|
||||
exportCommands: "导出采集命令",
|
||||
exportCommandsOk: "已导出采集命令",
|
||||
exportRawLog: "导出为文本",
|
||||
exportRawLogOk: "日志已导出",
|
||||
noBatches: "尚无批次",
|
||||
batchDetail: "批次详情",
|
||||
batchCollectDetail: "采集详情",
|
||||
batchWorkbook: "批次工作簿",
|
||||
batchId: "批次 ID",
|
||||
copyBatchId: "复制批次 ID",
|
||||
batchMessageEmpty: "无额外说明或报错",
|
||||
batchCmdSummary: "命令 {{ok}} 成功 / {{fail}} 失败 / {{other}} 其他 · 共 {{total}} 条",
|
||||
sheetCommands: "命令",
|
||||
sheetLldp: "LLDP 邻居",
|
||||
sheetVrfRoute: "VRF 路由摘要",
|
||||
|
|
@ -292,6 +297,8 @@ const zh = {
|
|||
viewRawLog: "原始日志",
|
||||
rawLogTitle: "原始采集日志",
|
||||
rawLogEmpty: "无原始输出",
|
||||
rawLogStats: "采集 {{lines}} 行 · 入库 {{rows}} 条",
|
||||
openWorkbook: "打开工作簿",
|
||||
filterColumn: "筛选列",
|
||||
filterAllCols: "全部列",
|
||||
listFilterPh: "筛选网元 / IP / 厂商 / 状态",
|
||||
|
|
@ -306,7 +313,8 @@ const zh = {
|
|||
colStarted: "开始时间",
|
||||
colEnded: "结束时间",
|
||||
colTime: "时间",
|
||||
colRows: "行数",
|
||||
colRows: "入库条数",
|
||||
colRawLines: "采集行数",
|
||||
colActions: "操作",
|
||||
colCommand: "命令",
|
||||
colMessage: "说明",
|
||||
|
|
|
|||
|
|
@ -10085,6 +10085,71 @@ html.login-page--paused .login-page__flare {
|
|||
min-height: min(50vh, 480px);
|
||||
}
|
||||
|
||||
.bs-rawlog-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.bs-batch-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
.bs-batch-message__label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.bs-batch-message__body {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.bs-batch-message--alert {
|
||||
border-color: rgba(248, 113, 113, 0.28);
|
||||
background: rgba(127, 29, 29, 0.22);
|
||||
}
|
||||
|
||||
.bs-batch-message--alert .bs-batch-message__label {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.bs-batch-message--alert .bs-batch-message__body {
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.bs-msg-cell {
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-cmd-stat {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bs-collect-detail-modal .bs-sheet-table {
|
||||
max-height: min(48vh, 480px);
|
||||
}
|
||||
|
||||
.bs-rawlog-pre {
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
bizStateDeleteBatch,
|
||||
bizStateDeleteTask,
|
||||
bizStateDiscover,
|
||||
bizStateDownloadCommandRaw,
|
||||
bizStateDownloadExport,
|
||||
bizStateDownloadTaskCommands,
|
||||
bizStateGetBatch,
|
||||
|
|
@ -86,6 +87,7 @@ type BatchRow = {
|
|||
status: string;
|
||||
row_count: number;
|
||||
command_count: number;
|
||||
message?: string;
|
||||
started_at?: string | null;
|
||||
ended_at?: string | null;
|
||||
is_baseline?: boolean;
|
||||
|
|
@ -108,6 +110,7 @@ type SheetCmd = {
|
|||
raw_command: string;
|
||||
parse_status?: string;
|
||||
row_count?: number;
|
||||
raw_line_count?: number;
|
||||
message?: string;
|
||||
has_raw?: boolean;
|
||||
profile_id?: string;
|
||||
|
|
@ -305,6 +308,13 @@ export function BizStatePage() {
|
|||
const [rawLogCmd, setRawLogCmd] = useState("");
|
||||
const [rawLogText, setRawLogText] = useState("");
|
||||
const [rawLogMeta, setRawLogMeta] = useState("");
|
||||
const [rawLogCommandId, setRawLogCommandId] = useState("");
|
||||
const [rawLogLines, setRawLogLines] = useState(0);
|
||||
const [rawLogRows, setRawLogRows] = useState(0);
|
||||
const [rawLogMessage, setRawLogMessage] = useState("");
|
||||
/** Collect status / errors detail (lighter than workbook). */
|
||||
const [collectDetail, setCollectDetail] = useState<any>(null);
|
||||
const [collectDetailLoading, setCollectDetailLoading] = useState(false);
|
||||
|
||||
const refreshTasks = useCallback(async () => {
|
||||
const purpose =
|
||||
|
|
@ -460,6 +470,7 @@ export function BizStatePage() {
|
|||
{ key: "raw_command", header: t("bizState.colCommand") },
|
||||
{ key: "metric_id", header: "metric" },
|
||||
{ key: "parse_status", header: t("bizState.colStatus") },
|
||||
{ key: "raw_line_count", header: t("bizState.colRawLines") },
|
||||
{ key: "row_count", header: t("bizState.colRows") },
|
||||
{ key: "message", header: t("bizState.colMessage") },
|
||||
{ key: "_actions", header: t("bizState.colActions") },
|
||||
|
|
@ -483,7 +494,7 @@ export function BizStatePage() {
|
|||
: cmds.filter((row) => {
|
||||
const keys = sheetColumn
|
||||
? [sheetColumn]
|
||||
: ["raw_command", "metric_id", "parse_status", "row_count", "message"];
|
||||
: ["raw_command", "metric_id", "parse_status", "raw_line_count", "row_count", "message"];
|
||||
return keys.some((k) => cellText(row[k]).toLowerCase().includes(kw));
|
||||
});
|
||||
const start = (sheetPage - 1) * sheetPageSize;
|
||||
|
|
@ -509,13 +520,27 @@ export function BizStatePage() {
|
|||
return cmds.filter((row) => {
|
||||
const keys = sheetColumn
|
||||
? [sheetColumn]
|
||||
: ["raw_command", "metric_id", "parse_status", "row_count", "message"];
|
||||
: ["raw_command", "metric_id", "parse_status", "raw_line_count", "row_count", "message"];
|
||||
return keys.some((k) => cellText(row[k]).toLowerCase().includes(kw));
|
||||
}).length;
|
||||
}
|
||||
return sheetTotal;
|
||||
}, [activeSheet, batchDetail, debouncedSheetKw, sheetColumn, sheetTotal]);
|
||||
|
||||
const collectDetailCmdSummary = useMemo(() => {
|
||||
const cmds = (collectDetail?.commands || []) as SheetCmd[];
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
let other = 0;
|
||||
for (const c of cmds) {
|
||||
const st = String(c.parse_status || "").toLowerCase();
|
||||
if (st === "ok" || st === "success" || st === "aux" || st === "aux_ok" || st === "aux_cached") ok += 1;
|
||||
else if (st === "failed" || st === "error" || st === "fail") fail += 1;
|
||||
else other += 1;
|
||||
}
|
||||
return { ok, fail, other, total: cmds.length };
|
||||
}, [collectDetail]);
|
||||
|
||||
const loadSheetPage = useCallback(
|
||||
async (batchId: string, metricId: string, page: number, pageSize: number, kw: string, column: string) => {
|
||||
if (!batchId || !metricId || metricId === "commands") return;
|
||||
|
|
@ -1067,23 +1092,52 @@ export function BizStatePage() {
|
|||
setSheetTotal(0);
|
||||
setRawLogOpen(false);
|
||||
setRawLogText("");
|
||||
setRawLogCommandId("");
|
||||
};
|
||||
|
||||
const openRawLog = async (commandId: string) => {
|
||||
if (!batchDetail?.id || !commandId) return;
|
||||
const openCollectDetail = async (batchId: string) => {
|
||||
setCollectDetailLoading(true);
|
||||
setCollectDetail(null);
|
||||
try {
|
||||
const d = await bizStateGetBatch(batchId);
|
||||
setCollectDetail(d);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setCollectDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeCollectDetail = () => {
|
||||
setCollectDetail(null);
|
||||
setCollectDetailLoading(false);
|
||||
};
|
||||
|
||||
const openRawLog = async (commandId: string, fromBatchId?: string) => {
|
||||
const bid = fromBatchId || (batchDetail?.id ? String(batchDetail.id) : "") || (collectDetail?.id ? String(collectDetail.id) : "");
|
||||
if (!bid || !commandId) return;
|
||||
setRawLogOpen(true);
|
||||
setRawLogLoading(true);
|
||||
setRawLogText("");
|
||||
setRawLogCmd("");
|
||||
setRawLogMeta("");
|
||||
setRawLogCommandId(commandId);
|
||||
setRawLogLines(0);
|
||||
setRawLogRows(0);
|
||||
setRawLogMessage("");
|
||||
try {
|
||||
const d = await bizStateGetBatchCommand(String(batchDetail.id), commandId);
|
||||
const d = await bizStateGetBatchCommand(bid, commandId);
|
||||
setRawLogCmd(String(d.raw_command || ""));
|
||||
setRawLogText(String(d.raw_text || ""));
|
||||
const lines = Number(d.raw_line_count ?? 0);
|
||||
const rows = Number(d.row_count ?? 0);
|
||||
setRawLogLines(lines);
|
||||
setRawLogRows(rows);
|
||||
setRawLogMessage(String(d.message || ""));
|
||||
const bits = [
|
||||
d.parse_status,
|
||||
d.metric_id,
|
||||
d.row_count != null ? `${d.row_count} rows` : "",
|
||||
t("bizState.rawLogStats", { lines, rows }),
|
||||
d.collected_at ? fmtTime(d.collected_at) : "",
|
||||
].filter(Boolean);
|
||||
setRawLogMeta(bits.join(" · "));
|
||||
|
|
@ -1095,6 +1149,35 @@ export function BizStatePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const exportRawLog = async () => {
|
||||
const bid =
|
||||
(batchDetail?.id && String(batchDetail.id)) ||
|
||||
(collectDetail?.id && String(collectDetail.id)) ||
|
||||
"";
|
||||
if (!bid || !rawLogCommandId) {
|
||||
// Fallback: download already-loaded text
|
||||
if (!rawLogText) return;
|
||||
const safe = (rawLogCmd || "command").replace(/[^\w.-]+/g, "_").slice(0, 80);
|
||||
const blob = new Blob([rawLogText], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${safe || "command"}.txt`;
|
||||
a.click();
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await bizStateDownloadCommandRaw(bid, rawLogCommandId);
|
||||
showOk(t("bizState.exportRawLogOk"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
}
|
||||
};
|
||||
|
||||
const selectSheet = (id: string) => {
|
||||
setSheetId(id);
|
||||
setSheetKeyword("");
|
||||
|
|
@ -1785,6 +1868,9 @@ export function BizStatePage() {
|
|||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
<Button size="sm" variant="secondary" onPress={() => void openCollectDetail(b.id)}>
|
||||
{t("bizState.batchDetail")}
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" onPress={() => void openBatch(b.id)}>
|
||||
{t("bizState.viewBatch")}
|
||||
</Button>
|
||||
|
|
@ -2079,6 +2165,21 @@ export function BizStatePage() {
|
|||
{t("bizState.setAlias")}
|
||||
</Button>
|
||||
</div>
|
||||
{batchDetail.message ? (
|
||||
<div
|
||||
className={`bs-batch-message${
|
||||
["failed", "partial", "error", "cancelled"].includes(
|
||||
String(batchDetail.status || "").toLowerCase(),
|
||||
)
|
||||
? " bs-batch-message--alert"
|
||||
: ""
|
||||
}`}
|
||||
role="status"
|
||||
>
|
||||
<span className="bs-batch-message__label">{t("bizState.colMessage")}</span>
|
||||
<pre className="bs-batch-message__body">{String(batchDetail.message)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
@ -2111,6 +2212,12 @@ export function BizStatePage() {
|
|||
{c.raw_command || "—"}
|
||||
</code>
|
||||
<div className="bs-sheet-cmd-actions">
|
||||
<span className="muted bs-cmd-stat">
|
||||
{t("bizState.rawLogStats", {
|
||||
lines: Number(c.raw_line_count ?? 0),
|
||||
rows: Number(c.row_count ?? 0),
|
||||
})}
|
||||
</span>
|
||||
{c.parse_status ? (
|
||||
<NmStatusChip color={jobChipColor(String(c.parse_status))}>
|
||||
{c.parse_status}
|
||||
|
|
@ -2177,14 +2284,29 @@ export function BizStatePage() {
|
|||
{displayColumns.map((c) => (
|
||||
<td key={c.key}>
|
||||
{c.key === "_actions" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={!row.has_raw}
|
||||
onPress={() => void openRawLog(String(row.id || ""))}
|
||||
>
|
||||
{t("bizState.viewRawLog")}
|
||||
</Button>
|
||||
<div className="pt-list-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={!row.has_raw}
|
||||
onPress={() => void openRawLog(String(row.id || ""))}
|
||||
>
|
||||
{t("bizState.viewRawLog")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={!row.has_raw || !batchDetail?.id}
|
||||
onPress={() =>
|
||||
void bizStateDownloadCommandRaw(
|
||||
String(batchDetail.id),
|
||||
String(row.id || ""),
|
||||
).catch((e) => showError(formatErr(e)))
|
||||
}
|
||||
>
|
||||
{t("bizState.exportRawLog")}
|
||||
</Button>
|
||||
</div>
|
||||
) : c.key === "parse_status" ? (
|
||||
<NmStatusChip color={jobChipColor(cellText(row[c.key]))}>
|
||||
{cellText(row[c.key]) || "—"}
|
||||
|
|
@ -2259,7 +2381,18 @@ export function BizStatePage() {
|
|||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-2 bs-rawlog-body">
|
||||
{rawLogCmd ? <code className="bs-sheet-cmd-code bs-sheet-cmd-code--block">{rawLogCmd}</code> : null}
|
||||
{rawLogMeta ? <p className="muted">{rawLogMeta}</p> : null}
|
||||
<div className="bs-rawlog-stats">
|
||||
<NmStatusChip color="accent">
|
||||
{t("bizState.rawLogStats", { lines: rawLogLines, rows: rawLogRows })}
|
||||
</NmStatusChip>
|
||||
{rawLogMeta ? <span className="muted">{rawLogMeta}</span> : null}
|
||||
</div>
|
||||
{rawLogMessage ? (
|
||||
<div className="bs-batch-message bs-batch-message--alert" role="status">
|
||||
<span className="bs-batch-message__label">{t("bizState.colMessage")}</span>
|
||||
<pre className="bs-batch-message__body">{rawLogMessage}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{rawLogLoading ? (
|
||||
<div className="pt-list-empty muted">{t("bizState.sheetLoading")}</div>
|
||||
) : (
|
||||
|
|
@ -2267,11 +2400,166 @@ export function BizStatePage() {
|
|||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={rawLogLoading || (!rawLogText && !rawLogCommandId)}
|
||||
onPress={() => void exportRawLog()}
|
||||
>
|
||||
{t("bizState.exportRawLog")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onPress={() => setRawLogOpen(false)}>
|
||||
{t("bizState.cancel")}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AppModalShell>
|
||||
|
||||
{/* Batch collect status / errors detail */}
|
||||
<AppModalShell
|
||||
open={Boolean(collectDetail) || collectDetailLoading}
|
||||
onClose={closeCollectDetail}
|
||||
size="lg"
|
||||
className="app-heroui-modal--lg bs-collect-detail-modal"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("bizState.batchCollectDetail")}</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-2">
|
||||
{collectDetailLoading ? (
|
||||
<div className="pt-list-empty muted">{t("bizState.sheetLoading")}</div>
|
||||
) : collectDetail ? (
|
||||
<>
|
||||
<div className="bs-workbook-meta">
|
||||
<NmStatusChip color={jobChipColor(String(collectDetail.status || ""))}>
|
||||
{String(collectDetail.status || "—")}
|
||||
</NmStatusChip>
|
||||
{collectDetail.alias ? (
|
||||
<NmStatusChip color="accent">{String(collectDetail.alias)}</NmStatusChip>
|
||||
) : null}
|
||||
<span className="muted">
|
||||
{collectDetail.command_count} cmd · {collectDetail.row_count} rows ·{" "}
|
||||
{t("bizState.colStarted")} {fmtTime(collectDetail.started_at)} ·{" "}
|
||||
{t("bizState.colEnded")} {fmtTime(collectDetail.ended_at)}
|
||||
{fmtDuration(collectDetail.started_at, collectDetail.ended_at)
|
||||
? ` · ${fmtDuration(collectDetail.started_at, collectDetail.ended_at)}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`bs-batch-message${
|
||||
["failed", "partial", "error", "cancelled"].includes(
|
||||
String(collectDetail.status || "").toLowerCase(),
|
||||
)
|
||||
? " bs-batch-message--alert"
|
||||
: ""
|
||||
}`}
|
||||
role="status"
|
||||
>
|
||||
<span className="bs-batch-message__label">{t("bizState.colMessage")}</span>
|
||||
<pre className="bs-batch-message__body">
|
||||
{String(collectDetail.message || "").trim() || t("bizState.batchMessageEmpty")}
|
||||
</pre>
|
||||
</div>
|
||||
<p className="muted">
|
||||
{t("bizState.batchCmdSummary", {
|
||||
ok: collectDetailCmdSummary.ok,
|
||||
fail: collectDetailCmdSummary.fail,
|
||||
other: collectDetailCmdSummary.other,
|
||||
total: collectDetailCmdSummary.total,
|
||||
})}
|
||||
</p>
|
||||
<div className="pt-list-table-wrap bs-sheet-table">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("bizState.colCommand")}</th>
|
||||
<th>{t("bizState.colStatus")}</th>
|
||||
<th>{t("bizState.colRawLines")}</th>
|
||||
<th>{t("bizState.colRows")}</th>
|
||||
<th>{t("bizState.colMessage")}</th>
|
||||
<th>{t("bizState.colActions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{((collectDetail.commands || []) as SheetCmd[]).map((c) => (
|
||||
<tr key={c.id}>
|
||||
<td>
|
||||
<code className="bs-inline-cmd">{c.raw_command || "—"}</code>
|
||||
</td>
|
||||
<td>
|
||||
<NmStatusChip color={jobChipColor(String(c.parse_status || ""))}>
|
||||
{c.parse_status || "—"}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-num">{Number(c.raw_line_count ?? 0)}</td>
|
||||
<td className="pt-list-num">{Number(c.row_count ?? 0)}</td>
|
||||
<td className="bs-msg-cell" title={c.message || ""}>
|
||||
{c.message?.trim() ? c.message : "—"}
|
||||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={!c.has_raw}
|
||||
onPress={() => void openRawLog(c.id, String(collectDetail.id))}
|
||||
>
|
||||
{t("bizState.viewRawLog")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={!c.has_raw}
|
||||
onPress={() =>
|
||||
void bizStateDownloadCommandRaw(String(collectDetail.id), c.id).catch((e) =>
|
||||
showError(formatErr(e)),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("bizState.exportRawLog")}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!((collectDetail.commands || []) as SheetCmd[]).length ? (
|
||||
<tr>
|
||||
<td colSpan={6}>
|
||||
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{collectDetail?.id ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
const id = String(collectDetail.id);
|
||||
closeCollectDetail();
|
||||
void openBatch(id);
|
||||
}}
|
||||
>
|
||||
{t("bizState.openWorkbook")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onPress={() => void bizStateDownloadExport(String(collectDetail.id))}>
|
||||
{t("bizState.export")}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button size="sm" variant="ghost" onPress={closeCollectDetail}>
|
||||
{t("bizState.cancel")}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AppModalShell>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1907,6 +1907,7 @@ export const bizStateGetBatchCommand = (batchId: string, commandId: string) =>
|
|||
metric_id: string;
|
||||
parse_status: string;
|
||||
row_count: number;
|
||||
raw_line_count?: number;
|
||||
message: string;
|
||||
raw_text: string;
|
||||
collected_at?: string | null;
|
||||
|
|
@ -1915,6 +1916,31 @@ export const bizStateGetBatchCommand = (batchId: string, commandId: string) =>
|
|||
`/v1/biz-state/batches/${encodeURIComponent(batchId)}/commands/${encodeURIComponent(commandId)}`,
|
||||
);
|
||||
|
||||
/** Download one command's raw CLI output as .txt */
|
||||
export const bizStateDownloadCommandRaw = async (batchId: string, commandId: string): Promise<void> => {
|
||||
const path =
|
||||
`/v1/biz-state/batches/${encodeURIComponent(batchId)}/commands/${encodeURIComponent(commandId)}/raw.txt`;
|
||||
const res = await fetch(path, { method: "GET", credentials: fetchCreds, headers: authHeaders() });
|
||||
if (res.status === 401) {
|
||||
handleUnauthorized(path);
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
if (!res.ok) throw new Error(`${res.status} download raw`);
|
||||
const blob = await res.blob();
|
||||
const cd = res.headers.get("Content-Disposition") || "";
|
||||
const m = /filename="?([^";]+)"?/i.exec(cd);
|
||||
const filename = m?.[1] || `biz_state_${commandId}.txt`;
|
||||
const url = URL.createObjectURL(blob);
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const bizStateDownloadExport = async (batchId: string): Promise<void> => {
|
||||
const path = `/v1/biz-state/batches/${encodeURIComponent(batchId)}/export`;
|
||||
const res = await fetch(path, { method: "GET", credentials: fetchCreds, headers: authHeaders() });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue