mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Add fabric-node delete on classify inventory for orphans and placeholders.
Allow removing detached or LLDP/WebCRT fabric rows from classify and slices without touching managed/UME inventory; keep UME-only nodes protected. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a5a3a4f156
commit
53ddb3b520
10 changed files with 391 additions and 11 deletions
|
|
@ -1,17 +1,35 @@
|
|||
"""Detach fabric ↔ inventory links without deleting topology placements/edges."""
|
||||
"""Detach fabric ↔ inventory links; purge orphan / non-managed fabric nodes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from .device_types import LLDP_DISCOVERED_NE_SOURCE, WEBCRT_NE_SOURCE
|
||||
from .models import ManagedNE, TopoFabricNode, TopoView, UmeInventoryNE
|
||||
from .models import (
|
||||
ManagedNE,
|
||||
TopoFabricEdge,
|
||||
TopoFabricNode,
|
||||
TopoView,
|
||||
TopoViewEdgeStyle,
|
||||
TopoViewNode,
|
||||
UmeInventoryNE,
|
||||
)
|
||||
|
||||
UME_SYNC_SOURCE = "ume_sync"
|
||||
# ManagedNE.source values that are not "real" ops inventory (placeholders / sessions).
|
||||
_NON_INVENTORY_MANAGED_SOURCES = frozenset(
|
||||
{
|
||||
LLDP_DISCOVERED_NE_SOURCE,
|
||||
WEBCRT_NE_SOURCE,
|
||||
"lldp",
|
||||
"webcrt",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
|
|
@ -152,6 +170,105 @@ def reconcile_dangling_fabric_links(db: Session) -> dict[str, int]:
|
|||
}
|
||||
|
||||
|
||||
def fabric_node_is_deletable(db: Session, n: TopoFabricNode) -> bool:
|
||||
"""True for orphaned / dangling / LLDP·WebCRT placeholder fabric rows.
|
||||
|
||||
UME-only and real managed inventory (manual / ume_sync) cannot be deleted here.
|
||||
"""
|
||||
mid = str(n.managed_ne_id or "").strip()
|
||||
uid = str(n.ume_ne_id or "").strip()
|
||||
if not mid:
|
||||
# Orphaned only — keep UME-linked fabric rows.
|
||||
return not uid
|
||||
row = db.get(ManagedNE, mid)
|
||||
if row is None:
|
||||
return True
|
||||
src = str(row.source or "").strip().lower()
|
||||
return src in _NON_INVENTORY_MANAGED_SOURCES
|
||||
|
||||
|
||||
def _strip_fabric_ids_from_membership(db: Session, fabric_ids: set[str]) -> int:
|
||||
if not fabric_ids:
|
||||
return 0
|
||||
touched = 0
|
||||
for view in db.query(TopoView).all():
|
||||
filt = dict(view.filter or {})
|
||||
mem = filt.get("membership")
|
||||
if not isinstance(mem, dict):
|
||||
continue
|
||||
changed = False
|
||||
for key in ("seed_fabric_node_ids", "member_fabric_node_ids"):
|
||||
raw = mem.get(key)
|
||||
if not isinstance(raw, list):
|
||||
continue
|
||||
next_ids = [str(x).strip() for x in raw if str(x).strip() not in fabric_ids]
|
||||
if next_ids != [str(x).strip() for x in raw if str(x).strip()]:
|
||||
mem[key] = next_ids
|
||||
changed = True
|
||||
if not changed:
|
||||
continue
|
||||
filt["membership"] = mem
|
||||
view.filter = filt
|
||||
flag_modified(view, "filter")
|
||||
view.updated_at = _utcnow()
|
||||
touched += 1
|
||||
return touched
|
||||
|
||||
|
||||
def delete_fabric_nodes(db: Session, fabric_node_ids: list[str]) -> dict[str, int]:
|
||||
"""Hard-delete fabric nodes (placements + edges). Does not touch managed/UME inventory."""
|
||||
ids = _norm_ids(fabric_node_ids)
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="fabric_node_ids_required")
|
||||
rows = db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(ids)).all()
|
||||
found = {str(r.id): r for r in rows}
|
||||
missing = [i for i in ids if i not in found]
|
||||
if missing:
|
||||
raise HTTPException(status_code=404, detail=f"fabric_node_not_found:{missing[0]}")
|
||||
blocked = [i for i, r in found.items() if not fabric_node_is_deletable(db, r)]
|
||||
if blocked:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"fabric_node_not_deletable:{blocked[0]}",
|
||||
)
|
||||
|
||||
edge_ids = [
|
||||
str(e.id)
|
||||
for e in db.query(TopoFabricEdge)
|
||||
.filter(
|
||||
(TopoFabricEdge.a_node_id.in_(ids)) | (TopoFabricEdge.b_node_id.in_(ids))
|
||||
)
|
||||
.all()
|
||||
]
|
||||
if edge_ids:
|
||||
db.query(TopoViewEdgeStyle).filter(
|
||||
TopoViewEdgeStyle.fabric_edge_id.in_(edge_ids)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(TopoFabricEdge).filter(TopoFabricEdge.id.in_(edge_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
placements = (
|
||||
db.query(TopoViewNode)
|
||||
.filter(TopoViewNode.fabric_node_id.in_(ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
_strip_fabric_ids_from_membership(db, set(ids))
|
||||
for r in rows:
|
||||
db.delete(r)
|
||||
db.commit()
|
||||
try:
|
||||
from .topology_service import refresh_fabric_stats
|
||||
|
||||
refresh_fabric_stats(db)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return {
|
||||
"deleted": len(rows),
|
||||
"edges_deleted": len(edge_ids),
|
||||
"placements_deleted": int(placements or 0),
|
||||
}
|
||||
|
||||
|
||||
def enrich_fabric_node_dicts(
|
||||
db: Session, items: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
|
|
@ -190,4 +307,12 @@ def enrich_fabric_node_dicts(
|
|||
item["managed_alive"] = bool(mrow)
|
||||
item["ume_alive"] = bool(uid and uid in ume_alive)
|
||||
item["managed_source"] = managed_source_label(mrow.source) if mrow else ""
|
||||
if not mid:
|
||||
item["deletable"] = not bool(uid)
|
||||
elif mrow is None:
|
||||
item["deletable"] = True
|
||||
else:
|
||||
item["deletable"] = (
|
||||
str(mrow.source or "").strip().lower() in _NON_INVENTORY_MANAGED_SOURCES
|
||||
)
|
||||
return items
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from .topology_schemas import (
|
|||
FabricDiscoverRequest,
|
||||
FabricManualEdgeIn,
|
||||
FabricNodesBulkTagRequest,
|
||||
FabricNodesDeleteRequest,
|
||||
FabricNodesMatchRequest,
|
||||
FabricNodeTagPatch,
|
||||
SliceGenerateRequest,
|
||||
|
|
@ -401,6 +402,27 @@ def api_fabric_nodes_bulk_tag(
|
|||
return bulk_tag_fabric_nodes(db, body).model_dump()
|
||||
|
||||
|
||||
@router.delete("/fabric/nodes/{fabric_node_id}")
|
||||
def api_delete_fabric_node(
|
||||
fabric_node_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""Delete a fabric node (orphaned / non-managed only). Does not touch inventory tables."""
|
||||
from .topology_inventory_lifecycle import delete_fabric_nodes
|
||||
|
||||
return delete_fabric_nodes(db, [fabric_node_id])
|
||||
|
||||
|
||||
@router.post("/fabric/nodes/delete")
|
||||
def api_delete_fabric_nodes(
|
||||
body: FabricNodesDeleteRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
from .topology_inventory_lifecycle import delete_fabric_nodes
|
||||
|
||||
return delete_fabric_nodes(db, body.fabric_node_ids)
|
||||
|
||||
|
||||
@router.post("/slices/generate")
|
||||
def api_generate_slices(
|
||||
body: SliceGenerateRequest,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class FabricNodeOut(BaseModel):
|
|||
managed_alive: bool = False
|
||||
ume_alive: bool = False
|
||||
managed_source: str = "" # manual | ume_sync | lldp | webcrt | …
|
||||
deletable: bool = False
|
||||
|
||||
|
||||
class FabricEdgeOut(BaseModel):
|
||||
|
|
@ -425,6 +426,16 @@ class FabricNodesBulkTagOut(BaseModel):
|
|||
samples: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricNodesDeleteRequest(BaseModel):
|
||||
fabric_node_ids: list[str] = Field(default_factory=list, min_length=1)
|
||||
|
||||
|
||||
class FabricNodesDeleteOut(BaseModel):
|
||||
deleted: int = 0
|
||||
edges_deleted: int = 0
|
||||
placements_deleted: int = 0
|
||||
|
||||
|
||||
class SliceGenerateRequest(BaseModel):
|
||||
folder_id: str
|
||||
template: str = Field(description="core_only | core_agg | agg_access")
|
||||
|
|
|
|||
|
|
@ -1203,6 +1203,65 @@ Management Addresses:
|
|||
self.db.refresh(dangling)
|
||||
self.assertFalse(str(dangling.managed_ne_id or "").strip())
|
||||
|
||||
def test_delete_fabric_node_only_orphans_and_placeholders(self) -> None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
from netx_api.topology_inventory_lifecycle import delete_fabric_nodes
|
||||
|
||||
suffix = uuid4().hex[:8]
|
||||
real = ManagedNE(
|
||||
id=f"real-{suffix}",
|
||||
name=f"REAL-{suffix}",
|
||||
vendor="Cisco",
|
||||
device_type="cisco_ios",
|
||||
ip_address=f"10.77.{(int(suffix[:2], 16) % 200) + 1}.1",
|
||||
source="",
|
||||
)
|
||||
ph = ManagedNE(
|
||||
id=f"ph-{suffix}",
|
||||
name=f"PH-{suffix}",
|
||||
vendor="Other",
|
||||
device_type="generic",
|
||||
ip_address="",
|
||||
source=LLDP_DISCOVERED_NE_SOURCE,
|
||||
)
|
||||
self.db.add(real)
|
||||
self.db.add(ph)
|
||||
self.db.commit()
|
||||
fab_real = svc.ensure_fabric_node_for_managed(self.db, real)
|
||||
fab_ph = svc.ensure_fabric_node_for_managed(self.db, ph)
|
||||
orphan = TopoFabricNode(
|
||||
id=f"orp-{suffix}",
|
||||
name=f"ORP-{suffix}",
|
||||
ip="10.77.0.9",
|
||||
)
|
||||
ume_only = TopoFabricNode(
|
||||
id=f"ume-{suffix}",
|
||||
name=f"UME-{suffix}",
|
||||
ip="10.77.0.8",
|
||||
ume_ne_id=f"ume-ne-{suffix}",
|
||||
)
|
||||
self.db.add(orphan)
|
||||
self.db.add(ume_only)
|
||||
self.db.commit()
|
||||
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
delete_fabric_nodes(self.db, [fab_real.id])
|
||||
self.assertEqual(ctx.exception.status_code, 400)
|
||||
|
||||
with self.assertRaises(HTTPException) as ctx_ume:
|
||||
delete_fabric_nodes(self.db, [ume_only.id])
|
||||
self.assertEqual(ctx_ume.exception.status_code, 400)
|
||||
|
||||
out = delete_fabric_nodes(self.db, [fab_ph.id, orphan.id])
|
||||
self.assertEqual(out["deleted"], 2)
|
||||
self.assertIsNone(self.db.get(TopoFabricNode, fab_ph.id))
|
||||
self.assertIsNone(self.db.get(TopoFabricNode, orphan.id))
|
||||
self.assertIsNotNone(self.db.get(TopoFabricNode, fab_real.id))
|
||||
self.assertIsNotNone(self.db.get(TopoFabricNode, ume_only.id))
|
||||
self.assertIsNotNone(self.db.get(ManagedNE, real.id))
|
||||
self.assertIsNotNone(self.db.get(ManagedNE, ph.id))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1424,6 +1424,14 @@ const en = {
|
|||
edit: "Edit",
|
||||
save: "Save",
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
deleteSelected: "Delete nodes ({{count}})",
|
||||
deleteConfirm:
|
||||
"Delete {{count}} fabric node(s) (placements and edges)? Managed/UME inventory is not touched.",
|
||||
deleteOk: "Deleted {{count}} node(s)",
|
||||
deleteHint:
|
||||
"Only orphaned or LLDP/WebCRT placeholder fabric nodes can be deleted; UME-only and real managed NEs cannot",
|
||||
deleteBlocked: "UME-only or still linked to real managed inventory — cannot delete here",
|
||||
rowSaved: "Saved",
|
||||
loading: "Loading…",
|
||||
empty: "No NEs.",
|
||||
|
|
|
|||
|
|
@ -1420,6 +1420,12 @@ const zh = {
|
|||
edit: "编辑",
|
||||
save: "保存",
|
||||
cancel: "取消",
|
||||
delete: "删除",
|
||||
deleteSelected: "删除节点 ({{count}})",
|
||||
deleteConfirm: "将从拓扑 Fabric 删除 {{count}} 个节点(含上图与边),不影响网元管理/UME 清单。确定?",
|
||||
deleteOk: "已删除 {{count}} 个节点",
|
||||
deleteHint: "仅可删除已解绑节点,或 LLDP/WebCRT 等非实际管理占位;仅 UME / 实际管理网元不可删",
|
||||
deleteBlocked: "仅 UME 或仍关联实际管理网元,不可在此删除",
|
||||
rowSaved: "已保存",
|
||||
loading: "加载中…",
|
||||
empty: "没有网元。",
|
||||
|
|
|
|||
|
|
@ -4160,6 +4160,61 @@ pre {
|
|||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.topo-classify__help {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #94a3b8;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: help;
|
||||
user-select: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.topo-classify__help:hover,
|
||||
.topo-classify__help:focus-visible {
|
||||
border-color: #64748b;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
}
|
||||
.topo-classify__help-tip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
z-index: 40;
|
||||
width: max-content;
|
||||
max-width: min(280px, 70vw);
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
.topo-classify__help-tip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 8px;
|
||||
border: 5px solid transparent;
|
||||
border-bottom-color: #0f172a;
|
||||
}
|
||||
.topo-classify__help:hover .topo-classify__help-tip,
|
||||
.topo-classify__help:focus-visible .topo-classify__help-tip {
|
||||
display: block;
|
||||
}
|
||||
.topo-classify__section h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useMemo, useState } from "react";
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
bulkTagFabricNodes,
|
||||
deleteFabricNode,
|
||||
deleteFabricNodes,
|
||||
fetchFabricNodes,
|
||||
fetchTopologyTree,
|
||||
generateTopologySlices,
|
||||
|
|
@ -21,6 +23,20 @@ function flattenRegions(root: TopologyTreeFolderItem | null | undefined): Topolo
|
|||
return (root.children || []).filter((c) => String(c.kind) === "region");
|
||||
}
|
||||
|
||||
/** Prefer API flag; fall back so UI still works if backend is stale. */
|
||||
function isFabricNodeDeletable(n: FabricNodeSearchHit): boolean {
|
||||
if (typeof n.deletable === "boolean") return n.deletable;
|
||||
const status = String(n.link_status || "").toLowerCase();
|
||||
if (status === "ume" || status === "both") return false;
|
||||
if (status === "orphaned") return true;
|
||||
if (!String(n.managed_ne_id || "").trim()) {
|
||||
return !String(n.ume_ne_id || "").trim();
|
||||
}
|
||||
if (n.managed_alive === false) return true;
|
||||
const src = String(n.managed_source || "").toLowerCase();
|
||||
return src === "lldp" || src === "webcrt";
|
||||
}
|
||||
|
||||
export function TopologyClassifyPage() {
|
||||
const { t } = useI18n();
|
||||
const { showOk, showError } = useToast();
|
||||
|
|
@ -156,6 +172,26 @@ export function TopologyClassifyPage() {
|
|||
onError: (err) => showError(String(err)),
|
||||
});
|
||||
|
||||
const deleteOneMut = useMutation({
|
||||
mutationFn: (id: string) => deleteFabricNode(id),
|
||||
onSuccess: async (out) => {
|
||||
showOk(t("topoClassify.deleteOk").replace("{{count}}", String(out.deleted)));
|
||||
setSelected({});
|
||||
await invalidateList();
|
||||
},
|
||||
onError: (err) => showError(String(err)),
|
||||
});
|
||||
|
||||
const deleteBulkMut = useMutation({
|
||||
mutationFn: (ids: string[]) => deleteFabricNodes(ids),
|
||||
onSuccess: async (out) => {
|
||||
showOk(t("topoClassify.deleteOk").replace("{{count}}", String(out.deleted)));
|
||||
setSelected({});
|
||||
await invalidateList();
|
||||
},
|
||||
onError: (err) => showError(String(err)),
|
||||
});
|
||||
|
||||
const slicePreviewMut = useMutation({
|
||||
mutationFn: () =>
|
||||
generateTopologySlices({
|
||||
|
|
@ -187,6 +223,9 @@ export function TopologyClassifyPage() {
|
|||
});
|
||||
|
||||
const items = listQuery.data?.items || [];
|
||||
const deletableSelected = items
|
||||
.filter((n) => selected[n.id] && isFabricNodeDeletable(n))
|
||||
.map((n) => n.id);
|
||||
const total = listQuery.data?.total || 0;
|
||||
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
const selectedCount = Object.values(selected).filter(Boolean).length;
|
||||
|
|
@ -373,8 +412,29 @@ export function TopologyClassifyPage() {
|
|||
>
|
||||
{t("topoClassify.confirmAssign")}
|
||||
</button>
|
||||
<span className="panel__hint">
|
||||
{t("topoClassify.selectedCount").replace("{{count}}", String(selectedCount))}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--danger"
|
||||
disabled={deleteBulkMut.isPending || deletableSelected.length === 0}
|
||||
title={t("topoClassify.deleteHint")}
|
||||
onClick={() => {
|
||||
const msg = t("topoClassify.deleteConfirm").replace(
|
||||
"{{count}}",
|
||||
String(deletableSelected.length),
|
||||
);
|
||||
if (window.confirm(msg)) deleteBulkMut.mutate(deletableSelected);
|
||||
}}
|
||||
>
|
||||
{t("topoClassify.deleteSelected").replace(
|
||||
"{{count}}",
|
||||
String(deletableSelected.length),
|
||||
)}
|
||||
</button>
|
||||
<span className="topo-classify__help" tabIndex={0} aria-label={t("topoClassify.deleteHint")}>
|
||||
?
|
||||
<span className="topo-classify__help-tip" role="tooltip">
|
||||
{t("topoClassify.deleteHint")}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -475,13 +535,35 @@ export function TopologyClassifyPage() {
|
|||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => startEdit(n)}
|
||||
>
|
||||
{t("topoClassify.edit")}
|
||||
</button>
|
||||
<div className="topo-classify__row-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => startEdit(n)}
|
||||
>
|
||||
{t("topoClassify.edit")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--danger"
|
||||
disabled={deleteOneMut.isPending || !isFabricNodeDeletable(n)}
|
||||
title={
|
||||
isFabricNodeDeletable(n)
|
||||
? t("topoClassify.deleteHint")
|
||||
: t("topoClassify.deleteBlocked")
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isFabricNodeDeletable(n)) return;
|
||||
const msg = t("topoClassify.deleteConfirm").replace(
|
||||
"{{count}}",
|
||||
"1",
|
||||
);
|
||||
if (window.confirm(msg)) deleteOneMut.mutate(n.id);
|
||||
}}
|
||||
>
|
||||
{t("topoClassify.delete")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,17 @@ export const patchFabricNodeTags = (
|
|||
body,
|
||||
);
|
||||
|
||||
export const deleteFabricNode = (fabricNodeId: string) =>
|
||||
apiDelete<{ deleted: number; edges_deleted: number; placements_deleted: number }>(
|
||||
`/v1/topology/fabric/nodes/${encodeURIComponent(fabricNodeId)}`,
|
||||
);
|
||||
|
||||
export const deleteFabricNodes = (fabricNodeIds: string[]) =>
|
||||
apiPost<{ deleted: number; edges_deleted: number; placements_deleted: number }>(
|
||||
"/v1/topology/fabric/nodes/delete",
|
||||
{ fabric_node_ids: fabricNodeIds },
|
||||
);
|
||||
|
||||
export const generateTopologySlices = (body: {
|
||||
folder_id: string;
|
||||
template: "core_only" | "core_agg" | "agg_access";
|
||||
|
|
|
|||
|
|
@ -445,6 +445,7 @@ export type FabricNodeSearchHit = {
|
|||
managed_alive?: boolean;
|
||||
ume_alive?: boolean;
|
||||
managed_source?: string;
|
||||
deletable?: boolean;
|
||||
views?: Array<{
|
||||
view_id: string;
|
||||
view_name: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue