mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Decouple cutover HF from portrait and tighten ops UI.
Portrait stays on biz-monitor; cutover create/ensure owns purpose=cutover_hf slots with project collect override, multi-interval bindings, window pause, and industrial detail UX. Also expand ZTE collect/config parsers and TextFSM for route/BGP/L2VPN/VRRP coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a65c9fc420
commit
285c19e5a3
67 changed files with 9510 additions and 723 deletions
|
|
@ -253,6 +253,11 @@ const en = {
|
|||
filterColumn: "Column",
|
||||
filterAllCols: "All columns",
|
||||
listFilterPh: "Filter NE / IP / vendor / status",
|
||||
purposeFilter: "Task purpose",
|
||||
purposeAll: "All purposes",
|
||||
purposePortrait: "Portrait",
|
||||
purposeCutoverHf: "Cutover HF",
|
||||
colPurpose: "Purpose",
|
||||
colNe: "NE",
|
||||
colStatus: "Status",
|
||||
colLast: "Last collect",
|
||||
|
|
@ -366,7 +371,10 @@ const en = {
|
|||
"One pair per line: before_if,after_if (comma / tab / pipe). Example:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# comments start with #. Not slash-grouped A,B/C,D.",
|
||||
mappingSaved: "Mapping saved",
|
||||
mappingOptionalHint:
|
||||
"Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map.",
|
||||
"Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map. Main-port pairs also cover .subinterfaces (suffix kept).",
|
||||
ifaceNormalize: "Interface normalize",
|
||||
ifaceNormalizeHint:
|
||||
"Template rules, one per line: alias,canonical. Applied before port mapping. Example:\nGE,gei\nSG,smartgroup\nXGE,xgei",
|
||||
createJob: "Create compare job",
|
||||
defaultJobName: "Cutover compare",
|
||||
jobName: "Name",
|
||||
|
|
@ -475,45 +483,78 @@ const en = {
|
|||
"Metric focus: port status (interface_brief / admin·phy·prot). Baseline → pick ports → evaluate board.",
|
||||
hintShort: "Monitor template sets rules; bind collect + dual baselines → expect → evaluate board.",
|
||||
create: "Create task",
|
||||
createHint: "Name the task and bind old/new business-monitor collect tasks. Mapping and baselines can be set later.",
|
||||
createHintShort: "Name + old/new collect tasks; template, mapping, baselines can come later.",
|
||||
createHint:
|
||||
"Except portrait baselines, the full cutover flow runs on this page; create auto-spawns HF biz-monitor tasks on schedule.",
|
||||
createHintShort:
|
||||
"Pick old/new NEs → monitor template & metrics → interval/window → baselines → port map → create. HF tasks are auto-created.",
|
||||
pickOldNe: "Old NE",
|
||||
pickNewNe: "New NE",
|
||||
pickNe: "Pick NE",
|
||||
pickNeHint: "Click to pick a device",
|
||||
hideNePicker: "Hide list",
|
||||
nePairReady: "Both NEs selected — continue, or click a card to re-pick.",
|
||||
needNePair: "Enter a name and select old/new NEs",
|
||||
needProjectName: "Enter a task name",
|
||||
needMonitorTemplate: "Select a monitor template",
|
||||
needMetrics: "Select at least one metric",
|
||||
noPortraitBaseline:
|
||||
"No portrait task/batches for this NE yet — run portrait collect in business monitor first (baselines can be set later).",
|
||||
baselineOptionalHint: "Baselines can be set later in detail; pick from portrait batches when available.",
|
||||
metricInterval: "Per-metric interval (sec)",
|
||||
metricIntervalOptional: "Per-metric interval (optional)",
|
||||
metricIntervalHint: "Leave blank to use the global interval; a filled value creates a separate HF task.",
|
||||
scheduleHint:
|
||||
"Enter the window in your local time; stored as UTC. Scheduler pauses HF outside the window.",
|
||||
sectionSchedule: "Collect interval & window",
|
||||
sectionBaseline: "Baselines & mapping",
|
||||
sectionDevices: "Old / new NEs",
|
||||
sectionMetrics: "Collect metrics",
|
||||
createSteps: "Create steps",
|
||||
stepDevices: "Devices",
|
||||
stepTemplate: "Template",
|
||||
stepSchedule: "Schedule",
|
||||
stepBaseline: "Baselines",
|
||||
next: "Next",
|
||||
back: "Back",
|
||||
colHf: "HF",
|
||||
hfReady: "Ready",
|
||||
hfMissing: "Missing",
|
||||
showCreateMore: "More: template / mapping / baselines",
|
||||
hideCreateMore: "Hide more",
|
||||
setupHintShort: "Collect tasks are fixed at create; set monitor template, mapping and dual baselines here.",
|
||||
setupHintShort: "Portrait tasks feed baselines; HF tasks feed cutover collect. Configure template, metrics, window and dual baselines here.",
|
||||
createProject: "Create task",
|
||||
projectName: "Task name",
|
||||
projectNamePh: "e.g. CN1/CN2 night-1 cutover",
|
||||
projectNamePh: "e.g. PE-A / PE-B cutover-1",
|
||||
sectionName: "1. Task name",
|
||||
sectionTasks: "2. Bind collect tasks (required)",
|
||||
sectionTasksHint: "Pick the old-device and new-device business-monitor tasks.",
|
||||
sectionOptional: "3. Monitor template, mapping & baselines (optional)",
|
||||
sectionOptionalHint:
|
||||
"Skip for now if needed; set later under Baseline. Defaults to the port cutover monitor template when omitted.",
|
||||
"Skip for now if needed; set later under Baseline. Defaults to the system monitor template when omitted.",
|
||||
sectionPair: "Bound collect tasks",
|
||||
sectionPairHint: "Collect tasks are fixed at create time; adjust monitor template, mapping and baselines here.",
|
||||
sectionBaseline: "Baseline batches",
|
||||
sectionBaselineHint: "Both old and new baselines are required; old lists expect objects, new feeds dual-side verdict.",
|
||||
optionalNone: "None",
|
||||
monitorTemplate: "Monitor template",
|
||||
monitorTemplateHint: "Chooses which metrics to compare and dual-side success semantics. Defaults to port cutover.",
|
||||
monitorTemplateHint: "Chooses which metrics to compare and dual-side success semantics. Defaults to the system monitor template.",
|
||||
compareTemplate: "Compare template",
|
||||
compareTemplateBound: "Bound compare template: {{name}}",
|
||||
portMappingHint:
|
||||
"With a mapping, iface metrics compare only mapped ports (they must appear after cutover). Same-name ports outside the map are not a migration; old down + new up is an anomaly. No mapping means same-name compare. BGP/PW still use the batch expect set.",
|
||||
baselineNeedTask: "Select the matching collect task first",
|
||||
baselinePickHint: "Choose from that task’s collect batches",
|
||||
oldTask: "Old-device collect task",
|
||||
newTask: "New-device collect task",
|
||||
oldTask: "Old-device portrait task",
|
||||
newTask: "New-device portrait task",
|
||||
portMapping: "Port mapping",
|
||||
oldBaseline: "Old baseline batch",
|
||||
newBaseline: "New baseline batch",
|
||||
needProjectFields: "Enter a name and pick old/new collect tasks",
|
||||
needProjectFields: "Enter a name and pick old/new portrait tasks",
|
||||
needExpectPorts: "Select at least one expected object for this batch",
|
||||
needBaselineFirst: "Save an old baseline batch first to list expect objects",
|
||||
needBothBaselines: "Save both old and new baseline batches before evaluating",
|
||||
errOldBaselineRequired: "Old baseline batch is required",
|
||||
errNewBaselineRequired: "New baseline batch is required",
|
||||
needBizStateTasks: "No business-monitor tasks yet. Create one first:",
|
||||
needBizStateTasks: "No portrait business-monitor tasks yet. Create one first:",
|
||||
projectCreated: "Task created",
|
||||
projects: "Tasks",
|
||||
empty: "No cutover tasks yet.",
|
||||
|
|
@ -538,42 +579,66 @@ const en = {
|
|||
tabSetup: "Baseline",
|
||||
tabBatches: "Batches",
|
||||
tabBoard: "Board",
|
||||
saveBaseline: "Save baseline / mapping / template",
|
||||
saveBaseline: "Save",
|
||||
baselineSaved: "Baseline saved",
|
||||
pickExpectPorts: "Expected objects (from old baseline)",
|
||||
portFilterPh: "Filter object / description",
|
||||
emptyBaselinePorts:
|
||||
"No matching metric rows in baseline (confirm monitor template and collect profiles)",
|
||||
pickExpectPorts: "Expect",
|
||||
portFilterPh: "Filter",
|
||||
emptyBaselinePorts: "No baseline data",
|
||||
batchLabel: "Batch label",
|
||||
defaultBatchLabel: "Night N",
|
||||
expectPorts: "Expected ports (old side)",
|
||||
expectPortsPh: "Comma or newline separated",
|
||||
createBatch: "New batch",
|
||||
createBatch: "New",
|
||||
batchCreated: "Batch created",
|
||||
batches: "Batches",
|
||||
startBatch: "Start batch",
|
||||
finishBatch: "Finish batch",
|
||||
evaluate: "Evaluate now",
|
||||
evaluated: "Evaluation done",
|
||||
startBatch: "Start",
|
||||
finishBatch: "Finish",
|
||||
evaluate: "Evaluate",
|
||||
evaluated: "Done",
|
||||
statusUpdated: "Status updated",
|
||||
board: "Progress board",
|
||||
board: "Board",
|
||||
metricPort: "Port status",
|
||||
progress: "Progress",
|
||||
anomaly: "Anomaly",
|
||||
onlyExpect: "Expected objects only",
|
||||
onlyExpect: "Expect only",
|
||||
onlyExpectHint: "Shows all drift by default; check to filter to this batch's expect set",
|
||||
pinCurrentBatches: "Current collect batches",
|
||||
pinOldBatch: "Old current batch",
|
||||
pinNewBatch: "New current batch",
|
||||
pinLatest: "Auto latest success",
|
||||
runHistory: "Run history",
|
||||
runLatest: "Latest run",
|
||||
pinOldBatch: "Old current",
|
||||
pinNewBatch: "New current",
|
||||
pinLatest: "Latest",
|
||||
runHistory: "Runs",
|
||||
runLatest: "Latest",
|
||||
colRuleHit: "Rule hit",
|
||||
mappingBound: "Mapping saved and bound to this project",
|
||||
allMetrics: "All metrics",
|
||||
colMetric: "Metric",
|
||||
colKey: "Object",
|
||||
colPort: "Old port",
|
||||
openRedChip: "Red {{n}}",
|
||||
metaPortrait: "Portrait",
|
||||
metaHf: "HF",
|
||||
metaBaseline: "Baseline",
|
||||
baselineOk: "set",
|
||||
baselineMissing: "missing",
|
||||
needBaselineFirst: "Set baseline first",
|
||||
enableHighfreq: "Enable HF",
|
||||
highfreqReady: "HF ready",
|
||||
collectNow: "Collect now",
|
||||
collectTriggered: "Collect triggered",
|
||||
openBizState: "Biz monitor",
|
||||
hfNotBound: "Unbound",
|
||||
projectCollectMetrics: "Metrics",
|
||||
hfInterval: "Interval(s)",
|
||||
hfStart: "Start",
|
||||
hfEnd: "End",
|
||||
markProjectDone: "Done / stop HF",
|
||||
confirmProjectDone: "Complete project and pause all HF?",
|
||||
projectMarkedDone: "Done; HF paused",
|
||||
deleteProject: "Delete",
|
||||
confirmDeleteProject: "Delete project? HF will pause and batches removed.",
|
||||
projectDeleted: "Deleted",
|
||||
acceptPassedShort: "Pass",
|
||||
acceptFailedShort: "Fail",
|
||||
colMapped: "Mapped new",
|
||||
colDesc: "Description",
|
||||
colOldPort: "Old port",
|
||||
|
|
@ -592,27 +657,34 @@ const en = {
|
|||
verdictNotInvolved: "Not involved",
|
||||
verdictUnexpected: "Unexpected new",
|
||||
verdictOk: "OK",
|
||||
enableHighfreq: "Enable high-freq collect",
|
||||
highfreqHint:
|
||||
"Creates/reuses high-freq biz-state tasks on old/new NEs using the monitor template’s collect_metric_ids (empty = all compare-template metrics).",
|
||||
highfreqReady: "High-freq collect tasks ready and bound to this task",
|
||||
collectNow: "Collect old+new now",
|
||||
collectTriggered: "Collect triggered",
|
||||
openBizState: "Open business monitor",
|
||||
boundTasks: "Bound collect tasks",
|
||||
acceptTitle: "Batch acceptance summary",
|
||||
acceptPassed: "Batch acceptance passed",
|
||||
acceptFailed: "Batch acceptance failed ({{n}} red tickets; you may continue)",
|
||||
acceptPassedShort: "Passed",
|
||||
acceptFailedShort: "Failed",
|
||||
newBaselineMissingBanner: "These sheets have no usable new baseline rows; verdicts are unreliable: {{metrics}}",
|
||||
acceptCarryHint: "Failure does not block the next batch; red tickets stay until resolved.",
|
||||
openRedHint: "{{n}} open red ticket(s) on this task (carry allowed)",
|
||||
highfreqHint: "",
|
||||
boundTasks: "Bound tasks",
|
||||
oldPortraitTask: "Old portrait",
|
||||
newPortraitTask: "New portrait",
|
||||
oldHfTask: "Old HF",
|
||||
newHfTask: "New HF",
|
||||
projectCollectHint: "",
|
||||
hfWindowInactive: "HF window inactive or project done",
|
||||
baselinePortraitOnly: "",
|
||||
collectSkipped: "skipped",
|
||||
collectSkippedBanner: "Skipped: {{metrics}}",
|
||||
currentMissing: "no current",
|
||||
currentMissingBanner: "No current: {{metrics}}",
|
||||
highfreqReadyInactive: "HF bound (window inactive, no collect)",
|
||||
collectPartial: "Partial fail (ok {{ok}} / fail {{fail}})",
|
||||
errMetricNeedsBindings: "Metric \"{{metric}}\" needs bindings ({{params}}); not supported for cutover HF — remove it",
|
||||
errNoProfileForMetric: "Metric \"{{metric}}\" has no collect profile for this vendor",
|
||||
acceptTitle: "Acceptance",
|
||||
acceptPassed: "Passed",
|
||||
acceptFailed: "Failed ({{n}} red)",
|
||||
newBaselineMissingBanner: "Missing new baseline: {{metrics}}",
|
||||
acceptCarryHint: "May continue with red",
|
||||
openRedHint: "Red {{n}}",
|
||||
redTitle: "Red tickets",
|
||||
redOpen: "open",
|
||||
resolveRed: "Resolve",
|
||||
redResolved: "Red ticket resolved",
|
||||
batchCreatedWithRed: "Batch created ({{n}} open red tickets; carry allowed)",
|
||||
redResolved: "Resolved",
|
||||
batchCreatedWithRed: "Batch created ({{n}} red)",
|
||||
colStatus: "Status",
|
||||
verdictUnfinished: "Unfinished",
|
||||
},
|
||||
|
|
@ -632,7 +704,7 @@ const en = {
|
|||
listFilterPh: "Filter name / compare template / note",
|
||||
kpiTotal: "Templates",
|
||||
colName: "Name",
|
||||
namePh: "e.g. Port+BGP cutover monitor",
|
||||
namePh: "e.g. Status cutover monitor",
|
||||
colCompare: "Compare template",
|
||||
colCollect: "HF collect metrics",
|
||||
colNote: "Note",
|
||||
|
|
@ -640,6 +712,15 @@ const en = {
|
|||
needName: "Name is required",
|
||||
defaultsInvalid: "Invalid defaults JSON",
|
||||
overridesInvalid: "Invalid sheet_overrides JSON",
|
||||
exportTemplate: "Export",
|
||||
importTemplate: "Import",
|
||||
templateExported: "Template exported",
|
||||
templateImported: "Template imported",
|
||||
templateImportInvalid:
|
||||
"Invalid import file: need name and compare_template_id (or compare_template_name)",
|
||||
templateImportHint: "Import/export JSON to share monitor templates (no job/batch data)",
|
||||
templateImportCompareMissing:
|
||||
"Matching compare template not found — import or create it first",
|
||||
formHint: "Pick a compare template → same sheet tabs appear → set success conditions per sheet.",
|
||||
compareHint: "Owns which sheets and Key/compare fields; here you only set dual verdict rules.",
|
||||
pickCompare: "Pick compare template…",
|
||||
|
|
@ -816,7 +897,7 @@ const en = {
|
|||
prev: "Back",
|
||||
next: "Next",
|
||||
fieldTitle: "Task name",
|
||||
titlePh: "e.g. Access uplinks",
|
||||
titlePh: "e.g. Uplink group",
|
||||
titleRequired: "Task name is required",
|
||||
portsRequired: "Select at least one interface",
|
||||
interval: "Interval (sec)",
|
||||
|
|
@ -2450,7 +2531,7 @@ const en = {
|
|||
sourceTopology: "Topology placeholder",
|
||||
sourceWebcrt: "WebCRT",
|
||||
regexTitle: "Regex find",
|
||||
patternPh: "Regex, e.g. ^CORE- or -BJ-",
|
||||
patternPh: "Regex, e.g. ^CORE- or -EDGE-",
|
||||
fieldName: "Name",
|
||||
fieldNameIp: "Name+IP",
|
||||
findMatches: "Find matches",
|
||||
|
|
|
|||
|
|
@ -253,6 +253,11 @@ const zh = {
|
|||
filterColumn: "筛选列",
|
||||
filterAllCols: "全部列",
|
||||
listFilterPh: "筛选网元 / IP / 厂商 / 状态",
|
||||
purposeFilter: "任务用途",
|
||||
purposeAll: "全部用途",
|
||||
purposePortrait: "画像",
|
||||
purposeCutoverHf: "割接高频",
|
||||
colPurpose: "用途",
|
||||
colNe: "网元",
|
||||
colStatus: "状态",
|
||||
colLast: "最近采集",
|
||||
|
|
@ -365,7 +370,10 @@ const zh = {
|
|||
"每行一对:操作前接口,操作后接口(逗号/Tab/竖线分隔)。例:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# 井号开头为注释。不是 A,B/C,D 这种斜杠分组。",
|
||||
mappingSaved: "映射已保存",
|
||||
mappingOptionalHint:
|
||||
"可选。留空=忽略本端端口改名,按非接口 Key 对齐。填写后才做端口映射校验。",
|
||||
"可选。留空=忽略本端端口改名,按非接口 Key 对齐。填写后才做端口映射校验。主接口映射会自动覆盖 .子接口(后缀不变)。",
|
||||
ifaceNormalize: "接口归一化",
|
||||
ifaceNormalizeHint:
|
||||
"模板级规则,每行:简写,规范名。先归一化再端口映射。例:\nGE,gei\nSG,smartgroup\nXGE,xgei",
|
||||
createJob: "建立比对任务",
|
||||
defaultJobName: "割接比对",
|
||||
jobName: "名称",
|
||||
|
|
@ -469,46 +477,77 @@ const zh = {
|
|||
title: "割接监控",
|
||||
hint: "与业务对比独立:相对基线漂移 + 本批预期 + 老/新双端判定。复用采集任务与端口映射。",
|
||||
hintPort: "当前监控项:端口状态(interface_brief / admin·phy·prot)。选基线 → 勾选本批端口 → 判定看板。",
|
||||
hintShort: "监控模板定判定规则;绑老/新采集与双基线 → 勾预期 → 看板评估。",
|
||||
hintShort: "选老/新设备与监控模板,创建后自动高频采集;基线来自业务监控画像。",
|
||||
create: "创建任务",
|
||||
createHint: "先填任务名并绑定老/新业务监控采集任务;映射与基线可稍后在详情里补。",
|
||||
createHintShort: "任务名 + 老/新采集任务即可创建;模板、映射、基线可稍后补。",
|
||||
createHint:
|
||||
"除画像基线外,整条割接流程在本页完成;创建后自动生成业务监控高频任务并按计划执行。",
|
||||
createHintShort:
|
||||
"选老/新设备 → 监控模板与采集项 → 周期/窗口 → 基线(画像)→ 端口映射 → 创建。后台自动建高频任务。",
|
||||
pickOldNe: "老设备",
|
||||
pickNewNe: "新设备",
|
||||
pickNe: "选择设备",
|
||||
pickNeHint: "点击选择网元",
|
||||
hideNePicker: "收起列表",
|
||||
nePairReady: "老/新设备已选好,可进入下一步;点击上方卡片可重新选择。",
|
||||
needNePair: "请填写任务名并选择老/新设备",
|
||||
needProjectName: "请填写任务名称",
|
||||
needMonitorTemplate: "请选择监控模板",
|
||||
needMetrics: "请至少勾选一项监控项",
|
||||
noPortraitBaseline: "该设备尚无画像采集任务或批次,请先在业务监控跑通画像后再选基线(可稍后补)。",
|
||||
baselineOptionalHint: "基线可稍后在详情里补;有画像批次时可在此直接选定。",
|
||||
metricInterval: "分项周期(秒)",
|
||||
metricIntervalOptional: "分项周期(可选)",
|
||||
metricIntervalHint: "留空则使用上方全局周期;填写后该监控项会单独建高频任务。",
|
||||
scheduleHint: "按本机时间填写窗口,入库为 UTC;窗口外调度器自动暂停高频任务。",
|
||||
sectionSchedule: "采集周期与窗口",
|
||||
sectionBaseline: "基线与映射",
|
||||
sectionDevices: "老 / 新设备",
|
||||
sectionMetrics: "监控项",
|
||||
createSteps: "创建步骤",
|
||||
stepDevices: "设备",
|
||||
stepTemplate: "模板",
|
||||
stepSchedule: "周期",
|
||||
stepBaseline: "基线",
|
||||
next: "下一步",
|
||||
back: "上一步",
|
||||
colHf: "高频",
|
||||
hfReady: "已建",
|
||||
hfMissing: "未建",
|
||||
showCreateMore: "更多:模板 / 映射 / 基线",
|
||||
hideCreateMore: "收起更多",
|
||||
setupHintShort: "采集任务创建后固定;此处配置监控模板、映射与双基线。",
|
||||
setupHintShort: "画像任务用于基线;高频任务用于窗口内采集。此处配置模板、采集项、窗口与双基线。",
|
||||
createProject: "创建任务",
|
||||
projectName: "任务名称",
|
||||
projectNamePh: "例如:CN1/CN2 第1夜割接",
|
||||
projectNamePh: "例如:PE-A / PE-B 割接-1",
|
||||
sectionName: "1. 任务名称",
|
||||
sectionTasks: "2. 绑定采集任务(必填)",
|
||||
sectionTasksHint: "分别选择老设备、新设备上的业务监控任务;两侧不能为空。",
|
||||
sectionOptional: "3. 监控模板、映射与基线(可选)",
|
||||
sectionOptionalHint: "可先跳过,创建后再到「基线配置」里设置。未选监控模板时默认绑端口割接监控。",
|
||||
sectionOptionalHint: "可先跳过,创建后再到「基线配置」里设置。未选监控模板时使用系统默认。",
|
||||
sectionPair: "已绑定采集任务",
|
||||
sectionPairHint: "采集任务在创建时绑定;此处可调整监控模板、映射与基线。",
|
||||
sectionBaseline: "基线批次",
|
||||
sectionBaselineHint: "老侧与新侧基线均必填;老侧用于列出本批预期对象,新侧参与双端判定。",
|
||||
optionalNone: "不选",
|
||||
monitorTemplate: "监控模板",
|
||||
monitorTemplateHint: "决定对比哪些 metric 与双端成功语义;默认端口割接监控。",
|
||||
monitorTemplateHint: "决定对比哪些 metric 与双端成功语义;默认绑定系统默认监控模板。",
|
||||
compareTemplate: "对比模板",
|
||||
compareTemplateBound: "绑定对比模板:{{name}}",
|
||||
portMappingHint:
|
||||
"有映射时,带接口字段的监控项只比映射内的口(这些口必须在割接后出现)。映射外的同名口不参与迁移对比;老口 down、新口同名 up 记为异常。未配映射则按同名直比。BGP/PW 仍用本批预期勾选。",
|
||||
baselineNeedTask: "请先选择对应采集任务",
|
||||
baselinePickHint: "从该任务的历史采集批次中选择",
|
||||
oldTask: "老设备采集任务",
|
||||
newTask: "新设备采集任务",
|
||||
oldTask: "老设备画像任务",
|
||||
newTask: "新设备画像任务",
|
||||
portMapping: "端口映射",
|
||||
oldBaseline: "老设备基线批次",
|
||||
newBaseline: "新设备基线批次",
|
||||
needProjectFields: "请填写任务名并选择老/新采集任务",
|
||||
needProjectFields: "请填写任务名并选择老/新画像任务",
|
||||
needExpectPorts: "请先勾选本批预期迁移的对象",
|
||||
needBaselineFirst: "请先保存老设备基线批次,才能列出预期对象",
|
||||
needBaselineFirst: "先设基线",
|
||||
needBothBaselines: "请先保存老、新两侧基线批次后再评估",
|
||||
errOldBaselineRequired: "缺少老设备基线批次",
|
||||
errNewBaselineRequired: "缺少新设备基线批次",
|
||||
needBizStateTasks: "尚无业务监控任务,请先创建:",
|
||||
needBizStateTasks: "尚无业务监控(画像)任务,请先创建:",
|
||||
projectCreated: "任务已创建",
|
||||
projects: "任务",
|
||||
empty: "暂无割接任务。",
|
||||
|
|
@ -533,20 +572,20 @@ const zh = {
|
|||
tabSetup: "基线配置",
|
||||
tabBatches: "割接批次",
|
||||
tabBoard: "判定看板",
|
||||
saveBaseline: "保存基线/映射/模板",
|
||||
saveBaseline: "保存配置",
|
||||
baselineSaved: "基线已保存",
|
||||
pickExpectPorts: "本批预期对象(来自老基线)",
|
||||
portFilterPh: "筛选对象 / 描述",
|
||||
emptyBaselinePorts: "基线中无对应 metric 数据(确认监控模板与采集任务已勾选相应 profile)",
|
||||
pickExpectPorts: "本批预期",
|
||||
portFilterPh: "筛选",
|
||||
emptyBaselinePorts: "基线无数据",
|
||||
batchLabel: "批次标签",
|
||||
defaultBatchLabel: "第N晚",
|
||||
expectPorts: "本批预期端口(老侧)",
|
||||
expectPortsPh: "逗号或换行分隔",
|
||||
createBatch: "新建批次",
|
||||
createBatch: "新建",
|
||||
batchCreated: "批次已创建",
|
||||
batches: "批次",
|
||||
startBatch: "本批开始",
|
||||
finishBatch: "本批完成",
|
||||
startBatch: "启动",
|
||||
finishBatch: "收工",
|
||||
evaluate: "立即判定",
|
||||
evaluated: "判定完成",
|
||||
statusUpdated: "状态已更新",
|
||||
|
|
@ -554,12 +593,12 @@ const zh = {
|
|||
metricPort: "端口状态",
|
||||
progress: "进度",
|
||||
anomaly: "异常",
|
||||
onlyExpect: "仅看预期对象",
|
||||
onlyExpect: "仅预期",
|
||||
onlyExpectHint: "默认显示全部漂移;勾选后只看本批预期",
|
||||
pinCurrentBatches: "当前采集批次",
|
||||
pinOldBatch: "老侧当前批",
|
||||
pinNewBatch: "新侧当前批",
|
||||
pinLatest: "自动最新成功批",
|
||||
pinOldBatch: "老侧当前",
|
||||
pinNewBatch: "新侧当前",
|
||||
pinLatest: "最新",
|
||||
runHistory: "判定历史",
|
||||
runLatest: "最新一次",
|
||||
colRuleHit: "命中规则",
|
||||
|
|
@ -568,6 +607,31 @@ const zh = {
|
|||
colMetric: "监控项",
|
||||
colKey: "对象",
|
||||
colPort: "老侧端口",
|
||||
openRedChip: "红单 {{n}}",
|
||||
metaPortrait: "画像",
|
||||
metaHf: "HF",
|
||||
metaBaseline: "基线",
|
||||
baselineOk: "已设",
|
||||
baselineMissing: "缺",
|
||||
needBaselineFirst: "先设基线",
|
||||
enableHighfreq: "开启高频",
|
||||
highfreqReady: "高频已就绪",
|
||||
collectNow: "立即采集",
|
||||
collectTriggered: "已触发采集",
|
||||
openBizState: "业务监控",
|
||||
hfNotBound: "未绑定",
|
||||
projectCollectMetrics: "采集项",
|
||||
hfInterval: "周期(秒)",
|
||||
hfStart: "开始",
|
||||
hfEnd: "结束",
|
||||
markProjectDone: "完成并停HF",
|
||||
confirmProjectDone: "确认完成项目并暂停全部高频?",
|
||||
projectMarkedDone: "已完成,高频已停",
|
||||
deleteProject: "删除",
|
||||
confirmDeleteProject: "确认删除项目?将停高频并清批次记录。",
|
||||
projectDeleted: "已删除",
|
||||
acceptPassedShort: "通过",
|
||||
acceptFailedShort: "未过",
|
||||
colMapped: "映射新口",
|
||||
colDesc: "描述",
|
||||
colOldPort: "老端口",
|
||||
|
|
@ -586,27 +650,38 @@ const zh = {
|
|||
verdictNotInvolved: "未涉及",
|
||||
verdictUnexpected: "意外新增",
|
||||
verdictOk: "正常",
|
||||
enableHighfreq: "开启高频采集",
|
||||
highfreqHint:
|
||||
"按当前监控模板的 collect_metric_ids(空则取对比模板全量)为老/新设备各建或复用高频采集任务并勾选对应 profile。",
|
||||
highfreqReady: "高频采集任务已就绪(已绑定到本任务)",
|
||||
collectNow: "立即采集老+新",
|
||||
collectTriggered: "已触发采集",
|
||||
openBizState: "打开业务监控",
|
||||
boundTasks: "当前绑定采集任务",
|
||||
acceptTitle: "本批验收小结",
|
||||
acceptPassed: "本批验收通过",
|
||||
acceptFailed: "本批验收未通过(红单 {{n}},可带红开下一批)",
|
||||
acceptPassedShort: "通过",
|
||||
acceptFailedShort: "未通过",
|
||||
newBaselineMissingBanner: "这些表没有可用的新侧基线内容,判定不可信:{{metrics}}",
|
||||
acceptCarryHint: "验收不过不拦截:可新建下一批继续;红单保留留痕,可手动关闭。",
|
||||
openRedHint: "任务仍有 {{n}} 条未关闭红单(带红继续)",
|
||||
redTitle: "红单留痕",
|
||||
highfreqHint: "",
|
||||
boundTasks: "绑定任务",
|
||||
oldPortraitTask: "老画像",
|
||||
newPortraitTask: "新画像",
|
||||
oldHfTask: "老HF",
|
||||
newHfTask: "新HF",
|
||||
projectCollectHint: "",
|
||||
hfWindowInactive: "高频窗口未到/已结束,或项目已完成,当前不可采集",
|
||||
baselinePortraitOnly: "",
|
||||
collectSkipped: "未采",
|
||||
collectSkippedBanner: "未采:{{metrics}}",
|
||||
currentMissing: "缺当前",
|
||||
currentMissingBanner: "缺当前批次:{{metrics}}",
|
||||
highfreqReadyInactive: "高频已绑定(窗口未开,未立即采集)",
|
||||
collectPartial: "部分失败(成功 {{ok}} / 失败 {{fail}})",
|
||||
confirmProjectDone: "确认完成项目并暂停全部高频?",
|
||||
projectMarkedDone: "已完成,高频已停",
|
||||
confirmDeleteProject: "确认删除项目?将停高频并清批次记录。",
|
||||
projectDeleted: "已删除",
|
||||
errMetricNeedsBindings: "监控项「{{metric}}」需要参数绑定({{params}}),割接高频暂不支持,请去掉该项",
|
||||
errNoProfileForMetric: "监控项「{{metric}}」在当前设备厂商下无可用采集 profile",
|
||||
acceptTitle: "本批验收",
|
||||
acceptPassed: "验收通过",
|
||||
acceptFailed: "验收未过(红单 {{n}})",
|
||||
newBaselineMissingBanner: "缺新基线:{{metrics}}",
|
||||
acceptCarryHint: "可带红开下一批",
|
||||
openRedHint: "红单 {{n}}",
|
||||
redTitle: "红单",
|
||||
redOpen: "未关闭",
|
||||
resolveRed: "关闭",
|
||||
redResolved: "红单已关闭",
|
||||
batchCreatedWithRed: "批次已创建(当前有 {{n}} 条未关闭红单,允许带红继续)",
|
||||
redResolved: "已关闭",
|
||||
batchCreatedWithRed: "已建批(红单 {{n}})",
|
||||
colStatus: "状态",
|
||||
verdictUnfinished: "未完成",
|
||||
},
|
||||
|
|
@ -626,7 +701,7 @@ const zh = {
|
|||
listFilterPh: "筛选名称 / 对比模板 / 备注",
|
||||
kpiTotal: "模板数",
|
||||
colName: "名称",
|
||||
namePh: "例如:端口+BGP 割接监控",
|
||||
namePh: "例如:状态割接监控",
|
||||
colCompare: "对比模板",
|
||||
colCollect: "高频采集项",
|
||||
colNote: "备注",
|
||||
|
|
@ -634,6 +709,13 @@ const zh = {
|
|||
needName: "请填写名称",
|
||||
defaultsInvalid: "defaults JSON 无效",
|
||||
overridesInvalid: "sheet_overrides JSON 无效",
|
||||
exportTemplate: "导出",
|
||||
importTemplate: "导入",
|
||||
templateExported: "模板已导出",
|
||||
templateImported: "模板已导入",
|
||||
templateImportInvalid: "导入文件无效:需要 name 与 compare_template_id(或 compare_template_name)",
|
||||
templateImportHint: "导入/导出 JSON,便于私下传递监控模板(不含任务/批次数据)",
|
||||
templateImportCompareMissing: "未找到对应对比模板,请先导入或创建同名对比模板",
|
||||
formHint: "先选对比模板 → 出现与对比模板相同的表页签 → 为每张表配置成功条件。",
|
||||
compareHint: "决定有哪些表以及 Key/比对字段;此处只配置双端判定。",
|
||||
pickCompare: "选择对比模板…",
|
||||
|
|
@ -809,7 +891,7 @@ const zh = {
|
|||
prev: "上一步",
|
||||
next: "下一步",
|
||||
fieldTitle: "任务名称",
|
||||
titlePh: "例如:接入侧上联",
|
||||
titlePh: "例如:上联链路组",
|
||||
titleRequired: "请填写任务名称",
|
||||
portsRequired: "请至少选择一个接口",
|
||||
interval: "周期(秒)",
|
||||
|
|
@ -2423,7 +2505,7 @@ const zh = {
|
|||
sourceTopology: "拓扑占位",
|
||||
sourceWebcrt: "WebCRT",
|
||||
regexTitle: "正则查找",
|
||||
patternPh: "正则,如 ^CORE- 或 -BJ-",
|
||||
patternPh: "正则,如 ^CORE- 或 -EDGE-",
|
||||
fieldName: "名称",
|
||||
fieldNameIp: "名称+IP",
|
||||
findMatches: "查找匹配",
|
||||
|
|
|
|||
|
|
@ -10062,8 +10062,10 @@ html.login-page--paused .login-page__flare {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 70vh;
|
||||
max-height: min(70vh, 720px);
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
.mt-editor__nav-head {
|
||||
display: flex;
|
||||
|
|
@ -10074,11 +10076,15 @@ html.login-page--paused .login-page__flare {
|
|||
font-weight: 600;
|
||||
padding: 2px 4px 6px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mt-editor__nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.mt-editor__nav-item {
|
||||
appearance: none;
|
||||
|
|
@ -10116,6 +10122,10 @@ html.login-page--paused .login-page__flare {
|
|||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-height: min(70vh, 720px);
|
||||
overflow: auto;
|
||||
align-self: stretch;
|
||||
}
|
||||
.mt-editor__pane-head {
|
||||
display: flex;
|
||||
|
|
@ -10251,7 +10261,7 @@ html.login-page--paused .login-page__flare {
|
|||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
align-items: stretch;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mt-rule-card__grid {
|
||||
|
|
@ -10262,26 +10272,51 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
}
|
||||
.mt-rule-arrow {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
opacity: 0.55;
|
||||
padding-top: 22px;
|
||||
padding: 0 2px;
|
||||
align-self: stretch;
|
||||
}
|
||||
.mt-side-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.mt-side-col__label {
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mt-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
.mt-side-empty-box {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
border-radius: 6px;
|
||||
padding: 14px 12px;
|
||||
min-height: 88px;
|
||||
background: rgba(2, 6, 23, 0.2);
|
||||
}
|
||||
.mt-side-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.mt-or-wrap {
|
||||
display: flex;
|
||||
|
|
@ -10733,18 +10768,75 @@ html.login-page--paused .login-page__flare {
|
|||
opacity: 0.55;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bm-detail__red {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.bm-detail__actions {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
.bm-detail__link {
|
||||
font-size: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
.bm-setup__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 6px 0 2px;
|
||||
}
|
||||
.bm-setup__sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.bm-setup__binds {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
.bm-setup__cmp {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
font-size: 12px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.bm-batches__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.bm-batches__toolbar-main,
|
||||
.bm-batches__toolbar-side {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.bm-board__status {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
align-items: baseline;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 6px;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 13px;
|
||||
}
|
||||
.bm-board__verdict {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.bm-board__verdict.is-ok {
|
||||
color: #34d399;
|
||||
}
|
||||
.bm-board__verdict.is-bad,
|
||||
.bm-board__num.is-bad {
|
||||
color: #f87171;
|
||||
}
|
||||
.bm-board__num {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.bm-batches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -10752,11 +10844,6 @@ html.login-page--paused .login-page__flare {
|
|||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.bm-batches__toolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
.bm-batches__main {
|
||||
min-height: 320px;
|
||||
flex: 1;
|
||||
|
|
@ -10788,6 +10875,220 @@ html.login-page--paused .login-page__flare {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Cutover create wizard */
|
||||
.bm-create {
|
||||
min-height: min(52vh, 520px);
|
||||
}
|
||||
.bm-create__pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.bm-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.bm-steps__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.22);
|
||||
color: inherit;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
opacity: 0.72;
|
||||
transition: border-color 0.15s ease, opacity 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
.bm-steps__item:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
.bm-steps__item.is-active {
|
||||
opacity: 1;
|
||||
border-color: rgba(148, 163, 184, 0.55);
|
||||
background: rgba(51, 65, 85, 0.35);
|
||||
}
|
||||
.bm-steps__item.is-done {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.bm-steps__num {
|
||||
flex: 0 0 auto;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: rgba(148, 163, 184, 0.22);
|
||||
}
|
||||
.bm-steps__item.is-active .bm-steps__num,
|
||||
.bm-steps__item.is-done .bm-steps__num {
|
||||
background: rgba(148, 163, 184, 0.4);
|
||||
}
|
||||
.bm-steps__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.bm-steps {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
.bm-ne-slot {
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
min-height: 78px;
|
||||
}
|
||||
.bm-ne-slot:hover {
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
}
|
||||
.bm-ne-slot.is-picking {
|
||||
border-color: rgba(148, 163, 184, 0.65);
|
||||
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.bm-ne-slot.is-filled {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
.bm-ne-slot__name {
|
||||
font-weight: 650;
|
||||
font-size: 14px;
|
||||
}
|
||||
.bm-ne-slot__meta,
|
||||
.bm-ne-slot__empty {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.bm-ne-slot__empty {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.bm-ne-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(2, 6, 23, 0.28);
|
||||
}
|
||||
.bm-ne-picker__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.bm-ne-picker__table {
|
||||
max-height: min(36vh, 320px);
|
||||
overflow: auto;
|
||||
}
|
||||
.bm-ne-picker .pt-list-pager,
|
||||
.bm-ne-picker .pager {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.bm-ne-picker__table tr.is-selected td {
|
||||
background: rgba(51, 65, 85, 0.45);
|
||||
}
|
||||
.bm-metric-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.bm-metric-chip {
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: 6px;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.bm-metric-chip.is-on {
|
||||
opacity: 1;
|
||||
border-color: rgba(148, 163, 184, 0.65);
|
||||
background: rgba(51, 65, 85, 0.45);
|
||||
font-weight: 600;
|
||||
}
|
||||
.bm-sched-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.bm-sched-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.bm-sched-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.bm-interval-table {
|
||||
max-height: min(28vh, 240px);
|
||||
overflow: auto;
|
||||
}
|
||||
.bm-hf-bindings {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 1.1em;
|
||||
list-style: disc;
|
||||
}
|
||||
.bm-create__summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.bm-create__summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.bm-create-modal .modal-footer,
|
||||
.bm-create-modal [data-slot="footer"],
|
||||
.bm-create-modal .app-heroui-modal__dialog [data-slot="footer"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bm-create__footer-spacer {
|
||||
flex: 1 1 auto;
|
||||
min-width: 8px;
|
||||
}
|
||||
.bm-list-hf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.bm-list-hf__iv {
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.bs-bind-panel {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ type Template = {
|
|||
key_fields: string[];
|
||||
iface_fields: string[];
|
||||
compare_fields: string[];
|
||||
iface_normalize_rules?: { from: string; to: string }[];
|
||||
note: string;
|
||||
};
|
||||
|
||||
|
|
@ -405,6 +406,7 @@ function templateExportPayload(tpl: Template) {
|
|||
exported_at: new Date().toISOString(),
|
||||
name: tpl.name,
|
||||
note: tpl.note || "",
|
||||
iface_normalize_rules: [...(tpl.iface_normalize_rules || [])],
|
||||
metrics,
|
||||
};
|
||||
}
|
||||
|
|
@ -413,6 +415,7 @@ function parseTemplateImport(raw: unknown): {
|
|||
name: string;
|
||||
note: string;
|
||||
metrics: MetricSheet[];
|
||||
iface_normalize_rules?: { from: string; to: string }[];
|
||||
} | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const o = raw as Record<string, unknown>;
|
||||
|
|
@ -462,7 +465,49 @@ function parseTemplateImport(raw: unknown): {
|
|||
});
|
||||
}
|
||||
if (!metrics.length) return null;
|
||||
return { name, note, metrics };
|
||||
const normRaw = o.iface_normalize_rules;
|
||||
const iface_normalize_rules: { from: string; to: string }[] = [];
|
||||
if (Array.isArray(normRaw)) {
|
||||
for (const item of normRaw) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
const r = item as Record<string, unknown>;
|
||||
const fr = String(r.from || "").trim();
|
||||
const to = String(r.to || "").trim();
|
||||
if (fr && to) iface_normalize_rules.push({ from: fr, to });
|
||||
}
|
||||
}
|
||||
return { name, note, metrics, iface_normalize_rules };
|
||||
}
|
||||
|
||||
function ifaceNormRulesToText(rules?: { from: string; to: string }[]): string {
|
||||
return (rules || []).map((r) => `${r.from},${r.to}`).join("\n");
|
||||
}
|
||||
|
||||
function parseIfaceNormText(text: string): { from: string; to: string }[] {
|
||||
const out: { from: string; to: string }[] = [];
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const s = line.trim();
|
||||
if (!s || s.startsWith("#")) continue;
|
||||
let fr = "";
|
||||
let to = "";
|
||||
if (s.includes(",")) {
|
||||
const i = s.indexOf(",");
|
||||
fr = s.slice(0, i).trim();
|
||||
to = s.slice(i + 1).trim();
|
||||
} else if (s.includes("\t")) {
|
||||
const i = s.indexOf("\t");
|
||||
fr = s.slice(0, i).trim();
|
||||
to = s.slice(i + 1).trim();
|
||||
} else {
|
||||
const parts = s.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
fr = parts[0];
|
||||
to = parts.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
if (fr && to) out.push({ from: fr, to });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function downloadJsonFile(filename: string, data: unknown) {
|
||||
|
|
@ -721,6 +766,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const [tplEditId, setTplEditId] = useState("");
|
||||
const [tplName, setTplName] = useState("");
|
||||
const [tplNote, setTplNote] = useState("");
|
||||
const [tplNormText, setTplNormText] = useState("");
|
||||
const [tplSheets, setTplSheets] = useState<MetricSheet[]>([]);
|
||||
const [tplSheetIdx, setTplSheetIdx] = useState(0);
|
||||
const [tplPaneTab, setTplPaneTab] = useState<"fields" | "filters">("fields");
|
||||
|
|
@ -1127,6 +1173,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setTplEditId("");
|
||||
setTplName("");
|
||||
setTplNote("");
|
||||
setTplNormText("XXVGE,xxvgei\nXGE,xgei\nCGE,cgei\nGE,gei\nSG,smartgroup");
|
||||
setTplSheets([]);
|
||||
setTplSheetIdx(0);
|
||||
setTplPaneTab("fields");
|
||||
|
|
@ -1139,6 +1186,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setTplEditId(tpl.id);
|
||||
setTplName(tpl.name);
|
||||
setTplNote(tpl.note || "");
|
||||
setTplNormText(ifaceNormRulesToText(tpl.iface_normalize_rules));
|
||||
setTplSheets(sheets.length ? sheets.map(cloneSheet) : []);
|
||||
setTplSheetIdx(0);
|
||||
setTplPaneTab("fields");
|
||||
|
|
@ -1229,6 +1277,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const body = {
|
||||
name: tplName || tplSheets.map((s) => sheetLabel(s)).join("+"),
|
||||
note: tplNote,
|
||||
iface_normalize_rules: parseIfaceNormText(tplNormText),
|
||||
metrics: tplSheets.map((s) => ({
|
||||
sheet_id: sheetIdentity(s),
|
||||
title: s.title || sheetIdentity(s),
|
||||
|
|
@ -1293,6 +1342,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
await bizCompareCreateTemplate({
|
||||
name: body.name,
|
||||
note: body.note,
|
||||
iface_normalize_rules: body.iface_normalize_rules || [],
|
||||
metrics: body.metrics.map((s) => ({
|
||||
sheet_id: sheetIdentity(s),
|
||||
title: s.title || sheetIdentity(s),
|
||||
|
|
@ -1890,6 +1940,19 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="ct-editor__norm">
|
||||
<div className="ct-editor__pick-title">{t("bizCompare.ifaceNormalize")}</div>
|
||||
<p className="muted ct-editor__pick-hint">{t("bizCompare.ifaceNormalizeHint")}</p>
|
||||
<textarea
|
||||
className="ct-editor__norm-area"
|
||||
rows={5}
|
||||
value={tplNormText}
|
||||
onChange={(e) => setTplNormText(e.target.value)}
|
||||
placeholder={"GE,gei\nSG,smartgroup"}
|
||||
aria-label={t("bizCompare.ifaceNormalize")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ct-editor__pick">
|
||||
<div className="ct-editor__pick-head">
|
||||
<span className="ct-editor__pick-title">
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Input, Modal } from "@heroui/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||
|
|
@ -70,6 +70,71 @@ type MonitorTpl = {
|
|||
note?: string;
|
||||
};
|
||||
|
||||
const MONITOR_EXPORT_FORMAT = "netx.biz_monitor_template";
|
||||
const MONITOR_EXPORT_VERSION = 1;
|
||||
|
||||
function downloadJsonFile(filename: string, data: unknown) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function monitorExportPayload(tpl: MonitorTpl) {
|
||||
return {
|
||||
format: MONITOR_EXPORT_FORMAT,
|
||||
version: MONITOR_EXPORT_VERSION,
|
||||
exported_at: new Date().toISOString(),
|
||||
name: tpl.name,
|
||||
note: tpl.note || "",
|
||||
compare_template_id: tpl.compare_template_id || "",
|
||||
compare_template_name: tpl.compare_template_name || "",
|
||||
collect_metric_ids: [...(tpl.collect_metric_ids || [])],
|
||||
defaults: { ...(tpl.defaults || {}) },
|
||||
sheet_overrides: JSON.parse(JSON.stringify(tpl.sheet_overrides || [])),
|
||||
};
|
||||
}
|
||||
|
||||
function parseMonitorImport(raw: unknown): {
|
||||
name: string;
|
||||
note: string;
|
||||
compare_template_id: string;
|
||||
compare_template_name: string;
|
||||
collect_metric_ids: string[];
|
||||
defaults: Record<string, unknown>;
|
||||
sheet_overrides: SheetOverride[];
|
||||
} | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const o = raw as Record<string, unknown>;
|
||||
const name = String(o.name || "").trim();
|
||||
if (!name) return null;
|
||||
const compare_template_id = String(o.compare_template_id || "").trim();
|
||||
const compare_template_name = String(o.compare_template_name || "").trim();
|
||||
if (!compare_template_id && !compare_template_name) return null;
|
||||
const collect = Array.isArray(o.collect_metric_ids)
|
||||
? (o.collect_metric_ids as unknown[]).map((x) => String(x).trim()).filter(Boolean)
|
||||
: [];
|
||||
const defaults =
|
||||
o.defaults && typeof o.defaults === "object" && !Array.isArray(o.defaults)
|
||||
? (o.defaults as Record<string, unknown>)
|
||||
: { dual_mode: "migrate_pair", out_of_expect: "strict" };
|
||||
const sheet_overrides = Array.isArray(o.sheet_overrides)
|
||||
? (o.sheet_overrides as SheetOverride[])
|
||||
: [];
|
||||
return {
|
||||
name,
|
||||
note: String(o.note || ""),
|
||||
compare_template_id,
|
||||
compare_template_name,
|
||||
collect_metric_ids: collect,
|
||||
defaults,
|
||||
sheet_overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const PRESENCE_TOKENS = [
|
||||
{ id: "removed", labelKey: "tokRemoved" },
|
||||
{ id: "added", labelKey: "tokAdded" },
|
||||
|
|
@ -481,7 +546,9 @@ function SideGroupsEditor({
|
|||
</label>
|
||||
) : null}
|
||||
{dontCare ? (
|
||||
<p className="muted mt-side-empty">{t("bizMonitorTpl.sideDontCareHint")}</p>
|
||||
<div className="mt-side-empty-box">
|
||||
<p className="muted mt-side-empty">{t("bizMonitorTpl.sideDontCareHint")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groups.map((group, gi) => (
|
||||
|
|
@ -759,6 +826,7 @@ export function BizMonitorTemplatesPage() {
|
|||
const [showMore, setShowMore] = useState(false);
|
||||
const [showAdvancedJson, setShowAdvancedJson] = useState(false);
|
||||
const [overridesText, setOverridesText] = useState("[]");
|
||||
const tplImportRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const [mon, cmp, metrics] = await Promise.all([
|
||||
|
|
@ -970,6 +1038,60 @@ export function BizMonitorTemplatesPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const exportTemplate = (tpl: MonitorTpl) => {
|
||||
const payload = monitorExportPayload(tpl);
|
||||
const safe = (tpl.name || "template").replace(/[^\w\u4e00-\u9fff.-]+/g, "_").slice(0, 64);
|
||||
downloadJsonFile(`netx-monitor-template-${safe}.json`, payload);
|
||||
showOk(t("bizMonitorTpl.templateExported"));
|
||||
};
|
||||
|
||||
const importTemplateFile = async (file: File) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const text = await file.text();
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
showError(t("bizMonitorTpl.templateImportInvalid"));
|
||||
return;
|
||||
}
|
||||
const body = parseMonitorImport(parsed);
|
||||
if (!body) {
|
||||
showError(t("bizMonitorTpl.templateImportInvalid"));
|
||||
return;
|
||||
}
|
||||
let compareId = body.compare_template_id;
|
||||
if (!compareTpls.some((c) => c.id === compareId)) {
|
||||
compareId = "";
|
||||
}
|
||||
if (!compareId && body.compare_template_name) {
|
||||
const want = body.compare_template_name.trim().toLowerCase();
|
||||
const byName = compareTpls.find((c) => c.name.trim().toLowerCase() === want);
|
||||
compareId = byName?.id || "";
|
||||
}
|
||||
if (!compareId) {
|
||||
showError(t("bizMonitorTpl.templateImportCompareMissing"));
|
||||
return;
|
||||
}
|
||||
await bizMonitorCreateTemplate({
|
||||
name: body.name,
|
||||
note: body.note,
|
||||
compare_template_id: compareId,
|
||||
collect_metric_ids: body.collect_metric_ids,
|
||||
defaults: body.defaults,
|
||||
sheet_overrides: body.sheet_overrides,
|
||||
});
|
||||
showOk(t("bizMonitorTpl.templateImported"));
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (tplImportRef.current) tplImportRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const fieldChoices: MetricField[] =
|
||||
activeFields.length > 0
|
||||
? activeFields
|
||||
|
|
@ -982,6 +1104,24 @@ export function BizMonitorTemplatesPage() {
|
|||
<div className="panel__toolbar">
|
||||
<h2>{t("bizMonitorTpl.title")}</h2>
|
||||
<div className="btn-row">
|
||||
<input
|
||||
ref={tplImportRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) void importTemplateFile(f);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => tplImportRef.current?.click()}
|
||||
>
|
||||
{t("bizMonitorTpl.importTemplate")}
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" onPress={openCreate}>
|
||||
{t("bizMonitorTpl.create")}
|
||||
</Button>
|
||||
|
|
@ -990,6 +1130,8 @@ export function BizMonitorTemplatesPage() {
|
|||
<p className="panel__hint muted">{t("bizMonitorTpl.hint")}</p>
|
||||
<p className="muted" style={{ fontSize: 12, marginTop: -4 }}>
|
||||
<Link to="/network/cutover/compare-templates">{t("bizMonitorTpl.openCompare")}</Link>
|
||||
{" · "}
|
||||
{t("bizMonitorTpl.templateImportHint")}
|
||||
</p>
|
||||
|
||||
<div className="pt-list">
|
||||
|
|
@ -1035,6 +1177,9 @@ export function BizMonitorTemplatesPage() {
|
|||
<Button size="sm" variant="primary" onPress={() => openEdit(row)}>
|
||||
{t("bizMonitorTpl.edit")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onPress={() => exportTemplate(row)}>
|
||||
{t("bizMonitorTpl.exportTemplate")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" isDisabled={busy} onPress={() => void remove(row.id)}>
|
||||
{t("bizMonitorTpl.delete")}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type TaskRow = {
|
|||
ne_ip: string;
|
||||
vendor: string;
|
||||
note?: string;
|
||||
purpose?: string;
|
||||
status: string;
|
||||
collect_running: boolean;
|
||||
last_error: string;
|
||||
|
|
@ -257,6 +258,7 @@ export function BizStatePage() {
|
|||
const [busy, setBusy] = useState(false);
|
||||
const [listKeyword, setListKeyword] = useState("");
|
||||
const debouncedListKw = useDebouncedValue(listKeyword, 250);
|
||||
const [purposeFilter, setPurposeFilter] = useState<"all" | "portrait" | "cutover_hf">("all");
|
||||
|
||||
// create-task modal (all valid CLI targets)
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
|
@ -296,9 +298,11 @@ export function BizStatePage() {
|
|||
const debouncedSheetKw = useDebouncedValue(sheetKeyword, 200);
|
||||
|
||||
const refreshTasks = useCallback(async () => {
|
||||
const res = await bizStateListTasks();
|
||||
const purpose =
|
||||
purposeFilter === "all" ? "" : purposeFilter === "portrait" ? "portrait" : "cutover_hf";
|
||||
const res = await bizStateListTasks(purpose);
|
||||
setTasks((res.items || []) as TaskRow[]);
|
||||
}, []);
|
||||
}, [purposeFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
|
|
@ -308,7 +312,7 @@ export function BizStatePage() {
|
|||
showError(formatErr(e));
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- refresh when purpose filter / refreshTasks changes
|
||||
}, [refreshTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -344,7 +348,7 @@ export function BizStatePage() {
|
|||
if (!kw) return tasks;
|
||||
return tasks.filter((row) => {
|
||||
const blob =
|
||||
`${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.note || ""} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase();
|
||||
`${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.note || ""} ${row.purpose || ""} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase();
|
||||
return blob.includes(kw);
|
||||
});
|
||||
}, [tasks, debouncedListKw]);
|
||||
|
|
@ -740,6 +744,17 @@ export function BizStatePage() {
|
|||
placeholder={t("bizState.listFilterPh")}
|
||||
onChange={(e) => setListKeyword(e.target.value)}
|
||||
/>
|
||||
<FieldSelect
|
||||
value={purposeFilter}
|
||||
onChange={(e) =>
|
||||
setPurposeFilter(e.target.value as "all" | "portrait" | "cutover_hf")
|
||||
}
|
||||
aria-label={t("bizState.purposeFilter")}
|
||||
>
|
||||
<option value="all">{t("bizState.purposeAll")}</option>
|
||||
<option value="portrait">{t("bizState.purposePortrait")}</option>
|
||||
<option value="cutover_hf">{t("bizState.purposeCutoverHf")}</option>
|
||||
</FieldSelect>
|
||||
</div>
|
||||
|
||||
<div className="pt-list-table-wrap">
|
||||
|
|
@ -748,6 +763,7 @@ export function BizStatePage() {
|
|||
<tr>
|
||||
<th>{t("bizState.colNe")}</th>
|
||||
<th>{t("bizState.colSource")}</th>
|
||||
<th>{t("bizState.colPurpose")}</th>
|
||||
<th>{t("bizState.colInterval")}</th>
|
||||
<th>{t("bizState.scheduleEnabled")}</th>
|
||||
<th>{t("bizState.colStatus")}</th>
|
||||
|
|
@ -771,6 +787,15 @@ export function BizStatePage() {
|
|||
{row.source || "managed"}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
<NmStatusChip
|
||||
color={row.purpose === "cutover_hf" ? "accent" : "default"}
|
||||
>
|
||||
{row.purpose === "cutover_hf"
|
||||
? t("bizState.purposeCutoverHf")
|
||||
: t("bizState.purposePortrait")}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-num">
|
||||
{fmtIntervalLabel(
|
||||
Number(row.interval_sec || 3600),
|
||||
|
|
@ -824,7 +849,7 @@ export function BizStatePage() {
|
|||
for (let i = 0; i < 20; i++) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
await refreshTasks();
|
||||
const cur = (await bizStateListTasks()).items?.find(
|
||||
const cur = (await bizStateListTasks(purposeFilter === "all" ? "" : purposeFilter)).items?.find(
|
||||
(x: any) => x.id === row.id,
|
||||
);
|
||||
if (!cur?.collect_running) break;
|
||||
|
|
@ -853,7 +878,7 @@ export function BizStatePage() {
|
|||
))}
|
||||
{!filteredTasks.length ? (
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<td colSpan={8}>
|
||||
<div className="pt-list-empty">{t("bizState.empty")}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1790,8 +1790,10 @@ export const bizStateSetBindings = (
|
|||
{ bindings },
|
||||
);
|
||||
|
||||
export const bizStateListTasks = () =>
|
||||
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-state/tasks");
|
||||
export const bizStateListTasks = (purpose = "") => {
|
||||
const q = purpose ? `?purpose=${encodeURIComponent(purpose)}` : "";
|
||||
return apiGet<{ items: Record<string, unknown>[] }>(`/v1/biz-state/tasks${q}`);
|
||||
};
|
||||
|
||||
export const bizStateCreateTask = (body: Record<string, unknown>) =>
|
||||
apiPost<Record<string, unknown>>("/v1/biz-state/tasks", body);
|
||||
|
|
@ -1988,6 +1990,20 @@ export const bizCompareDownloadRun = async (runId: string): Promise<void> => {
|
|||
export const bizMigrationListProjects = () =>
|
||||
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-migration/projects");
|
||||
|
||||
export const bizMigrationNePortrait = (params: { source: string; ne_id: string; limit?: number }) => {
|
||||
const q = new URLSearchParams();
|
||||
q.set("source", params.source || "managed");
|
||||
q.set("ne_id", params.ne_id);
|
||||
if (params.limit != null) q.set("limit", String(params.limit));
|
||||
return apiGet<{
|
||||
source: string;
|
||||
ne_id: string;
|
||||
task: Record<string, unknown> | null;
|
||||
batches: Array<{ id: string; started_at?: string | null; status?: string; row_count?: number }>;
|
||||
hint?: string;
|
||||
}>(`/v1/biz-migration/ne-portrait?${q.toString()}`);
|
||||
};
|
||||
|
||||
export const bizMigrationCreateProject = (body: Record<string, unknown>) =>
|
||||
apiPost<Record<string, unknown>>("/v1/biz-migration/projects", body);
|
||||
|
||||
|
|
@ -1997,6 +2013,9 @@ export const bizMigrationPatchProject = (projectId: string, body: Record<string,
|
|||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationDeleteProject = (projectId: string) =>
|
||||
apiDelete<{ ok?: boolean }>(`/v1/biz-migration/projects/${encodeURIComponent(projectId)}`);
|
||||
|
||||
export const bizMigrationListBatches = (projectId: string) =>
|
||||
apiGet<{ items: Record<string, unknown>[] }>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/batches`,
|
||||
|
|
@ -2037,7 +2056,16 @@ export const bizMigrationListBaselineExpect = (projectId: string) =>
|
|||
|
||||
export const bizMigrationEnsureHighfreq = (
|
||||
projectId: string,
|
||||
body: { interval_sec?: number; retention_days?: number; collect_now?: boolean } = {},
|
||||
body: {
|
||||
interval_sec?: number;
|
||||
retention_days?: number;
|
||||
collect_now?: boolean;
|
||||
collect_metric_ids?: string[];
|
||||
hf_start_at?: string | null;
|
||||
hf_end_at?: string | null;
|
||||
old_ne?: Record<string, unknown>;
|
||||
new_ne?: Record<string, unknown>;
|
||||
} = {},
|
||||
) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/ensure-highfreq`,
|
||||
|
|
|
|||
|
|
@ -44,3 +44,31 @@ export function formatSystemTime(
|
|||
timeZone: systemTimeZone,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `<input type="datetime-local">` (browser system wall clock) → UTC ISO with `Z` for API/DB.
|
||||
* Global deployments store UTC; each operator edits in their local timezone.
|
||||
*/
|
||||
export function localDatetimeInputToUtcIso(localValue: string): string | null {
|
||||
const v = String(localValue || "").trim();
|
||||
if (!v) return null;
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/.exec(v);
|
||||
if (!m) return null;
|
||||
const y = Number(m[1]);
|
||||
const mo = Number(m[2]) - 1;
|
||||
const day = Number(m[3]);
|
||||
const h = Number(m[4]);
|
||||
const mi = Number(m[5]);
|
||||
const sec = Number(m[6] || 0);
|
||||
const local = new Date(y, mo, day, h, mi, sec);
|
||||
if (Number.isNaN(local.getTime())) return null;
|
||||
return local.toISOString();
|
||||
}
|
||||
|
||||
/** API/DB UTC (naive or `Z`) → `datetime-local` value in the browser system timezone. */
|
||||
export function utcIsoToLocalDatetimeInput(iso: string | null | undefined): string {
|
||||
const d = parseApiTime(iso);
|
||||
if (!d) return "";
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue