mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Add one-click export of biz-state collect commands.
Plan enabled monitoring items into concrete CLIs (with aux) and download as txt from the task detail modal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1fcd613bc6
commit
32deca1687
7 changed files with 446 additions and 1 deletions
|
|
@ -26,7 +26,14 @@ from ..models import (
|
||||||
ManagedNE,
|
ManagedNE,
|
||||||
)
|
)
|
||||||
from ..timeutil import utcnow_naive
|
from ..timeutil import utcnow_naive
|
||||||
from .command_match import normalize_command, preview_task_item
|
from .command_match import (
|
||||||
|
EXPAND_ALL_COMMAND,
|
||||||
|
expand_from_bindings,
|
||||||
|
match_command,
|
||||||
|
normalize_command,
|
||||||
|
preview_task_item,
|
||||||
|
)
|
||||||
|
from .collect_session import resolve_aux_command
|
||||||
from .profiles import (
|
from .profiles import (
|
||||||
all_profiles,
|
all_profiles,
|
||||||
get_profile,
|
get_profile,
|
||||||
|
|
@ -1002,3 +1009,228 @@ def preview_items(db: Session, *, vendor: str, device_type: str, items: list[dic
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_export_profile(profile_id: str):
|
||||||
|
"""Resolve collect profile; remap disabled if_intf → config_interface."""
|
||||||
|
pid = str(profile_id or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return None
|
||||||
|
profile = get_profile(pid)
|
||||||
|
if profile is None:
|
||||||
|
return None
|
||||||
|
if profile.enabled:
|
||||||
|
return profile
|
||||||
|
if profile.metric_id == "if_intf" or pid.endswith(".if_intf"):
|
||||||
|
vk = str(profile.vendor_key or "zte").strip() or "zte"
|
||||||
|
remapped = get_profile(f"{vk}.config_interface") or get_profile("zte.config_interface")
|
||||||
|
if remapped and remapped.enabled:
|
||||||
|
return remapped
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def plan_task_collect_commands(
|
||||||
|
db: Session,
|
||||||
|
task_id: str,
|
||||||
|
*,
|
||||||
|
enabled_only: bool = True,
|
||||||
|
include_aux: bool = True,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Plan concrete collect CLIs for a task (no device login).
|
||||||
|
|
||||||
|
expand_all items (unbound optional discover) are listed with a note; aux
|
||||||
|
commands are resolved from primary params when include_aux is True.
|
||||||
|
"""
|
||||||
|
task = db.get(BizStateTask, task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="task_not_found")
|
||||||
|
vendor_key = resolve_vendor_key(task.vendor or "", task.device_type or "")
|
||||||
|
q = db.query(BizStateTaskItem).filter(BizStateTaskItem.task_id == task_id)
|
||||||
|
if enabled_only:
|
||||||
|
q = q.filter(BizStateTaskItem.enabled.is_(True))
|
||||||
|
items = q.order_by(BizStateTaskItem.sort_order.asc()).all()
|
||||||
|
|
||||||
|
sections: list[dict[str, Any]] = []
|
||||||
|
flat: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
|
||||||
|
def _add_cmd(cmd: str) -> bool:
|
||||||
|
c = normalize_command(cmd)
|
||||||
|
if not c or c in seen:
|
||||||
|
return False
|
||||||
|
seen.add(c)
|
||||||
|
flat.append(c)
|
||||||
|
return True
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
title = str(item.title or "").strip()
|
||||||
|
kind = str(item.kind or "catalog")
|
||||||
|
section: dict[str, Any] = {
|
||||||
|
"item_id": item.id,
|
||||||
|
"kind": kind,
|
||||||
|
"source_profile_id": str(item.source_profile_id or ""),
|
||||||
|
"title": title,
|
||||||
|
"enabled": bool(item.enabled),
|
||||||
|
"commands": [],
|
||||||
|
"notes": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == "custom_raw":
|
||||||
|
cmd = normalize_command(item.command_override)
|
||||||
|
if cmd and _add_cmd(cmd):
|
||||||
|
section["commands"].append(
|
||||||
|
{"command": cmd, "role": "primary", "params": {}, "profile_id": ""}
|
||||||
|
)
|
||||||
|
elif not cmd:
|
||||||
|
section["notes"].append("empty custom command")
|
||||||
|
sections.append(section)
|
||||||
|
continue
|
||||||
|
|
||||||
|
profile = _resolve_export_profile(item.source_profile_id)
|
||||||
|
if profile is None:
|
||||||
|
section["notes"].append(
|
||||||
|
f"skip profile {item.source_profile_id} (missing or disabled)"
|
||||||
|
)
|
||||||
|
sections.append(section)
|
||||||
|
continue
|
||||||
|
|
||||||
|
section["title"] = title or str(profile.title or profile.profile_id)
|
||||||
|
section["source_profile_id"] = profile.profile_id
|
||||||
|
binds = (
|
||||||
|
db.query(BizStateTaskItemBinding)
|
||||||
|
.filter(BizStateTaskItemBinding.item_id == item.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
binding_dicts = [
|
||||||
|
{
|
||||||
|
"placeholder": str(b.placeholder or "").strip(),
|
||||||
|
"value": str(b.value or "").strip(),
|
||||||
|
}
|
||||||
|
for b in binds
|
||||||
|
if str(b.placeholder or "").strip() and str(b.value or "").strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
pairs = expand_from_bindings(
|
||||||
|
profile=profile,
|
||||||
|
bindings=binding_dicts,
|
||||||
|
command_override=item.command_override,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
section["notes"].append(str(exc))
|
||||||
|
sections.append(section)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pairs and pairs[0][0] == EXPAND_ALL_COMMAND:
|
||||||
|
tmpl = normalize_command(profile.command_template)
|
||||||
|
section["notes"].append(
|
||||||
|
"expand_all: no bindings; collect will expand discover values"
|
||||||
|
)
|
||||||
|
section["commands"].append(
|
||||||
|
{
|
||||||
|
"command": tmpl,
|
||||||
|
"role": "template",
|
||||||
|
"params": {"__expand_all__": "1"},
|
||||||
|
"profile_id": profile.profile_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
sections.append(section)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for concrete, params in pairs:
|
||||||
|
cmd = normalize_command(concrete)
|
||||||
|
if not cmd:
|
||||||
|
continue
|
||||||
|
hit = match_command(vendor_key=vendor_key, command=cmd)
|
||||||
|
pid = str(
|
||||||
|
(hit.profile.profile_id if hit else profile.profile_id) or ""
|
||||||
|
).strip()
|
||||||
|
if _add_cmd(cmd):
|
||||||
|
section["commands"].append(
|
||||||
|
{
|
||||||
|
"command": cmd,
|
||||||
|
"role": "primary",
|
||||||
|
"params": dict(params or {}),
|
||||||
|
"profile_id": pid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not include_aux:
|
||||||
|
continue
|
||||||
|
for aux in list(getattr(hit.profile if hit else profile, "aux_commands", None) or []):
|
||||||
|
try:
|
||||||
|
ra = resolve_aux_command(aux, params=dict(params or {}))
|
||||||
|
except ValueError as exc:
|
||||||
|
section["notes"].append(f"aux {getattr(aux, 'key', '')}: {exc}")
|
||||||
|
continue
|
||||||
|
if _add_cmd(ra.command):
|
||||||
|
section["commands"].append(
|
||||||
|
{
|
||||||
|
"command": ra.command,
|
||||||
|
"role": "aux",
|
||||||
|
"aux_key": ra.key,
|
||||||
|
"params": dict(params or {}),
|
||||||
|
"profile_id": ra.profile_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sections.append(section)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"task_id": task.id,
|
||||||
|
"ne_name": task.ne_name or "",
|
||||||
|
"ne_ip": task.ne_ip or "",
|
||||||
|
"vendor": task.vendor or "",
|
||||||
|
"device_type": task.device_type or "",
|
||||||
|
"command_count": len(flat),
|
||||||
|
"commands": flat,
|
||||||
|
"items": sections,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def export_task_commands_text(
|
||||||
|
db: Session,
|
||||||
|
task_id: str,
|
||||||
|
*,
|
||||||
|
enabled_only: bool = True,
|
||||||
|
include_aux: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Plain-text export of planned collect commands (one CLI per line + section headers)."""
|
||||||
|
plan = plan_task_collect_commands(
|
||||||
|
db, task_id, enabled_only=enabled_only, include_aux=include_aux
|
||||||
|
)
|
||||||
|
lines = [
|
||||||
|
"# biz-state collect commands",
|
||||||
|
f"# task_id={plan['task_id']}",
|
||||||
|
f"# ne={plan['ne_name'] or '-'} ({plan['ne_ip'] or '-'})",
|
||||||
|
f"# vendor={plan['vendor'] or '-'} device_type={plan['device_type'] or '-'}",
|
||||||
|
f"# command_count={plan['command_count']}",
|
||||||
|
f"# include_aux={'1' if include_aux else '0'}",
|
||||||
|
f"# enabled_only={'1' if enabled_only else '0'}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for sec in plan["items"]:
|
||||||
|
title = str(sec.get("title") or sec.get("source_profile_id") or "item").strip()
|
||||||
|
pid = str(sec.get("source_profile_id") or "").strip()
|
||||||
|
header = f"## {title}"
|
||||||
|
if pid and pid not in title:
|
||||||
|
header = f"## {title} · {pid}"
|
||||||
|
lines.append(header)
|
||||||
|
for note in sec.get("notes") or []:
|
||||||
|
lines.append(f"# note: {note}")
|
||||||
|
cmds = list(sec.get("commands") or [])
|
||||||
|
if not cmds and not (sec.get("notes") or []):
|
||||||
|
lines.append("# (no commands)")
|
||||||
|
for c in cmds:
|
||||||
|
role = str(c.get("role") or "primary")
|
||||||
|
if role == "aux":
|
||||||
|
lines.append(f"# aux:{c.get('aux_key') or ''}")
|
||||||
|
elif role == "template":
|
||||||
|
lines.append("# template (expand_all at collect):")
|
||||||
|
lines.append(str(c.get("command") or ""))
|
||||||
|
lines.append("")
|
||||||
|
# Flat unique list at end for easy copy into scripts
|
||||||
|
lines.append("# ---- flat unique commands ----")
|
||||||
|
for cmd in plan["commands"]:
|
||||||
|
lines.append(str(cmd))
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,40 @@ def api_get_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
return svc.get_task(db, task_id)
|
return svc.get_task(db, task_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tasks/{task_id}/commands")
|
||||||
|
def api_plan_task_commands(
|
||||||
|
task_id: str,
|
||||||
|
enabled_only: bool = Query(True),
|
||||||
|
include_aux: bool = Query(True),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""List planned collect CLIs for a task (bindings expanded; no device login)."""
|
||||||
|
return svc.plan_task_collect_commands(
|
||||||
|
db, task_id, enabled_only=enabled_only, include_aux=include_aux
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tasks/{task_id}/commands/export")
|
||||||
|
def api_export_task_commands(
|
||||||
|
task_id: str,
|
||||||
|
enabled_only: bool = Query(True),
|
||||||
|
include_aux: bool = Query(True),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> StreamingResponse:
|
||||||
|
"""Download planned collect commands as a .txt file."""
|
||||||
|
text = svc.export_task_commands_text(
|
||||||
|
db, task_id, enabled_only=enabled_only, include_aux=include_aux
|
||||||
|
)
|
||||||
|
safe = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in task_id)[:40]
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([text.encode("utf-8")]),
|
||||||
|
media_type="text/plain; charset=utf-8",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="biz_state_commands_{safe}.txt"'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/tasks/{task_id}")
|
@router.patch("/tasks/{task_id}")
|
||||||
def api_patch_task(
|
def api_patch_task(
|
||||||
task_id: str, body: TaskPatchIn, db: Session = Depends(get_db)
|
task_id: str, body: TaskPatchIn, db: Session = Depends(get_db)
|
||||||
|
|
|
||||||
115
tests/test_biz_state_export_commands.py
Normal file
115
tests/test_biz_state_export_commands.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
"""Plan/export collect commands for a biz-state task (no device)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from netx_api.biz_state.service import export_task_commands_text, plan_task_collect_commands
|
||||||
|
from netx_api.db import Base
|
||||||
|
from netx_api.models import BizStateTask, BizStateTaskItem, BizStateTaskItemBinding
|
||||||
|
|
||||||
|
|
||||||
|
class BizStateExportCommandsTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||||
|
TestingSession = sessionmaker(
|
||||||
|
bind=engine, autoflush=False, autocommit=False, expire_on_commit=False
|
||||||
|
)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
self.db = TestingSession()
|
||||||
|
self.task = BizStateTask(
|
||||||
|
id="t_export",
|
||||||
|
source="managed",
|
||||||
|
ne_id="ne1",
|
||||||
|
ne_name="PE1",
|
||||||
|
ne_ip="10.0.0.1",
|
||||||
|
vendor="zte",
|
||||||
|
device_type="zte_zxros",
|
||||||
|
status="running",
|
||||||
|
interval_sec=300,
|
||||||
|
)
|
||||||
|
self.db.add(self.task)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def test_plan_and_export_lldp_and_bound_vrf(self) -> None:
|
||||||
|
lldp = BizStateTaskItem(
|
||||||
|
id="i_lldp",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.lldp_neighbors",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=True,
|
||||||
|
title="LLDP",
|
||||||
|
sort_order=1,
|
||||||
|
)
|
||||||
|
vrf = BizStateTaskItem(
|
||||||
|
id="i_vrf",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.bgp_vpnv4_vrf_summary",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=True,
|
||||||
|
title="BGP VRF",
|
||||||
|
sort_order=2,
|
||||||
|
)
|
||||||
|
disabled = BizStateTaskItem(
|
||||||
|
id="i_off",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.isis_neighbors",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=False,
|
||||||
|
title="ISIS off",
|
||||||
|
sort_order=3,
|
||||||
|
)
|
||||||
|
self.db.add_all([lldp, vrf, disabled])
|
||||||
|
self.db.add(
|
||||||
|
BizStateTaskItemBinding(
|
||||||
|
id="b1",
|
||||||
|
item_id=vrf.id,
|
||||||
|
placeholder="vrf",
|
||||||
|
value="CUST_A",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
plan = plan_task_collect_commands(self.db, self.task.id, include_aux=True)
|
||||||
|
self.assertEqual(plan["ne_name"], "PE1")
|
||||||
|
self.assertGreaterEqual(plan["command_count"], 2)
|
||||||
|
cmds = plan["commands"]
|
||||||
|
self.assertTrue(any("lldp" in c.lower() for c in cmds))
|
||||||
|
self.assertTrue(any("CUST_A" in c for c in cmds))
|
||||||
|
# Disabled item excluded by default
|
||||||
|
self.assertFalse(any("isis" in c.lower() for c in cmds))
|
||||||
|
|
||||||
|
text = export_task_commands_text(self.db, self.task.id)
|
||||||
|
self.assertIn("task_id=t_export", text)
|
||||||
|
self.assertIn("show lldp neighbor brief", text)
|
||||||
|
self.assertIn("CUST_A", text)
|
||||||
|
self.assertIn("# ---- flat unique commands ----", text)
|
||||||
|
|
||||||
|
def test_unbound_required_placeholder_noted(self) -> None:
|
||||||
|
item = BizStateTaskItem(
|
||||||
|
id="i_exp",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.bgp_vpnv4_vrf_summary",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=True,
|
||||||
|
title="BGP unbound",
|
||||||
|
sort_order=1,
|
||||||
|
)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
plan = plan_task_collect_commands(self.db, self.task.id)
|
||||||
|
sec = plan["items"][0]
|
||||||
|
notes = " ".join(sec.get("notes") or [])
|
||||||
|
self.assertIn("requires parameter bindings", notes)
|
||||||
|
self.assertEqual(plan["command_count"], 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -258,6 +258,8 @@ const en = {
|
||||||
batches: "Batches",
|
batches: "Batches",
|
||||||
viewBatch: "Open",
|
viewBatch: "Open",
|
||||||
export: "Export",
|
export: "Export",
|
||||||
|
exportCommands: "Export collect commands",
|
||||||
|
exportCommandsOk: "Collect commands exported",
|
||||||
noBatches: "No batches yet",
|
noBatches: "No batches yet",
|
||||||
batchDetail: "Batch detail",
|
batchDetail: "Batch detail",
|
||||||
batchWorkbook: "Batch workbook",
|
batchWorkbook: "Batch workbook",
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ const zh = {
|
||||||
batches: "采集批次",
|
batches: "采集批次",
|
||||||
viewBatch: "查看",
|
viewBatch: "查看",
|
||||||
export: "导出",
|
export: "导出",
|
||||||
|
exportCommands: "导出采集命令",
|
||||||
|
exportCommandsOk: "已导出采集命令",
|
||||||
noBatches: "尚无批次",
|
noBatches: "尚无批次",
|
||||||
batchDetail: "批次详情",
|
batchDetail: "批次详情",
|
||||||
batchWorkbook: "批次工作簿",
|
batchWorkbook: "批次工作簿",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
bizStateDeleteTask,
|
bizStateDeleteTask,
|
||||||
bizStateDiscover,
|
bizStateDiscover,
|
||||||
bizStateDownloadExport,
|
bizStateDownloadExport,
|
||||||
|
bizStateDownloadTaskCommands,
|
||||||
bizStateGetBatch,
|
bizStateGetBatch,
|
||||||
bizStateGetBatchCommand,
|
bizStateGetBatchCommand,
|
||||||
bizStateGetTask,
|
bizStateGetTask,
|
||||||
|
|
@ -747,6 +748,19 @@ export function BizStatePage() {
|
||||||
await collectNowForTask(taskId, true);
|
await collectNowForTask(taskId, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportTaskCommands = async () => {
|
||||||
|
if (!taskId) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await bizStateDownloadTaskCommands(taskId);
|
||||||
|
showOk(t("bizState.exportCommandsOk"));
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeTask = async (id: string) => {
|
const removeTask = async (id: string) => {
|
||||||
if (!window.confirm(t("bizState.confirmDelete"))) return;
|
if (!window.confirm(t("bizState.confirmDelete"))) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -1666,6 +1680,14 @@ export function BizStatePage() {
|
||||||
>
|
>
|
||||||
{t("bizState.collectNow")}
|
{t("bizState.collectNow")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
isDisabled={busy || !taskId}
|
||||||
|
onPress={() => void exportTaskCommands()}
|
||||||
|
>
|
||||||
|
{t("bizState.exportCommands")}
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}>
|
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}>
|
||||||
{t("bizState.delete")}
|
{t("bizState.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1926,6 +1926,44 @@ export const bizStateDownloadExport = async (batchId: string): Promise<void> =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const bizStateDownloadTaskCommands = async (
|
||||||
|
taskId: string,
|
||||||
|
opts?: { includeAux?: boolean; enabledOnly?: boolean },
|
||||||
|
): Promise<void> => {
|
||||||
|
const q = new URLSearchParams();
|
||||||
|
if (opts?.includeAux === false) q.set("include_aux", "false");
|
||||||
|
if (opts?.enabledOnly === false) q.set("enabled_only", "false");
|
||||||
|
const qs = q.toString();
|
||||||
|
const path =
|
||||||
|
`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/commands/export` +
|
||||||
|
(qs ? `?${qs}` : "");
|
||||||
|
const res = await fetch(path, { method: "GET", credentials: fetchCreds, headers: authHeaders() });
|
||||||
|
if (res.status === 401) {
|
||||||
|
handleUnauthorized(path);
|
||||||
|
throw new Error("unauthorized");
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
let detail = `${res.status} export commands`;
|
||||||
|
try {
|
||||||
|
const j = (await res.json()) as { detail?: unknown };
|
||||||
|
if (j?.detail != null) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(detail);
|
||||||
|
}
|
||||||
|
const blob = await res.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
try {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `biz_state_commands_${taskId}.txt`;
|
||||||
|
a.click();
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* ---- biz_state compare (Phase2) ---- */
|
/* ---- biz_state compare (Phase2) ---- */
|
||||||
|
|
||||||
export const bizCompareListTemplates = () =>
|
export const bizCompareListTemplates = () =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue