fix(web): scope 1-hop neighbor expansion to rooted graph views only
Two bugs found while verifying against real production data: - Excluded activity types (execution/check/task etc., see categories.ts) were falling through inCategory's "unknown type -> always visible" fallback, since typeCategory only stored entries whose category was defined. That fallback exists for types the ontology never returned at all; it wrongly re-admitted types the ontology returned but categories.ts deliberately excludes. Fixed by storing every type (including undefined categories) and checking key presence, not value truthiness. - Once that was fixed, the previous commit's 1-hop neighbor expansion (dimmed cross-category context) turned out fine for a rooted view but flooded an unrooted "browse the whole category" view: Fleet's ~49 focus entities are hub-like enough that 1-hop pulled in 325+ of the system's 479 total entities. Neighbor expansion now only applies when a root is set; the unscoped view goes back to same-category-only edges, which measured at a clean 49 nodes for Fleet. Verified against live production data (real bearer token, real DB) rather than mocks: Fleet unrooted = 49 nodes matching the DB's compute+physical count exactly; rooting on host:strong shows 33 nodes with both bright same-category and dimmed cross-category neighbors, no isolated dots. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,8 +66,11 @@
|
||||
let sim: Simulation<Node, Link> | null = null
|
||||
|
||||
// type → browsing category, so the graph can be scoped client-side (the
|
||||
// graph endpoint itself has no category/domain param).
|
||||
let typeCategory = $state<Map<string, Category>>(new Map())
|
||||
// graph endpoint itself has no category/domain param). Value is undefined
|
||||
// for types deliberately excluded from every category (e.g. execution/
|
||||
// check/task — see categories.ts); the key is still present so inCategory
|
||||
// can tell "excluded on purpose" apart from "not in the ontology at all."
|
||||
let typeCategory = $state<Map<string, Category | undefined>>(new Map())
|
||||
|
||||
let hoveredId = $state<string | null>(null)
|
||||
|
||||
@@ -108,11 +111,13 @@
|
||||
return typeof end === 'object' ? end.id : end
|
||||
}
|
||||
|
||||
// Node belongs to the active category? (Unknown types fall back to visible
|
||||
// so a missing ontology entry never blanks the graph.)
|
||||
// Node belongs to the active category? Types the ontology never returned
|
||||
// at all fall back to visible (so a missing entry never blanks the
|
||||
// graph); types the ontology returned but categories.ts deliberately
|
||||
// excludes (key present, value undefined) do not.
|
||||
function inCategory(type: string): boolean {
|
||||
const c = typeCategory.get(type)
|
||||
return c === undefined || c === category
|
||||
if (!typeCategory.has(type)) return true
|
||||
return typeCategory.get(type) === category
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -162,9 +167,7 @@
|
||||
|
||||
onMount(() => {
|
||||
fetchEntityTypes().then((types) => {
|
||||
typeCategory = new Map(
|
||||
types.map((t) => [t.name, typeToCategory(t.name, t.domain)]).filter((e): e is [string, Category] => e[1] !== undefined)
|
||||
)
|
||||
typeCategory = new Map(types.map((t) => [t.name, typeToCategory(t.name, t.domain)]))
|
||||
// Re-derive the active node types now that category membership is known.
|
||||
activeNodeTypes = new Set(nodes.filter((n) => inCategory(n.type)).map((n) => n.type))
|
||||
})
|
||||
@@ -259,11 +262,17 @@
|
||||
// Real infra relationships mostly cross category lines (a service sits on
|
||||
// a network, uses storage, runs on an lxc — different categories under
|
||||
// this taxonomy). Hard-hiding any edge whose other end isn't in-category
|
||||
// left focus nodes looking like disconnected dots. Instead, pull in their
|
||||
// 1-hop neighbors (any category) so the edges — and what they connect
|
||||
// to — stay visible, just visually secondary (see nodeOpacity).
|
||||
// left focus nodes looking like disconnected dots. Rooted views (the user
|
||||
// is exploring out from one entity) pull in 1-hop neighbors of any
|
||||
// category, dimmed, so the edges — and what they connect to — stay
|
||||
// visible. Unscoped "browse the whole category" views (no root) skip
|
||||
// this: with ~50 focus nodes that touch nearly everything, 1-hop
|
||||
// expansion floods in most of the graph (measured: 417 of 479 total
|
||||
// entities for an unrooted Fleet view) — worse than the isolated-dot
|
||||
// problem it was meant to fix. There, same-category-only edges stay.
|
||||
const neighborNodeIds = $derived.by(() => {
|
||||
const neighbors = new Set<string>()
|
||||
if (!root.trim()) return neighbors
|
||||
for (const l of links) {
|
||||
if (!activeRelTypes.has(l.type)) continue
|
||||
const s = endpointId(l.source)
|
||||
|
||||
Reference in New Issue
Block a user