mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 22:10:48 +08:00
Add cutover-compare nav group and monitor templates.
Move biz monitor/compare/migration under Cutover compare, split compare templates into their own page, and add monitor-template CRUD that overlays compare templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f3a8f80822
commit
202196a197
13 changed files with 812 additions and 36 deletions
216
netx_api/biz_migration/monitor_templates.py
Normal file
216
netx_api/biz_migration/monitor_templates.py
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""CRUD for BizMonitorTemplate (cutover overlay on compare templates)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from ..models import BizCompareTemplate, BizMonitorTemplate
|
||||||
|
from ..timeutil import utcnow_naive
|
||||||
|
from ..biz_state import compare_service as cmp_svc
|
||||||
|
from .evaluate import PORT_METRIC_ID, PORT_STATUS_FIELDS
|
||||||
|
|
||||||
|
|
||||||
|
def _utcnow():
|
||||||
|
return utcnow_naive()
|
||||||
|
|
||||||
|
|
||||||
|
def _out(row: BizMonitorTemplate, compare_name: str = "") -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"name": row.name,
|
||||||
|
"compare_template_id": row.compare_template_id or "",
|
||||||
|
"compare_template_name": compare_name,
|
||||||
|
"collect_metric_ids": list(row.collect_metric_ids_json or []),
|
||||||
|
"defaults": dict(row.defaults_json or {}),
|
||||||
|
"sheet_overrides": list(row.sheet_overrides_json or []),
|
||||||
|
"note": row.note or "",
|
||||||
|
"created_at": row.created_at.isoformat() + "Z" if row.created_at else None,
|
||||||
|
"updated_at": row.updated_at.isoformat() + "Z" if row.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_name_map(db: Session) -> dict[str, str]:
|
||||||
|
rows = db.query(BizCompareTemplate.id, BizCompareTemplate.name).all()
|
||||||
|
return {str(r.id): str(r.name or "") for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_port_compare_template(db: Session) -> BizCompareTemplate:
|
||||||
|
"""Built-in HOW template for interface_brief (matches former port_sheet_def)."""
|
||||||
|
name = "Cutover port status"
|
||||||
|
row = db.query(BizCompareTemplate).filter(BizCompareTemplate.name == name).one_or_none()
|
||||||
|
sheet = {
|
||||||
|
"metric_id": PORT_METRIC_ID,
|
||||||
|
"key_fields": ["interface"],
|
||||||
|
"iface_fields": ["interface"],
|
||||||
|
"compare_fields": list(PORT_STATUS_FIELDS),
|
||||||
|
"display_fields": ["interface", *PORT_STATUS_FIELDS, "description"],
|
||||||
|
"row_filters": [],
|
||||||
|
"field_rules": [],
|
||||||
|
}
|
||||||
|
if row:
|
||||||
|
return row
|
||||||
|
row = BizCompareTemplate(
|
||||||
|
id=uuid4().hex,
|
||||||
|
name=name,
|
||||||
|
note="Built-in port status sheet for cutover monitor (admin/phy/prot)",
|
||||||
|
created_at=_utcnow(),
|
||||||
|
updated_at=_utcnow(),
|
||||||
|
)
|
||||||
|
cmp_svc._apply_sheets_to_row(row, [sheet]) # noqa: SLF001 — shared normalizer
|
||||||
|
db.add(row)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_default_monitor_templates(db: Session) -> None:
|
||||||
|
"""Seed once when monitor-template table is empty."""
|
||||||
|
if db.query(BizMonitorTemplate.id).limit(1).first():
|
||||||
|
return
|
||||||
|
cmp_svc.ensure_default_templates(db)
|
||||||
|
port_tpl = ensure_port_compare_template(db)
|
||||||
|
zte = (
|
||||||
|
db.query(BizCompareTemplate)
|
||||||
|
.filter(BizCompareTemplate.name == "ZTE status default")
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
seeds = [
|
||||||
|
BizMonitorTemplate(
|
||||||
|
id=uuid4().hex,
|
||||||
|
name="端口割接监控",
|
||||||
|
compare_template_id=port_tpl.id,
|
||||||
|
collect_metric_ids_json=[PORT_METRIC_ID],
|
||||||
|
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||||
|
sheet_overrides_json=[
|
||||||
|
{
|
||||||
|
"metric_id": PORT_METRIC_ID,
|
||||||
|
"status_fields": list(PORT_STATUS_FIELDS),
|
||||||
|
"down_values": ["down"],
|
||||||
|
"up_values": ["up"],
|
||||||
|
"success": [
|
||||||
|
{
|
||||||
|
"old": ["removed", "down"],
|
||||||
|
"new": ["added", "up", "unchanged"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
note="Default: port status dual-verdict with up/down semantics",
|
||||||
|
created_at=_utcnow(),
|
||||||
|
updated_at=_utcnow(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if zte:
|
||||||
|
seeds.append(
|
||||||
|
BizMonitorTemplate(
|
||||||
|
id=uuid4().hex,
|
||||||
|
name="ZTE 状态割接监控",
|
||||||
|
compare_template_id=zte.id,
|
||||||
|
collect_metric_ids_json=[],
|
||||||
|
defaults_json={"dual_mode": "migrate_pair", "out_of_expect": "strict"},
|
||||||
|
sheet_overrides_json=[],
|
||||||
|
note="Uses ZTE status compare template; presence dual-verdict",
|
||||||
|
created_at=_utcnow(),
|
||||||
|
updated_at=_utcnow(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for s in seeds:
|
||||||
|
db.add(s)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def list_monitor_templates(db: Session) -> list[dict[str, Any]]:
|
||||||
|
ensure_default_monitor_templates(db)
|
||||||
|
names = _compare_name_map(db)
|
||||||
|
rows = db.query(BizMonitorTemplate).order_by(BizMonitorTemplate.name.asc()).all()
|
||||||
|
return [_out(r, names.get(r.compare_template_id or "", "")) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_monitor_template(db: Session, template_id: str) -> dict[str, Any]:
|
||||||
|
row = db.get(BizMonitorTemplate, template_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||||
|
names = _compare_name_map(db)
|
||||||
|
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def create_monitor_template(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
name = str(body.get("name") or "").strip()
|
||||||
|
if not name:
|
||||||
|
raise HTTPException(status_code=400, detail="name_required")
|
||||||
|
compare_id = str(body.get("compare_template_id") or "").strip()
|
||||||
|
if compare_id and not db.get(BizCompareTemplate, compare_id):
|
||||||
|
raise HTTPException(status_code=404, detail="compare_template_not_found")
|
||||||
|
collect = body.get("collect_metric_ids")
|
||||||
|
if not isinstance(collect, list):
|
||||||
|
collect = []
|
||||||
|
defaults = body.get("defaults")
|
||||||
|
if not isinstance(defaults, dict):
|
||||||
|
defaults = {"dual_mode": "migrate_pair", "out_of_expect": "strict"}
|
||||||
|
overrides = body.get("sheet_overrides")
|
||||||
|
if not isinstance(overrides, list):
|
||||||
|
overrides = []
|
||||||
|
row = BizMonitorTemplate(
|
||||||
|
id=uuid4().hex,
|
||||||
|
name=name[:256],
|
||||||
|
compare_template_id=compare_id,
|
||||||
|
collect_metric_ids_json=[str(x) for x in collect if str(x).strip()],
|
||||||
|
defaults_json=defaults,
|
||||||
|
sheet_overrides_json=overrides,
|
||||||
|
note=str(body.get("note") or "")[:512],
|
||||||
|
created_at=_utcnow(),
|
||||||
|
updated_at=_utcnow(),
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(row)
|
||||||
|
names = _compare_name_map(db)
|
||||||
|
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def update_monitor_template(db: Session, template_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
row = db.get(BizMonitorTemplate, template_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||||
|
if "name" in body and body["name"] is not None:
|
||||||
|
name = str(body["name"] or "").strip()
|
||||||
|
if not name:
|
||||||
|
raise HTTPException(status_code=400, detail="name_required")
|
||||||
|
row.name = name[:256]
|
||||||
|
if "compare_template_id" in body and body["compare_template_id"] is not None:
|
||||||
|
compare_id = str(body["compare_template_id"] or "").strip()
|
||||||
|
if compare_id and not db.get(BizCompareTemplate, compare_id):
|
||||||
|
raise HTTPException(status_code=404, detail="compare_template_not_found")
|
||||||
|
row.compare_template_id = compare_id
|
||||||
|
if "collect_metric_ids" in body and body["collect_metric_ids"] is not None:
|
||||||
|
collect = body["collect_metric_ids"]
|
||||||
|
if not isinstance(collect, list):
|
||||||
|
raise HTTPException(status_code=400, detail="collect_metric_ids_invalid")
|
||||||
|
row.collect_metric_ids_json = [str(x) for x in collect if str(x).strip()]
|
||||||
|
if "defaults" in body and body["defaults"] is not None:
|
||||||
|
if not isinstance(body["defaults"], dict):
|
||||||
|
raise HTTPException(status_code=400, detail="defaults_invalid")
|
||||||
|
row.defaults_json = body["defaults"]
|
||||||
|
if "sheet_overrides" in body and body["sheet_overrides"] is not None:
|
||||||
|
if not isinstance(body["sheet_overrides"], list):
|
||||||
|
raise HTTPException(status_code=400, detail="sheet_overrides_invalid")
|
||||||
|
row.sheet_overrides_json = body["sheet_overrides"]
|
||||||
|
if "note" in body and body["note"] is not None:
|
||||||
|
row.note = str(body["note"] or "")[:512]
|
||||||
|
row.updated_at = _utcnow()
|
||||||
|
db.commit()
|
||||||
|
db.refresh(row)
|
||||||
|
names = _compare_name_map(db)
|
||||||
|
return _out(row, names.get(row.compare_template_id or "", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_monitor_template(db: Session, template_id: str) -> None:
|
||||||
|
row = db.get(BizMonitorTemplate, template_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="monitor_template_not_found")
|
||||||
|
db.delete(row)
|
||||||
|
db.commit()
|
||||||
|
|
@ -9,6 +9,7 @@ from pydantic import BaseModel, Field
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .biz_migration import service as svc
|
from .biz_migration import service as svc
|
||||||
|
from .biz_migration import monitor_templates as mon_tpl
|
||||||
from .db import get_db
|
from .db import get_db
|
||||||
|
|
||||||
router = APIRouter(prefix="/v1/biz-migration", tags=["biz-migration"])
|
router = APIRouter(prefix="/v1/biz-migration", tags=["biz-migration"])
|
||||||
|
|
@ -41,6 +42,24 @@ class BatchIn(BaseModel):
|
||||||
note: str = ""
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorTemplateIn(BaseModel):
|
||||||
|
name: str = ""
|
||||||
|
compare_template_id: str = ""
|
||||||
|
collect_metric_ids: list[str] = Field(default_factory=list)
|
||||||
|
defaults: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
sheet_overrides: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorTemplatePatchIn(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
compare_template_id: str | None = None
|
||||||
|
collect_metric_ids: list[str] | None = None
|
||||||
|
defaults: dict[str, Any] | None = None
|
||||||
|
sheet_overrides: list[dict[str, Any]] | None = None
|
||||||
|
note: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class BatchPatchIn(BaseModel):
|
class BatchPatchIn(BaseModel):
|
||||||
batch_label: str | None = None
|
batch_label: str | None = None
|
||||||
expect_set: dict[str, Any] | None = None
|
expect_set: dict[str, Any] | None = None
|
||||||
|
|
@ -209,3 +228,35 @@ def api_list_diffs(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/monitor-templates")
|
||||||
|
def api_list_monitor_templates(db: Session = Depends(get_db)):
|
||||||
|
return {"items": mon_tpl.list_monitor_templates(db)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/monitor-templates")
|
||||||
|
def api_create_monitor_template(body: MonitorTemplateIn, db: Session = Depends(get_db)):
|
||||||
|
return mon_tpl.create_monitor_template(db, body.model_dump())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/monitor-templates/{template_id}")
|
||||||
|
def api_get_monitor_template(template_id: str, db: Session = Depends(get_db)):
|
||||||
|
return mon_tpl.get_monitor_template(db, template_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/monitor-templates/{template_id}")
|
||||||
|
def api_patch_monitor_template(
|
||||||
|
template_id: str,
|
||||||
|
body: MonitorTemplatePatchIn,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
return mon_tpl.update_monitor_template(
|
||||||
|
db, template_id, body.model_dump(exclude_unset=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/monitor-templates/{template_id}")
|
||||||
|
def api_delete_monitor_template(template_id: str, db: Session = Depends(get_db)):
|
||||||
|
mon_tpl.delete_monitor_template(db, template_id)
|
||||||
|
return {"ok": True}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,19 @@ def apply_biz_state_schema(conn: Connection) -> None:
|
||||||
"ALTER TABLE biz_migration_batch ADD COLUMN IF NOT EXISTS accept_summary_json JSON DEFAULT '{}'",
|
"ALTER TABLE biz_migration_batch ADD COLUMN IF NOT EXISTS accept_summary_json JSON DEFAULT '{}'",
|
||||||
"ALTER TABLE biz_migration_run ADD COLUMN IF NOT EXISTS purpose VARCHAR(32) DEFAULT 'manual'",
|
"ALTER TABLE biz_migration_run ADD COLUMN IF NOT EXISTS purpose VARCHAR(32) DEFAULT 'manual'",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_migration_red_project_status ON biz_migration_red_ticket (project_id, status)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_migration_red_project_status ON biz_migration_red_ticket (project_id, status)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS biz_monitor_template ("
|
||||||
|
"id VARCHAR(64) PRIMARY KEY,"
|
||||||
|
"name VARCHAR(256) DEFAULT '',"
|
||||||
|
"compare_template_id VARCHAR(64) DEFAULT '',"
|
||||||
|
"collect_metric_ids_json JSON DEFAULT '[]',"
|
||||||
|
"defaults_json JSON DEFAULT '{}',"
|
||||||
|
"sheet_overrides_json JSON DEFAULT '[]',"
|
||||||
|
"note VARCHAR(512) DEFAULT '',"
|
||||||
|
"created_at TIMESTAMP,"
|
||||||
|
"updated_at TIMESTAMP"
|
||||||
|
")",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_biz_monitor_template_name ON biz_monitor_template (name)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_biz_monitor_template_compare ON biz_monitor_template (compare_template_id)",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
_run_sql(conn, sql)
|
_run_sql(conn, sql)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from .biz_state import (
|
||||||
BizCompareJob,
|
BizCompareJob,
|
||||||
BizCompareRun,
|
BizCompareRun,
|
||||||
BizCompareTemplate,
|
BizCompareTemplate,
|
||||||
|
BizMonitorTemplate,
|
||||||
BizPortMapping,
|
BizPortMapping,
|
||||||
BizPortMappingRow,
|
BizPortMappingRow,
|
||||||
BizStateBatch,
|
BizStateBatch,
|
||||||
|
|
@ -148,6 +149,7 @@ __all__ = [
|
||||||
"BizStateEvent",
|
"BizStateEvent",
|
||||||
"BizStateCommandOverride",
|
"BizStateCommandOverride",
|
||||||
"BizCompareTemplate",
|
"BizCompareTemplate",
|
||||||
|
"BizMonitorTemplate",
|
||||||
"BizPortMapping",
|
"BizPortMapping",
|
||||||
"BizPortMappingRow",
|
"BizPortMappingRow",
|
||||||
"BizCompareJob",
|
"BizCompareJob",
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,27 @@ class BizCompareTemplate(Base):
|
||||||
ignore_fields: Mapped[list] = mapped_column(_JsonType, default=list)
|
ignore_fields: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||||
metrics_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
metrics_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||||
note: Mapped[str] = mapped_column(String(512), default="")
|
note: Mapped[str] = mapped_column(String(512), default="")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive, index=True)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||||
|
|
||||||
|
|
||||||
|
class BizMonitorTemplate(Base):
|
||||||
|
"""Cutover/monitor overlay on a BizCompareTemplate (HOW + WHEN rules).
|
||||||
|
|
||||||
|
``compare_template_id`` selects sheets / field rules.
|
||||||
|
``defaults_json`` / ``sheet_overrides_json`` carry dual-verdict & status semantics.
|
||||||
|
``collect_metric_ids_json`` optional HF collect subset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_monitor_template"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex)
|
||||||
|
name: Mapped[str] = mapped_column(String(256), default="", index=True)
|
||||||
|
compare_template_id: Mapped[str] = mapped_column(String(64), default="", index=True)
|
||||||
|
collect_metric_ids_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||||
|
defaults_json: Mapped[dict] = mapped_column(_JsonType, default=dict)
|
||||||
|
sheet_overrides_json: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||||
|
note: Mapped[str] = mapped_column(String(512), default="")
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,11 @@ const BizComparePage = lazy(() =>
|
||||||
const BizMigrationPage = lazy(() =>
|
const BizMigrationPage = lazy(() =>
|
||||||
import("./pages/network/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
|
import("./pages/network/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
|
||||||
);
|
);
|
||||||
|
const BizMonitorTemplatesPage = lazy(() =>
|
||||||
|
import("./pages/network/BizMonitorTemplatesPage").then((m) => ({
|
||||||
|
default: m.BizMonitorTemplatesPage,
|
||||||
|
})),
|
||||||
|
);
|
||||||
const UsersPage = lazy(() => import("./pages/UsersPage").then((m) => ({ default: m.UsersPage })));
|
const UsersPage = lazy(() => import("./pages/UsersPage").then((m) => ({ default: m.UsersPage })));
|
||||||
const AuditLayout = lazy(() =>
|
const AuditLayout = lazy(() =>
|
||||||
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
||||||
|
|
@ -129,9 +134,23 @@ function ProtectedApp() {
|
||||||
<Route path="tasks/config-sync" element={<ConfigSyncPage />} />
|
<Route path="tasks/config-sync" element={<ConfigSyncPage />} />
|
||||||
<Route path="tasks/port-traffic/wall" element={<LegacyPortTrafficWallRedirect />} />
|
<Route path="tasks/port-traffic/wall" element={<LegacyPortTrafficWallRedirect />} />
|
||||||
<Route path="tasks/port-traffic" element={<PortTrafficPage />} />
|
<Route path="tasks/port-traffic" element={<PortTrafficPage />} />
|
||||||
<Route path="tasks/biz-state" element={<BizStatePage />} />
|
<Route path="tasks/biz-state" element={<Navigate to="/network/cutover/biz-state" replace />} />
|
||||||
<Route path="tasks/biz-compare" element={<BizComparePage />} />
|
<Route
|
||||||
<Route path="tasks/biz-migration" element={<BizMigrationPage />} />
|
path="tasks/biz-compare"
|
||||||
|
element={<Navigate to="/network/cutover/biz-compare" replace />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="tasks/biz-migration"
|
||||||
|
element={<Navigate to="/network/cutover/biz-migration" replace />}
|
||||||
|
/>
|
||||||
|
<Route path="cutover/biz-state" element={<BizStatePage />} />
|
||||||
|
<Route path="cutover/biz-compare" element={<BizComparePage pageMode="jobs" />} />
|
||||||
|
<Route path="cutover/biz-migration" element={<BizMigrationPage />} />
|
||||||
|
<Route
|
||||||
|
path="cutover/compare-templates"
|
||||||
|
element={<BizComparePage pageMode="templates" />}
|
||||||
|
/>
|
||||||
|
<Route path="cutover/monitor-templates" element={<BizMonitorTemplatesPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
||||||
<Route path="/users" element={<UsersPage />} />
|
<Route path="/users" element={<UsersPage />} />
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Left-nav tree for the Network Management module.
|
* Left-nav tree for the Network Management module.
|
||||||
* Add leaf items here when extending (devices / topology / alarms / tasks).
|
* Add leaf items here when extending (devices / topology / alarms / tasks / cutover).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type NetworkNavGroupId = "detail" | "topologyMgmt" | "tasks";
|
export type NetworkNavGroupId = "detail" | "topologyMgmt" | "tasks" | "cutoverCompare";
|
||||||
|
|
||||||
export type NetworkNavItem = {
|
export type NetworkNavItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -74,23 +74,41 @@ export const NETWORK_NAV: readonly NetworkNavGroup[] = [
|
||||||
labelKey: "network.nav.portTrafficWall",
|
labelKey: "network.nav.portTrafficWall",
|
||||||
group: "tasks",
|
group: "tasks",
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cutoverCompare",
|
||||||
|
labelKey: "network.nav.cutoverCompare",
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
id: "biz-state",
|
id: "biz-state",
|
||||||
path: "/network/tasks/biz-state",
|
path: "/network/cutover/biz-state",
|
||||||
labelKey: "network.nav.bizState",
|
labelKey: "network.nav.bizState",
|
||||||
group: "tasks",
|
group: "cutoverCompare",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "biz-compare",
|
id: "biz-compare",
|
||||||
path: "/network/tasks/biz-compare",
|
path: "/network/cutover/biz-compare",
|
||||||
labelKey: "network.nav.bizCompare",
|
labelKey: "network.nav.bizCompare",
|
||||||
group: "tasks",
|
group: "cutoverCompare",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "biz-migration",
|
id: "biz-migration",
|
||||||
path: "/network/tasks/biz-migration",
|
path: "/network/cutover/biz-migration",
|
||||||
labelKey: "network.nav.bizMigration",
|
labelKey: "network.nav.bizMigration",
|
||||||
group: "tasks",
|
group: "cutoverCompare",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "compare-templates",
|
||||||
|
path: "/network/cutover/compare-templates",
|
||||||
|
labelKey: "network.nav.compareTemplates",
|
||||||
|
group: "cutoverCompare",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "monitor-templates",
|
||||||
|
path: "/network/cutover/monitor-templates",
|
||||||
|
labelKey: "network.nav.monitorTemplates",
|
||||||
|
group: "cutoverCompare",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ const en = {
|
||||||
nav: {
|
nav: {
|
||||||
detail: "Network details",
|
detail: "Network details",
|
||||||
tasks: "Monitoring tasks",
|
tasks: "Monitoring tasks",
|
||||||
|
cutoverCompare: "Cutover compare",
|
||||||
topologyMgmt: "Topology management",
|
topologyMgmt: "Topology management",
|
||||||
devices: "Device list",
|
devices: "Device list",
|
||||||
topology: "Topology",
|
topology: "Topology",
|
||||||
|
|
@ -159,6 +160,8 @@ const en = {
|
||||||
bizState: "Business monitor",
|
bizState: "Business monitor",
|
||||||
bizCompare: "Business compare",
|
bizCompare: "Business compare",
|
||||||
bizMigration: "Cutover monitor",
|
bizMigration: "Cutover monitor",
|
||||||
|
compareTemplates: "Compare templates",
|
||||||
|
monitorTemplates: "Monitor templates",
|
||||||
},
|
},
|
||||||
collapseNav: "Collapse sidebar",
|
collapseNav: "Collapse sidebar",
|
||||||
expandNav: "Expand sidebar",
|
expandNav: "Expand sidebar",
|
||||||
|
|
@ -568,6 +571,40 @@ const en = {
|
||||||
colStatus: "Status",
|
colStatus: "Status",
|
||||||
verdictUnfinished: "Unfinished",
|
verdictUnfinished: "Unfinished",
|
||||||
},
|
},
|
||||||
|
bizMonitorTpl: {
|
||||||
|
title: "Monitor templates",
|
||||||
|
hint: "Reference a compare template for HOW-to-compare, then add dual-device / baseline rules for cutover monitor.",
|
||||||
|
create: "New monitor template",
|
||||||
|
edit: "Edit",
|
||||||
|
save: "Save",
|
||||||
|
cancel: "Close",
|
||||||
|
created: "Created",
|
||||||
|
saved: "Saved",
|
||||||
|
deleted: "Deleted",
|
||||||
|
confirmDelete: "Delete this monitor template?",
|
||||||
|
delete: "Delete",
|
||||||
|
empty: "No monitor templates yet.",
|
||||||
|
listFilterPh: "Filter name / compare template / note",
|
||||||
|
kpiTotal: "Templates",
|
||||||
|
colName: "Name",
|
||||||
|
colCompare: "Compare template",
|
||||||
|
colCollect: "HF collect metrics",
|
||||||
|
colNote: "Note",
|
||||||
|
colActions: "Actions",
|
||||||
|
needName: "Name is required",
|
||||||
|
defaultsInvalid: "Invalid defaults JSON",
|
||||||
|
overridesInvalid: "Invalid sheet_overrides JSON",
|
||||||
|
formHint: "Compare template owns alignment & field rules; this page owns dual verdict + optional HF collect subset.",
|
||||||
|
compareHint: "From Compare templates; create/edit sheets there first if needed.",
|
||||||
|
pickCompare: "Pick compare template…",
|
||||||
|
openCompare: "Manage compare templates",
|
||||||
|
collectAll: "All sheets",
|
||||||
|
collectPh: "interface_brief, bgp_peer (empty = all)",
|
||||||
|
collectHint: "Comma-separated metric_id; empty uses every sheet on the compare template.",
|
||||||
|
defaults: "defaults (JSON)",
|
||||||
|
overrides: "sheet_overrides (JSON)",
|
||||||
|
overridesHint: "Optional per-metric status_fields / success patterns; empty array uses global defaults.",
|
||||||
|
},
|
||||||
portTraffic: {
|
portTraffic: {
|
||||||
title: "Port traffic",
|
title: "Port traffic",
|
||||||
create: "Monitor device",
|
create: "Monitor device",
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ const zh = {
|
||||||
nav: {
|
nav: {
|
||||||
detail: "网络详情",
|
detail: "网络详情",
|
||||||
tasks: "监控任务",
|
tasks: "监控任务",
|
||||||
|
cutoverCompare: "割接对比",
|
||||||
topologyMgmt: "拓扑管理",
|
topologyMgmt: "拓扑管理",
|
||||||
devices: "设备列表",
|
devices: "设备列表",
|
||||||
topology: "拓扑信息",
|
topology: "拓扑信息",
|
||||||
|
|
@ -159,6 +160,8 @@ const zh = {
|
||||||
bizState: "业务监控",
|
bizState: "业务监控",
|
||||||
bizCompare: "业务对比",
|
bizCompare: "业务对比",
|
||||||
bizMigration: "割接监控",
|
bizMigration: "割接监控",
|
||||||
|
compareTemplates: "对比模板",
|
||||||
|
monitorTemplates: "监控模板",
|
||||||
},
|
},
|
||||||
collapseNav: "折叠侧栏",
|
collapseNav: "折叠侧栏",
|
||||||
expandNav: "展开侧栏",
|
expandNav: "展开侧栏",
|
||||||
|
|
@ -565,6 +568,40 @@ const zh = {
|
||||||
colStatus: "状态",
|
colStatus: "状态",
|
||||||
verdictUnfinished: "未完成",
|
verdictUnfinished: "未完成",
|
||||||
},
|
},
|
||||||
|
bizMonitorTpl: {
|
||||||
|
title: "监控模板",
|
||||||
|
hint: "引用对比模板定义「如何比」,再叠加双端迁移/基线判定规则。供割接监控选用。",
|
||||||
|
create: "新建监控模板",
|
||||||
|
edit: "编辑",
|
||||||
|
save: "保存",
|
||||||
|
cancel: "关闭",
|
||||||
|
created: "已创建",
|
||||||
|
saved: "已保存",
|
||||||
|
deleted: "已删除",
|
||||||
|
confirmDelete: "删除该监控模板?",
|
||||||
|
delete: "删除",
|
||||||
|
empty: "暂无监控模板。",
|
||||||
|
listFilterPh: "筛选名称 / 对比模板 / 备注",
|
||||||
|
kpiTotal: "模板数",
|
||||||
|
colName: "名称",
|
||||||
|
colCompare: "对比模板",
|
||||||
|
colCollect: "高频采集项",
|
||||||
|
colNote: "备注",
|
||||||
|
colActions: "操作",
|
||||||
|
needName: "请填写名称",
|
||||||
|
defaultsInvalid: "defaults JSON 无效",
|
||||||
|
overridesInvalid: "sheet_overrides JSON 无效",
|
||||||
|
formHint: "对比模板决定对齐与字段规则;本页配置双端判定与可选高频采集子集。",
|
||||||
|
compareHint: "来自「对比模板」;可先去那边新建或编辑 sheets。",
|
||||||
|
pickCompare: "选择对比模板…",
|
||||||
|
openCompare: "管理对比模板",
|
||||||
|
collectAll: "模板全量",
|
||||||
|
collectPh: "interface_brief, bgp_peer(空=全量)",
|
||||||
|
collectHint: "逗号分隔 metric_id;留空表示使用对比模板全部 sheets。",
|
||||||
|
defaults: "defaults(JSON)",
|
||||||
|
overrides: "sheet_overrides(JSON)",
|
||||||
|
overridesHint: "可按 metric 声明 status_fields / success 模式;空数组则用全局 defaults。",
|
||||||
|
},
|
||||||
portTraffic: {
|
portTraffic: {
|
||||||
title: "端口流量监控",
|
title: "端口流量监控",
|
||||||
create: "添加设备监控",
|
create: "添加设备监控",
|
||||||
|
|
|
||||||
|
|
@ -529,11 +529,14 @@ function metricLabel(id: string) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BizComparePage() {
|
export type BizComparePageMode = "jobs" | "templates" | "all";
|
||||||
|
|
||||||
|
export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePageMode }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showOk, showError } = useToast();
|
const { showOk, showError } = useToast();
|
||||||
|
|
||||||
const [pageTab, setPageTab] = useState<PageTab>("jobs");
|
const [pageTab, setPageTab] = useState<PageTab>(pageMode === "templates" ? "templates" : "jobs");
|
||||||
|
const showTabSwitch = pageMode === "all";
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const [tasks, setTasks] = useState<TaskOpt[]>([]);
|
const [tasks, setTasks] = useState<TaskOpt[]>([]);
|
||||||
|
|
@ -1094,7 +1097,7 @@ export function BizComparePage() {
|
||||||
});
|
});
|
||||||
showOk(t("bizCompare.templateImported"));
|
showOk(t("bizCompare.templateImported"));
|
||||||
await refresh();
|
await refresh();
|
||||||
setPageTab("templates");
|
if (pageMode !== "jobs") setPageTab("templates");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -1453,7 +1456,9 @@ export function BizComparePage() {
|
||||||
return (
|
return (
|
||||||
<section className="panel nm-page-panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("bizCompare.title")}</h2>
|
<h2>
|
||||||
|
{pageMode === "templates" ? t("bizCompare.templates") : t("bizCompare.title")}
|
||||||
|
</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
{pageTab === "templates" ? (
|
{pageTab === "templates" ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1488,24 +1493,26 @@ export function BizComparePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-list">
|
<div className="pt-list">
|
||||||
<div className="btn-row nm-config-modal__tabs" role="tablist">
|
{showTabSwitch ? (
|
||||||
<Button
|
<div className="btn-row nm-config-modal__tabs" role="tablist">
|
||||||
size="sm"
|
<Button
|
||||||
variant={pageTab === "jobs" ? "primary" : "secondary"}
|
size="sm"
|
||||||
className={pageTab === "jobs" ? "is-active" : undefined}
|
variant={pageTab === "jobs" ? "primary" : "secondary"}
|
||||||
onPress={() => setPageTab("jobs")}
|
className={pageTab === "jobs" ? "is-active" : undefined}
|
||||||
>
|
onPress={() => setPageTab("jobs")}
|
||||||
{t("bizCompare.jobList")}
|
>
|
||||||
</Button>
|
{t("bizCompare.jobList")}
|
||||||
<Button
|
</Button>
|
||||||
size="sm"
|
<Button
|
||||||
variant={pageTab === "templates" ? "primary" : "secondary"}
|
size="sm"
|
||||||
className={pageTab === "templates" ? "is-active" : undefined}
|
variant={pageTab === "templates" ? "primary" : "secondary"}
|
||||||
onPress={() => setPageTab("templates")}
|
className={pageTab === "templates" ? "is-active" : undefined}
|
||||||
>
|
onPress={() => setPageTab("templates")}
|
||||||
{t("bizCompare.templates")}
|
>
|
||||||
</Button>
|
{t("bizCompare.templates")}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="filter-inline">
|
<div className="filter-inline">
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -688,7 +688,12 @@ export function BizMigrationPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="panel__hint muted">{t("bizMigration.hintPort")}</p>
|
<p className="panel__hint muted">
|
||||||
|
{t("bizMigration.hintPort")}{" "}
|
||||||
|
<Link to="/network/cutover/monitor-templates">{t("network.nav.monitorTemplates")}</Link>
|
||||||
|
{" · "}
|
||||||
|
<Link to="/network/cutover/compare-templates">{t("network.nav.compareTemplates")}</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="pt-list">
|
<div className="pt-list">
|
||||||
<div className="pt-list-kpis">
|
<div className="pt-list-kpis">
|
||||||
|
|
@ -838,7 +843,7 @@ export function BizMigrationPage() {
|
||||||
{!tasks.length ? (
|
{!tasks.length ? (
|
||||||
<p className="muted" style={{ margin: 0, fontSize: 12 }}>
|
<p className="muted" style={{ margin: 0, fontSize: 12 }}>
|
||||||
{t("bizMigration.needBizStateTasks")}{" "}
|
{t("bizMigration.needBizStateTasks")}{" "}
|
||||||
<Link to="/network/tasks/biz-state" onClick={closeCreate}>
|
<Link to="/network/cutover/biz-state" onClick={closeCreate}>
|
||||||
{t("bizMigration.openBizState")}
|
{t("bizMigration.openBizState")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -941,7 +946,7 @@ export function BizMigrationPage() {
|
||||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void onCollectNow()}>
|
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void onCollectNow()}>
|
||||||
{t("bizMigration.collectNow")}
|
{t("bizMigration.collectNow")}
|
||||||
</Button>
|
</Button>
|
||||||
<Link to="/network/tasks/biz-state" style={{ fontSize: 12, alignSelf: "center" }}>
|
<Link to="/network/cutover/biz-state" style={{ fontSize: 12, alignSelf: "center" }}>
|
||||||
{t("bizMigration.openBizState")}
|
{t("bizMigration.openBizState")}
|
||||||
</Link>
|
</Link>
|
||||||
{openRedCount > 0 ? (
|
{openRedCount > 0 ? (
|
||||||
|
|
|
||||||
333
web/src/pages/network/BizMonitorTemplatesPage.tsx
Normal file
333
web/src/pages/network/BizMonitorTemplatesPage.tsx
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
import { Button, Input, Modal } from "@heroui/react";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
|
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
||||||
|
import { useToast } from "../../hooks/useToast";
|
||||||
|
import { useI18n } from "../../i18n";
|
||||||
|
import {
|
||||||
|
bizCompareListTemplates,
|
||||||
|
bizMonitorCreateTemplate,
|
||||||
|
bizMonitorDeleteTemplate,
|
||||||
|
bizMonitorListTemplates,
|
||||||
|
bizMonitorUpdateTemplate,
|
||||||
|
formatErr,
|
||||||
|
} from "../../services/api";
|
||||||
|
|
||||||
|
type CompareTpl = { id: string; name: string };
|
||||||
|
type MonitorTpl = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
compare_template_id: string;
|
||||||
|
compare_template_name?: string;
|
||||||
|
collect_metric_ids?: string[];
|
||||||
|
defaults?: Record<string, unknown>;
|
||||||
|
sheet_overrides?: unknown[];
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BizMonitorTemplatesPage() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { showOk, showError } = useToast();
|
||||||
|
|
||||||
|
const [items, setItems] = useState<MonitorTpl[]>([]);
|
||||||
|
const [compareTpls, setCompareTpls] = useState<CompareTpl[]>([]);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [listKw, setListKw] = useState("");
|
||||||
|
const debouncedKw = useDebouncedValue(listKw, 250);
|
||||||
|
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [editId, setEditId] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [compareId, setCompareId] = useState("");
|
||||||
|
const [note, setNote] = useState("");
|
||||||
|
const [collectText, setCollectText] = useState("");
|
||||||
|
const [defaultsText, setDefaultsText] = useState('{"dual_mode":"migrate_pair","out_of_expect":"strict"}');
|
||||||
|
const [overridesText, setOverridesText] = useState("[]");
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
const [mon, cmp] = await Promise.all([bizMonitorListTemplates(), bizCompareListTemplates()]);
|
||||||
|
setItems((mon.items || []) as MonitorTpl[]);
|
||||||
|
setCompareTpls(
|
||||||
|
((cmp.items || []) as Record<string, unknown>[]).map((x) => ({
|
||||||
|
id: String(x.id || ""),
|
||||||
|
name: String(x.name || x.id || ""),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [refresh, showError]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const kw = debouncedKw.trim().toLowerCase();
|
||||||
|
if (!kw) return items;
|
||||||
|
return items.filter((row) => {
|
||||||
|
const blob = `${row.name} ${row.compare_template_name || ""} ${row.note || ""} ${row.compare_template_id}`.toLowerCase();
|
||||||
|
return blob.includes(kw);
|
||||||
|
});
|
||||||
|
}, [items, debouncedKw]);
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditId("");
|
||||||
|
setName("");
|
||||||
|
setCompareId(compareTpls[0]?.id || "");
|
||||||
|
setNote("");
|
||||||
|
setCollectText("");
|
||||||
|
setDefaultsText('{"dual_mode":"migrate_pair","out_of_expect":"strict"}');
|
||||||
|
setOverridesText("[]");
|
||||||
|
setEditOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (row: MonitorTpl) => {
|
||||||
|
setEditId(row.id);
|
||||||
|
setName(row.name || "");
|
||||||
|
setCompareId(row.compare_template_id || "");
|
||||||
|
setNote(row.note || "");
|
||||||
|
setCollectText((row.collect_metric_ids || []).join(", "));
|
||||||
|
setDefaultsText(JSON.stringify(row.defaults || {}, null, 2));
|
||||||
|
setOverridesText(JSON.stringify(row.sheet_overrides || [], null, 2));
|
||||||
|
setEditOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEdit = () => setEditOpen(false);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
showError(t("bizMonitorTpl.needName"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let defaults: Record<string, unknown> = {};
|
||||||
|
let overrides: unknown[] = [];
|
||||||
|
try {
|
||||||
|
defaults = JSON.parse(defaultsText || "{}") as Record<string, unknown>;
|
||||||
|
if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
|
||||||
|
throw new Error("defaults");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError(t("bizMonitorTpl.defaultsInvalid"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
overrides = JSON.parse(overridesText || "[]") as unknown[];
|
||||||
|
if (!Array.isArray(overrides)) throw new Error("overrides");
|
||||||
|
} catch {
|
||||||
|
showError(t("bizMonitorTpl.overridesInvalid"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collect_metric_ids = collectText
|
||||||
|
.split(/[,;\s]+/)
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const body = {
|
||||||
|
name: name.trim(),
|
||||||
|
compare_template_id: compareId,
|
||||||
|
collect_metric_ids,
|
||||||
|
defaults,
|
||||||
|
sheet_overrides: overrides,
|
||||||
|
note: note.trim(),
|
||||||
|
};
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
if (editId) {
|
||||||
|
await bizMonitorUpdateTemplate(editId, body);
|
||||||
|
showOk(t("bizMonitorTpl.saved"));
|
||||||
|
} else {
|
||||||
|
await bizMonitorCreateTemplate(body);
|
||||||
|
showOk(t("bizMonitorTpl.created"));
|
||||||
|
}
|
||||||
|
closeEdit();
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (id: string) => {
|
||||||
|
if (!window.confirm(t("bizMonitorTpl.confirmDelete"))) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await bizMonitorDeleteTemplate(id);
|
||||||
|
showOk(t("bizMonitorTpl.deleted"));
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel nm-page-panel">
|
||||||
|
<div className="panel__toolbar">
|
||||||
|
<h2>{t("bizMonitorTpl.title")}</h2>
|
||||||
|
<div className="btn-row">
|
||||||
|
<Button size="sm" variant="primary" onPress={openCreate}>
|
||||||
|
{t("bizMonitorTpl.create")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="panel__hint muted">{t("bizMonitorTpl.hint")}</p>
|
||||||
|
|
||||||
|
<div className="pt-list">
|
||||||
|
<div className="pt-list-kpis">
|
||||||
|
<div className="pt-list-kpi">
|
||||||
|
<div className="pt-list-kpi__label">{t("bizMonitorTpl.kpiTotal")}</div>
|
||||||
|
<div className="pt-list-kpi__value">{items.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-inline">
|
||||||
|
<Input
|
||||||
|
value={listKw}
|
||||||
|
placeholder={t("bizMonitorTpl.listFilterPh")}
|
||||||
|
onChange={(e) => setListKw(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-list-table-wrap">
|
||||||
|
<table className="data-table pt-list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("bizMonitorTpl.colName")}</th>
|
||||||
|
<th>{t("bizMonitorTpl.colCompare")}</th>
|
||||||
|
<th>{t("bizMonitorTpl.colCollect")}</th>
|
||||||
|
<th>{t("bizMonitorTpl.colNote")}</th>
|
||||||
|
<th>{t("bizMonitorTpl.colActions")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((row) => (
|
||||||
|
<tr key={row.id}>
|
||||||
|
<td>
|
||||||
|
<div className="pt-list-task-name">{row.name}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{row.compare_template_name || row.compare_template_id || "—"}
|
||||||
|
{row.compare_template_id ? (
|
||||||
|
<div className="muted" style={{ fontSize: 11 }}>
|
||||||
|
<Link to="/network/cutover/compare-templates">
|
||||||
|
{t("bizMonitorTpl.openCompare")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{(row.collect_metric_ids || []).length
|
||||||
|
? (row.collect_metric_ids || []).join(", ")
|
||||||
|
: t("bizMonitorTpl.collectAll")}
|
||||||
|
</td>
|
||||||
|
<td className="muted">{row.note || "—"}</td>
|
||||||
|
<td>
|
||||||
|
<div className="pt-list-actions">
|
||||||
|
<Button size="sm" variant="primary" onPress={() => openEdit(row)}>
|
||||||
|
{t("bizMonitorTpl.edit")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void remove(row.id)}
|
||||||
|
>
|
||||||
|
{t("bizMonitorTpl.delete")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!filtered.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<div className="pt-list-empty">{t("bizMonitorTpl.empty")}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppModalShell open={editOpen} onClose={closeEdit} size="lg">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Heading>
|
||||||
|
{editId ? t("bizMonitorTpl.edit") : t("bizMonitorTpl.create")}
|
||||||
|
</Modal.Heading>
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="flex flex-col gap-3">
|
||||||
|
<p className="muted" style={{ margin: 0, fontSize: 12 }}>
|
||||||
|
{t("bizMonitorTpl.formHint")}
|
||||||
|
</p>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizMonitorTpl.colName")}</span>
|
||||||
|
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<FieldSelect
|
||||||
|
label={t("bizMonitorTpl.colCompare")}
|
||||||
|
value={compareId}
|
||||||
|
onChange={(e) => setCompareId(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
hint={t("bizMonitorTpl.compareHint")}
|
||||||
|
>
|
||||||
|
<option value="">{t("bizMonitorTpl.pickCompare")}</option>
|
||||||
|
{compareTpls.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</FieldSelect>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizMonitorTpl.colCollect")}</span>
|
||||||
|
<Input
|
||||||
|
value={collectText}
|
||||||
|
onChange={(e) => setCollectText(e.target.value)}
|
||||||
|
placeholder={t("bizMonitorTpl.collectPh")}
|
||||||
|
/>
|
||||||
|
<span className="ui-field__hint">{t("bizMonitorTpl.collectHint")}</span>
|
||||||
|
</label>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizMonitorTpl.defaults")}</span>
|
||||||
|
<textarea
|
||||||
|
value={defaultsText}
|
||||||
|
onChange={(e) => setDefaultsText(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
style={{ width: "100%", fontFamily: "ui-monospace, monospace", fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizMonitorTpl.overrides")}</span>
|
||||||
|
<textarea
|
||||||
|
value={overridesText}
|
||||||
|
onChange={(e) => setOverridesText(e.target.value)}
|
||||||
|
rows={6}
|
||||||
|
style={{ width: "100%", fontFamily: "ui-monospace, monospace", fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<span className="ui-field__hint">{t("bizMonitorTpl.overridesHint")}</span>
|
||||||
|
</label>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizMonitorTpl.colNote")}</span>
|
||||||
|
<Input value={note} onChange={(e) => setNote(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button size="sm" variant="secondary" onPress={closeEdit}>
|
||||||
|
{t("bizMonitorTpl.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void save()}>
|
||||||
|
{t("bizMonitorTpl.save")}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AppModalShell>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2096,3 +2096,20 @@ export const bizMigrationListDiffs = (params: {
|
||||||
`/v1/biz-migration/runs/${encodeURIComponent(params.runId)}/diffs${qs ? `?${qs}` : ""}`,
|
`/v1/biz-migration/runs/${encodeURIComponent(params.runId)}/diffs${qs ? `?${qs}` : ""}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const bizMonitorListTemplates = () =>
|
||||||
|
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-migration/monitor-templates");
|
||||||
|
|
||||||
|
export const bizMonitorCreateTemplate = (body: Record<string, unknown>) =>
|
||||||
|
apiPost<Record<string, unknown>>("/v1/biz-migration/monitor-templates", body);
|
||||||
|
|
||||||
|
export const bizMonitorUpdateTemplate = (templateId: string, body: Record<string, unknown>) =>
|
||||||
|
apiPatch<Record<string, unknown>>(
|
||||||
|
`/v1/biz-migration/monitor-templates/${encodeURIComponent(templateId)}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const bizMonitorDeleteTemplate = (templateId: string) =>
|
||||||
|
apiDelete<{ ok: boolean }>(
|
||||||
|
`/v1/biz-migration/monitor-templates/${encodeURIComponent(templateId)}`,
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue