mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 04:00:47 +08:00
Use modals and Excel-like metric sheets for biz_state batches.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1352535a9b
commit
0a967b4172
4 changed files with 733 additions and 342 deletions
|
|
@ -183,27 +183,39 @@ const en = {
|
||||||
started: "Schedule started",
|
started: "Schedule started",
|
||||||
paused: "Paused",
|
paused: "Paused",
|
||||||
collecting: "Collecting",
|
collecting: "Collecting",
|
||||||
|
detail: "Details",
|
||||||
profiles: "Monitor items",
|
profiles: "Monitor items",
|
||||||
enable: "On",
|
enable: "On",
|
||||||
command: "Command template",
|
command: "Command template",
|
||||||
params: "Bindings",
|
params: "Bindings",
|
||||||
discoverVrf: "Discover VRFs",
|
discoverVrf: "Discover VRFs",
|
||||||
saveBindings: "Save bindings",
|
saveBindings: "Save bindings",
|
||||||
cancel: "Cancel",
|
cancel: "Close",
|
||||||
bindTitle: "Select VRF bindings",
|
bindTitle: "Select VRF bindings",
|
||||||
unbound: "Not bound",
|
unbound: "Not bound",
|
||||||
batches: "Batches",
|
batches: "Batches",
|
||||||
viewBatch: "View",
|
viewBatch: "Open",
|
||||||
export: "Export",
|
export: "Export",
|
||||||
noBatches: "No batches yet",
|
noBatches: "No batches yet",
|
||||||
batchDetail: "Batch detail",
|
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",
|
colNe: "NE",
|
||||||
colStatus: "Status",
|
colStatus: "Status",
|
||||||
colLast: "Last collect",
|
colLast: "Last collect",
|
||||||
colTime: "Time",
|
colTime: "Time",
|
||||||
colRows: "Rows",
|
colRows: "Rows",
|
||||||
colActions: "Actions",
|
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",
|
noNeedBind: "This item needs no parameter bindings",
|
||||||
discoverFailed: "Discover failed",
|
discoverFailed: "Discover failed",
|
||||||
bindingsSaved: "Bindings saved",
|
bindingsSaved: "Bindings saved",
|
||||||
|
|
|
||||||
|
|
@ -183,13 +183,14 @@ const zh = {
|
||||||
started: "已启动周期采集",
|
started: "已启动周期采集",
|
||||||
paused: "已暂停",
|
paused: "已暂停",
|
||||||
collecting: "采集中",
|
collecting: "采集中",
|
||||||
|
detail: "详情",
|
||||||
profiles: "监控项",
|
profiles: "监控项",
|
||||||
enable: "启用",
|
enable: "启用",
|
||||||
command: "命令模板",
|
command: "命令模板",
|
||||||
params: "参数关联",
|
params: "参数关联",
|
||||||
discoverVrf: "发现 VRF",
|
discoverVrf: "发现 VRF",
|
||||||
saveBindings: "保存关联",
|
saveBindings: "保存关联",
|
||||||
cancel: "取消",
|
cancel: "关闭",
|
||||||
bindTitle: "选择 VRF 绑定",
|
bindTitle: "选择 VRF 绑定",
|
||||||
unbound: "未关联",
|
unbound: "未关联",
|
||||||
batches: "采集批次",
|
batches: "采集批次",
|
||||||
|
|
@ -197,13 +198,24 @@ const zh = {
|
||||||
export: "导出",
|
export: "导出",
|
||||||
noBatches: "尚无批次",
|
noBatches: "尚无批次",
|
||||||
batchDetail: "批次详情",
|
batchDetail: "批次详情",
|
||||||
|
batchWorkbook: "批次工作簿",
|
||||||
|
sheetCommands: "命令",
|
||||||
|
sheetLldp: "LLDP 邻居",
|
||||||
|
sheetVrfRoute: "VRF 路由摘要",
|
||||||
|
sheetFilterPh: "筛选当前表…",
|
||||||
|
sheetEmpty: "本表无数据",
|
||||||
|
filterColumn: "筛选列",
|
||||||
|
filterAllCols: "全部列",
|
||||||
|
listFilterPh: "筛选网元 / IP / 厂商 / 状态",
|
||||||
colNe: "网元",
|
colNe: "网元",
|
||||||
colStatus: "状态",
|
colStatus: "状态",
|
||||||
colLast: "最近采集",
|
colLast: "最近采集",
|
||||||
colTime: "时间",
|
colTime: "时间",
|
||||||
colRows: "行数",
|
colRows: "行数",
|
||||||
colActions: "操作",
|
colActions: "操作",
|
||||||
pickTask: "选择左侧任务查看详情",
|
colCommand: "命令",
|
||||||
|
colMessage: "说明",
|
||||||
|
pickTask: "打开详情查看任务",
|
||||||
noNeedBind: "该监控项无需参数关联",
|
noNeedBind: "该监控项无需参数关联",
|
||||||
discoverFailed: "发现失败",
|
discoverFailed: "发现失败",
|
||||||
bindingsSaved: "参数关联已保存",
|
bindingsSaved: "参数关联已保存",
|
||||||
|
|
|
||||||
|
|
@ -9917,6 +9917,82 @@ html.login-page--paused .login-page__flare {
|
||||||
margin-bottom: 10px;
|
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 {
|
.nm-config-modal__body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
|
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
||||||
import { useToast } from "../../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
|
|
@ -34,12 +36,7 @@ type TaskRow = {
|
||||||
last_collect_ended_at?: string | null;
|
last_collect_ended_at?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Placeholder = {
|
type Placeholder = { name: string };
|
||||||
name: string;
|
|
||||||
bind_mode?: string;
|
|
||||||
discover_profile_id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Profile = {
|
type Profile = {
|
||||||
profile_id: string;
|
profile_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -60,28 +57,127 @@ type BatchRow = {
|
||||||
|
|
||||||
type Candidate = { value: string; label: string; rd?: string };
|
type Candidate = { value: string; label: string; rd?: string };
|
||||||
|
|
||||||
|
type SheetCol = { key: string; header: string };
|
||||||
|
type SheetDef = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
columns: SheetCol[];
|
||||||
|
rows: Record<string, unknown>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskTab = "profiles" | "batches";
|
||||||
|
|
||||||
function fmtTime(v?: string | null) {
|
function fmtTime(v?: string | null) {
|
||||||
if (!v) return "—";
|
if (!v) return "—";
|
||||||
return formatSystemTime(v) || v;
|
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<string, unknown>[],
|
||||||
|
columns: SheetCol[],
|
||||||
|
keyword: string,
|
||||||
|
columnKey: string,
|
||||||
|
): Record<string, unknown>[] {
|
||||||
|
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() {
|
export function BizStatePage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showOk, showError } = useToast();
|
const { showOk, showError } = useToast();
|
||||||
|
|
||||||
const [tasks, setTasks] = useState<TaskRow[]>([]);
|
const [tasks, setTasks] = useState<TaskRow[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState("");
|
|
||||||
const [detail, setDetail] = useState<any>(null);
|
|
||||||
const [batches, setBatches] = useState<BatchRow[]>([]);
|
|
||||||
const [batchDetail, setBatchDetail] = useState<any>(null);
|
|
||||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
|
||||||
const [nes, setNes] = useState<CliTargetItem[]>([]);
|
const [nes, setNes] = useState<CliTargetItem[]>([]);
|
||||||
const [neId, setNeId] = useState("");
|
const [neId, setNeId] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [listKeyword, setListKeyword] = useState("");
|
||||||
|
const debouncedListKw = useDebouncedValue(listKeyword, 250);
|
||||||
|
|
||||||
|
// task modal
|
||||||
|
const [taskId, setTaskId] = useState("");
|
||||||
|
const [detail, setDetail] = useState<any>(null);
|
||||||
|
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||||
|
const [batches, setBatches] = useState<BatchRow[]>([]);
|
||||||
|
const [taskTab, setTaskTab] = useState<TaskTab>("profiles");
|
||||||
|
|
||||||
|
// VRF bind (inside task modal)
|
||||||
const [bindItemId, setBindItemId] = useState("");
|
const [bindItemId, setBindItemId] = useState("");
|
||||||
const [candidates, setCandidates] = useState<Candidate[]>([]);
|
const [candidates, setCandidates] = useState<Candidate[]>([]);
|
||||||
const [selectedVrfs, setSelectedVrfs] = useState<string[]>([]);
|
const [selectedVrfs, setSelectedVrfs] = useState<string[]>([]);
|
||||||
const [discoverCmd, setDiscoverCmd] = useState("");
|
const [discoverCmd, setDiscoverCmd] = useState("");
|
||||||
|
|
||||||
|
// batch workbook modal
|
||||||
|
const [batchDetail, setBatchDetail] = useState<any>(null);
|
||||||
|
const [sheetId, setSheetId] = useState("");
|
||||||
|
const [sheetKeyword, setSheetKeyword] = useState("");
|
||||||
|
const [sheetColumn, setSheetColumn] = useState("");
|
||||||
|
const debouncedSheetKw = useDebouncedValue(sheetKeyword, 200);
|
||||||
|
|
||||||
const refreshTasks = useCallback(async () => {
|
const refreshTasks = useCallback(async () => {
|
||||||
const res = await bizStateListTasks();
|
const res = await bizStateListTasks();
|
||||||
setTasks((res.items || []) as TaskRow[]);
|
setTasks((res.items || []) as TaskRow[]);
|
||||||
|
|
@ -105,18 +201,41 @@ export function BizStatePage() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount once
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount once
|
||||||
}, [refreshTasks]);
|
}, [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(
|
const collectProfiles = useMemo(
|
||||||
() => profiles.filter((p) => (p.kind || "collect") === "collect"),
|
() => profiles.filter((p) => (p.kind || "collect") === "collect"),
|
||||||
[profiles],
|
[profiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openTask = async (id: string) => {
|
const sheets = useMemo(
|
||||||
setSelectedId(id);
|
() => (batchDetail ? buildBatchSheets(batchDetail, t) : []),
|
||||||
setBatchDetail(null);
|
[batchDetail, t],
|
||||||
setBindItemId("");
|
);
|
||||||
setCandidates([]);
|
|
||||||
setSelectedVrfs([]);
|
const activeSheet = useMemo(() => {
|
||||||
try {
|
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);
|
const task = await bizStateGetTask(id);
|
||||||
setDetail(task);
|
setDetail(task);
|
||||||
const b = await bizStateListBatches(id);
|
const b = await bizStateListBatches(id);
|
||||||
|
|
@ -126,11 +245,29 @@ export function BizStatePage() {
|
||||||
device_type: task.device_type || "",
|
device_type: task.device_type || "",
|
||||||
});
|
});
|
||||||
setProfiles((p.items || []) as Profile[]);
|
setProfiles((p.items || []) as Profile[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTask = async (id: string, tab: TaskTab = "profiles") => {
|
||||||
|
setTaskId(id);
|
||||||
|
setTaskTab(tab);
|
||||||
|
setBindItemId("");
|
||||||
|
setCandidates([]);
|
||||||
|
setBatchDetail(null);
|
||||||
|
try {
|
||||||
|
await loadTask(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
|
setTaskId("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeTask = () => {
|
||||||
|
setTaskId("");
|
||||||
|
setDetail(null);
|
||||||
|
setBindItemId("");
|
||||||
|
setCandidates([]);
|
||||||
|
};
|
||||||
|
|
||||||
const createTask = async () => {
|
const createTask = async () => {
|
||||||
if (!neId) return;
|
if (!neId) return;
|
||||||
const ne = nes.find((n) => n.id === neId);
|
const ne = nes.find((n) => n.id === neId);
|
||||||
|
|
@ -146,9 +283,9 @@ export function BizStatePage() {
|
||||||
device_type: ne.device_type,
|
device_type: ne.device_type,
|
||||||
});
|
});
|
||||||
showOk(t("bizState.created"));
|
showOk(t("bizState.created"));
|
||||||
await refreshTasks();
|
|
||||||
await openTask(String(task.id));
|
|
||||||
setNeId("");
|
setNeId("");
|
||||||
|
await refreshTasks();
|
||||||
|
await openTask(String(task.id), "profiles");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(t("bizState.createFailed") + ": " + formatErr(e));
|
showError(t("bizState.createFailed") + ": " + formatErr(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -157,12 +294,12 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setStatus = async (status: string) => {
|
const setStatus = async (status: string) => {
|
||||||
if (!selectedId) return;
|
if (!taskId) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStatePatchTask(selectedId, { status });
|
await bizStatePatchTask(taskId, { status });
|
||||||
showOk(status === "running" ? t("bizState.started") : t("bizState.paused"));
|
showOk(status === "running" ? t("bizState.started") : t("bizState.paused"));
|
||||||
await openTask(selectedId);
|
await loadTask(taskId);
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
|
|
@ -172,23 +309,19 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const collectNow = async () => {
|
const collectNow = async () => {
|
||||||
if (!selectedId) return;
|
if (!taskId) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStateCollectNow(selectedId);
|
await bizStateCollectNow(taskId);
|
||||||
await openTask(selectedId);
|
|
||||||
await refreshTasks();
|
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
const task = await bizStateGetTask(selectedId);
|
const task = await bizStateGetTask(taskId);
|
||||||
setDetail(task);
|
setDetail(task);
|
||||||
if (!task.collect_running) {
|
if (!task.collect_running) break;
|
||||||
const b = await bizStateListBatches(selectedId);
|
|
||||||
setBatches((b.items || []) as BatchRow[]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await loadTask(taskId);
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
|
setTaskTab("batches");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -196,26 +329,13 @@ export function BizStatePage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openBatch = async (batchId: string) => {
|
const removeTask = async (id: string) => {
|
||||||
try {
|
|
||||||
const d = await bizStateGetBatch(batchId);
|
|
||||||
setBatchDetail(d);
|
|
||||||
} catch (e) {
|
|
||||||
showError(formatErr(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTask = async () => {
|
|
||||||
if (!selectedId) return;
|
|
||||||
if (!window.confirm(t("bizState.confirmDelete"))) return;
|
if (!window.confirm(t("bizState.confirmDelete"))) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStateDeleteTask(selectedId);
|
await bizStateDeleteTask(id);
|
||||||
showOk(t("bizState.deleted"));
|
showOk(t("bizState.deleted"));
|
||||||
setSelectedId("");
|
if (taskId === id) closeTask();
|
||||||
setDetail(null);
|
|
||||||
setBatches([]);
|
|
||||||
setBatchDetail(null);
|
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
|
|
@ -225,7 +345,7 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleProfileItem = async (profile: Profile, enable: boolean) => {
|
const toggleProfileItem = async (profile: Profile, enable: boolean) => {
|
||||||
if (!selectedId || !detail) return;
|
if (!taskId || !detail) return;
|
||||||
const items = [...(detail.items || [])];
|
const items = [...(detail.items || [])];
|
||||||
const idx = items.findIndex((it: any) => it.source_profile_id === profile.profile_id);
|
const idx = items.findIndex((it: any) => it.source_profile_id === profile.profile_id);
|
||||||
if (enable) {
|
if (enable) {
|
||||||
|
|
@ -244,8 +364,8 @@ export function BizStatePage() {
|
||||||
}
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStatePatchTask(selectedId, { items });
|
await bizStatePatchTask(taskId, { items });
|
||||||
await openTask(selectedId);
|
await loadTask(taskId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -254,7 +374,7 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const startDiscover = async (item: any) => {
|
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 prof = profiles.find((p) => p.profile_id === item.source_profile_id);
|
||||||
const ph = (prof?.placeholders || [])[0];
|
const ph = (prof?.placeholders || [])[0];
|
||||||
if (!ph) {
|
if (!ph) {
|
||||||
|
|
@ -265,7 +385,7 @@ export function BizStatePage() {
|
||||||
setBindItemId(item.id);
|
setBindItemId(item.id);
|
||||||
try {
|
try {
|
||||||
const res = await bizStateDiscover({
|
const res = await bizStateDiscover({
|
||||||
task_id: selectedId,
|
task_id: taskId,
|
||||||
collect_profile_id: item.source_profile_id,
|
collect_profile_id: item.source_profile_id,
|
||||||
placeholder: ph.name,
|
placeholder: ph.name,
|
||||||
});
|
});
|
||||||
|
|
@ -289,19 +409,19 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveBindings = async () => {
|
const saveBindings = async () => {
|
||||||
if (!selectedId || !bindItemId) return;
|
if (!taskId || !bindItemId) return;
|
||||||
const item = (detail?.items || []).find((it: any) => it.id === bindItemId);
|
const item = (detail?.items || []).find((it: any) => it.id === bindItemId);
|
||||||
const prof = profiles.find((p) => p.profile_id === item?.source_profile_id);
|
const prof = profiles.find((p) => p.profile_id === item?.source_profile_id);
|
||||||
const phName = (prof?.placeholders || [])[0]?.name || "vrf";
|
const phName = (prof?.placeholders || [])[0]?.name || "vrf";
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStateSetBindings(
|
await bizStateSetBindings(
|
||||||
selectedId,
|
taskId,
|
||||||
bindItemId,
|
bindItemId,
|
||||||
selectedVrfs.map((v) => ({ placeholder: phName, value: v })),
|
selectedVrfs.map((v) => ({ placeholder: phName, value: v })),
|
||||||
);
|
);
|
||||||
showOk(t("bizState.bindingsSaved"));
|
showOk(t("bizState.bindingsSaved"));
|
||||||
await openTask(selectedId);
|
await loadTask(taskId);
|
||||||
setBindItemId("");
|
setBindItemId("");
|
||||||
setCandidates([]);
|
setCandidates([]);
|
||||||
} catch (e) {
|
} 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;
|
const runningCount = tasks.filter((x) => x.status === "running").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -348,7 +488,14 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nm-split" style={{ display: "grid", gridTemplateColumns: "1fr 1.45fr", gap: 16 }}>
|
<div className="filter-inline">
|
||||||
|
<Input
|
||||||
|
value={listKeyword}
|
||||||
|
placeholder={t("bizState.listFilterPh")}
|
||||||
|
onChange={(e) => setListKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table pt-list-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -356,19 +503,18 @@ export function BizStatePage() {
|
||||||
<th>{t("bizState.colNe")}</th>
|
<th>{t("bizState.colNe")}</th>
|
||||||
<th>{t("bizState.colStatus")}</th>
|
<th>{t("bizState.colStatus")}</th>
|
||||||
<th>{t("bizState.colLast")}</th>
|
<th>{t("bizState.colLast")}</th>
|
||||||
|
<th>{t("bizState.colActions")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tasks.map((row) => (
|
{filteredTasks.map((row) => (
|
||||||
<tr
|
<tr key={row.id}>
|
||||||
key={row.id}
|
|
||||||
className={selectedId === row.id ? "is-selected" : undefined}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => void openTask(row.id)}
|
|
||||||
>
|
|
||||||
<td>
|
<td>
|
||||||
<div className="pt-list-task-name">{row.ne_name || row.ne_ip || "—"}</div>
|
<div className="pt-list-task-name">{row.ne_name || row.ne_ip || "—"}</div>
|
||||||
<div className="muted">{row.vendor || "—"}</div>
|
<div className="muted">
|
||||||
|
{row.vendor || "—"} · {row.ne_ip || "—"}
|
||||||
|
</div>
|
||||||
|
{row.last_error ? <div className="form-error">{row.last_error}</div> : null}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
||||||
|
|
@ -379,11 +525,70 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="pt-list-time">{fmtTime(row.last_collect_ended_at)}</td>
|
<td className="pt-list-time">{fmtTime(row.last_collect_ended_at)}</td>
|
||||||
|
<td>
|
||||||
|
<div className="pt-list-actions">
|
||||||
|
<Button size="sm" variant="primary" onPress={() => void openTask(row.id, "profiles")}>
|
||||||
|
{t("bizState.detail")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onPress={() => void openTask(row.id, "batches")}>
|
||||||
|
{t("bizState.batches")}
|
||||||
|
</Button>
|
||||||
|
{row.status !== "running" ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={async () => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await bizStatePatchTask(row.id, { status: "running" });
|
||||||
|
showOk(t("bizState.started"));
|
||||||
|
await refreshTasks();
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bizState.start")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={async () => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await bizStatePatchTask(row.id, { status: "paused" });
|
||||||
|
showOk(t("bizState.paused"));
|
||||||
|
await refreshTasks();
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bizState.pause")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void removeTask(row.id)}
|
||||||
|
>
|
||||||
|
{t("bizState.delete")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!tasks.length ? (
|
{!filteredTasks.length ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3}>
|
<td colSpan={4}>
|
||||||
<div className="pt-list-empty">{t("bizState.empty")}</div>
|
<div className="pt-list-empty">{t("bizState.empty")}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -391,36 +596,46 @@ export function BizStatePage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Task detail modal */}
|
||||||
{!detail ? (
|
<AppModalShell open={Boolean(taskId)} onClose={closeTask} size="lg" className="app-heroui-modal--xl">
|
||||||
<div className="pt-list-empty">{t("bizState.pickTask")}</div>
|
<Modal.Header>
|
||||||
) : (
|
<Modal.Heading>
|
||||||
|
{detail?.ne_name || detail?.ne_ip || t("bizState.detail")}
|
||||||
|
{detail ? ` · ${detail.status}` : ""}
|
||||||
|
</Modal.Heading>
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="flex flex-col gap-3">
|
||||||
|
{detail ? (
|
||||||
|
<p className="muted">
|
||||||
|
{detail.vendor || "—"} · {detail.ne_ip || "—"} · interval {detail.interval_sec}s
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{detail?.last_error ? <p className="form-error">{detail.last_error}</p> : null}
|
||||||
|
|
||||||
|
<div className="btn-row nm-config-modal__tabs">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={taskTab === "profiles" ? "primary" : "secondary"}
|
||||||
|
onPress={() => setTaskTab("profiles")}
|
||||||
|
>
|
||||||
|
{t("bizState.profiles")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={taskTab === "batches" ? "primary" : "secondary"}
|
||||||
|
onPress={() => setTaskTab("batches")}
|
||||||
|
>
|
||||||
|
{t("bizState.batches")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{taskTab === "profiles" ? (
|
||||||
<>
|
<>
|
||||||
<div className="panel__toolbar" style={{ padding: 0, marginBottom: 8 }}>
|
|
||||||
<h3 style={{ margin: 0 }}>
|
|
||||||
{detail.ne_name || detail.ne_ip} · {detail.status}
|
|
||||||
</h3>
|
|
||||||
<div className="btn-row">
|
|
||||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void setStatus("running")}>
|
|
||||||
{t("bizState.start")}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void setStatus("paused")}>
|
|
||||||
{t("bizState.pause")}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void collectNow()}>
|
|
||||||
{t("bizState.collectNow")}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask()}>
|
|
||||||
{t("bizState.delete")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{detail.last_error ? <p className="form-error">{detail.last_error}</p> : null}
|
|
||||||
|
|
||||||
<h4 style={{ margin: "12px 0 8px" }}>{t("bizState.profiles")}</h4>
|
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("bizState.enable")}</th>
|
<th>{t("bizState.enable")}</th>
|
||||||
|
|
@ -432,7 +647,7 @@ export function BizStatePage() {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{collectProfiles.map((prof) => {
|
{collectProfiles.map((prof) => {
|
||||||
const it = (detail.items || []).find(
|
const it = (detail?.items || []).find(
|
||||||
(x: any) => x.source_profile_id === prof.profile_id,
|
(x: any) => x.source_profile_id === prof.profile_id,
|
||||||
);
|
);
|
||||||
const enabled = Boolean(it?.enabled);
|
const enabled = Boolean(it?.enabled);
|
||||||
|
|
@ -482,14 +697,14 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bindItemId && candidates.length ? (
|
{bindItemId && candidates.length ? (
|
||||||
<div className="panel" style={{ marginTop: 12, padding: 12 }}>
|
<div className="bs-bind-panel">
|
||||||
<h4 style={{ marginTop: 0 }}>{t("bizState.bindTitle")}</h4>
|
<h4 style={{ margin: "0 0 8px" }}>{t("bizState.bindTitle")}</h4>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
<code>{discoverCmd}</code> · {selectedVrfs.length}
|
<code>{discoverCmd}</code> · {selectedVrfs.length}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ maxHeight: 220, overflow: "auto", marginBottom: 8 }}>
|
<div className="bs-bind-list">
|
||||||
{candidates.map((c) => (
|
{candidates.map((c) => (
|
||||||
<label key={c.value} style={{ display: "block", marginBottom: 4 }}>
|
<label key={c.value} className="bs-bind-item">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedVrfs.includes(c.value)}
|
checked={selectedVrfs.includes(c.value)}
|
||||||
|
|
@ -506,7 +721,7 @@ export function BizStatePage() {
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="btn-row">
|
<div className="btn-row" style={{ marginTop: 8 }}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -529,10 +744,10 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
<h4 style={{ margin: "16px 0 8px" }}>{t("bizState.batches")}</h4>
|
) : (
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("bizState.colTime")}</th>
|
<th>{t("bizState.colTime")}</th>
|
||||||
|
|
@ -548,17 +763,16 @@ export function BizStatePage() {
|
||||||
<td>
|
<td>
|
||||||
<NmStatusChip color={jobChipColor(b.status)}>{b.status}</NmStatusChip>
|
<NmStatusChip color={jobChipColor(b.status)}>{b.status}</NmStatusChip>
|
||||||
</td>
|
</td>
|
||||||
<td className="pt-list-num">{b.row_count}</td>
|
<td className="pt-list-num">
|
||||||
|
{b.row_count}
|
||||||
|
<span className="muted"> / {b.command_count} cmd</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="pt-list-actions">
|
<div className="pt-list-actions">
|
||||||
<Button size="sm" variant="secondary" onPress={() => void openBatch(b.id)}>
|
<Button size="sm" variant="primary" onPress={() => void openBatch(b.id)}>
|
||||||
{t("bizState.viewBatch")}
|
{t("bizState.viewBatch")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="ghost" onPress={() => void bizStateDownloadExport(b.id)}>
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onPress={() => void bizStateDownloadExport(b.id)}
|
|
||||||
>
|
|
||||||
{t("bizState.export")}
|
{t("bizState.export")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -575,68 +789,145 @@ export function BizStatePage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{batchDetail ? (
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<h4 style={{ marginBottom: 4 }}>
|
|
||||||
{t("bizState.batchDetail")} {batchDetail.id}
|
|
||||||
</h4>
|
|
||||||
<p className="muted">
|
|
||||||
{batchDetail.command_count} · {batchDetail.row_count} · {batchDetail.status}
|
|
||||||
</p>
|
|
||||||
{(batchDetail.lldp_neighbors || []).length ? (
|
|
||||||
<div className="pt-list-table-wrap" style={{ marginTop: 8 }}>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>local_if</th>
|
|
||||||
<th>remote_sys</th>
|
|
||||||
<th>remote_if</th>
|
|
||||||
<th>remote_ip</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{(batchDetail.lldp_neighbors || []).slice(0, 200).map((n: any, i: number) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>{n.local_if}</td>
|
|
||||||
<td>{n.remote_sys}</td>
|
|
||||||
<td>{n.remote_if}</td>
|
|
||||||
<td>{n.remote_ip}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{(batchDetail.vrf_route_summary || []).length ? (
|
|
||||||
<div className="pt-list-table-wrap" style={{ marginTop: 8 }}>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>vrf</th>
|
|
||||||
<th>source</th>
|
|
||||||
<th>networks</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{(batchDetail.vrf_route_summary || []).map((r: any, i: number) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>{r.vrf}</td>
|
|
||||||
<td>{r.source}</td>
|
|
||||||
<td className="pt-list-num">{r.networks}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void setStatus("running")}>
|
||||||
|
{t("bizState.start")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void setStatus("paused")}>
|
||||||
|
{t("bizState.pause")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void collectNow()}>
|
||||||
|
{t("bizState.collectNow")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}>
|
||||||
|
{t("bizState.delete")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onPress={closeTask}>
|
||||||
|
{t("bizState.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AppModalShell>
|
||||||
|
|
||||||
|
{/* Batch workbook: one sheet per monitor metric */}
|
||||||
|
<AppModalShell
|
||||||
|
open={Boolean(batchDetail)}
|
||||||
|
onClose={closeBatch}
|
||||||
|
size="lg"
|
||||||
|
className="app-heroui-modal--xl bs-workbook-modal"
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Heading>
|
||||||
|
{t("bizState.batchWorkbook")} · {batchDetail?.id?.slice(0, 8)}…
|
||||||
|
</Modal.Heading>
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="flex flex-col gap-2 bs-workbook-body">
|
||||||
|
{batchDetail ? (
|
||||||
|
<p className="muted">
|
||||||
|
{batchDetail.status} · {batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "}
|
||||||
|
{fmtTime(batchDetail.started_at)}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeSheet ? (
|
||||||
|
<>
|
||||||
|
<div className="filter-inline bs-sheet-filter">
|
||||||
|
<FieldSelect
|
||||||
|
value={sheetColumn}
|
||||||
|
onChange={(e) => setSheetColumn(e.target.value)}
|
||||||
|
aria-label={t("bizState.filterColumn")}
|
||||||
|
>
|
||||||
|
<option value="">{t("bizState.filterAllCols")}</option>
|
||||||
|
{activeSheet.columns.map((c) => (
|
||||||
|
<option key={c.key} value={c.key}>
|
||||||
|
{c.header}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</FieldSelect>
|
||||||
|
<Input
|
||||||
|
value={sheetKeyword}
|
||||||
|
placeholder={t("bizState.sheetFilterPh")}
|
||||||
|
onChange={(e) => setSheetKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="muted bs-sheet-count">
|
||||||
|
{filteredSheetRows.length}/{activeSheet.rows.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-list-table-wrap bs-sheet-table">
|
||||||
|
<table className="data-table pt-list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{activeSheet.columns.map((c) => (
|
||||||
|
<th key={c.key}>{c.header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredSheetRows.slice(0, 2000).map((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
{activeSheet.columns.map((c) => (
|
||||||
|
<td key={c.key}>
|
||||||
|
{c.key === "parse_status" ? (
|
||||||
|
<NmStatusChip color={jobChipColor(cellText(row[c.key]))}>
|
||||||
|
{cellText(row[c.key]) || "—"}
|
||||||
|
</NmStatusChip>
|
||||||
|
) : (
|
||||||
|
cellText(row[c.key]) || "—"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!filteredSheetRows.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={activeSheet.columns.length}>
|
||||||
|
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bs-sheet-tabs" role="tablist" aria-label={t("bizState.batchWorkbook")}>
|
||||||
|
{sheets.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeSheet?.id === s.id}
|
||||||
|
className={`bs-sheet-tab${activeSheet?.id === s.id ? " is-active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSheetId(s.id);
|
||||||
|
setSheetKeyword("");
|
||||||
|
setSheetColumn("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
<span className="bs-sheet-tab__count">{s.rows.length}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{!sheets.length ? <span className="muted">{t("bizState.sheetEmpty")}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
{batchDetail ? (
|
||||||
|
<Button size="sm" variant="secondary" onPress={() => void bizStateDownloadExport(batchDetail.id)}>
|
||||||
|
{t("bizState.export")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button size="sm" variant="ghost" onPress={closeBatch}>
|
||||||
|
{t("bizState.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AppModalShell>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue