Connections V2: force-directed pivot + multi-field support
Connections V2 — an evolution of the Connections SPIGraph that adds two capabilities on top of today's static bipartite view:
- Force-directed pivot (multi-hop): a new SPIGraph type where you expand nodes in place — click a node, pull in its connections, and the graph grows hop by hop (link-analysis / Maltego style).
- Multi-field / heterogeneous nodes: support multiple field types (
host, ip, port, …) as node types in one graph, not just a singlesrcField → dstFieldpair.
Both reuse and extend the existing Connections model (configurable src/dst fields). Part of the Arkime 7 UI epic (#3926). Large.
Consolidates #147. #147 (awick, 2013) asked for two things on connections: (1) multi-field / heterogeneous node types (
host, ip, port, …), and (2)a → b → cmulti-hop chains "like Kibana Graph." Connections V2 is the single vehicle for both, so #147 is closed in favor of this issue. Phase 1 delivers the multi-hop pivot (accretive expand-in-place); the multi-field / heterogeneous half is Phase 2 (see Phasing / Out of scope).
Decisions (settled — Phase 1 pivot)
| Area | Decision |
|---|---|
| What it is | A new SPIGraph graph type (pivot), alongside connections/heatmap/etc. — not a session-detail tool or standalone view. |
| Pivot interaction | Expand in place (accretive). Acting on a node fetches that entity's neighbors and merges them into the existing graph (dedupe nodes/edges). The graph grows across multiple hops. |
| Node/edge model | Same as Connections — homogeneous nodes from a configured srcField + dstField pair; edges = sessions between them. Reuses /api/connections directly. (Heterogeneous nodes are Phase 2.) |
| Code reuse | Extract a shared force-graph engine from ConnectionsGraph.vue (~1,700 lines of D3); both Connections and Pivot render through it. Pivot adds the expand/merge/prune layer. |
| Performance ceiling | Hard node cap (default ~500, configurable) + auto-prune. Expansions past the cap prune the lowest-weight / least-recently-touched nodes (pinned + just-expanded nodes protected). |
How it relates to Connections (today)
Connections is already a force-directed SPIGraph type: self-contained ConnectionsGraph.vue, owns its /api/connections fetch, draws a bipartite graph of srcField → dstField (default source.ip → destination.ip, saved in user settings). Controls: session-count cap (length), minConn, weight metric, baselineDate compare, force strength, zoom, text size, lock, export PNG. Node values are clickable via the standard SessionField menu.
The gaps Connections V2 fills: Connections only ever shows src↔dst for the current query — no expansion — and only a single src/dst field pair. Its only node action is "hide node." Connections V2 keeps the same node/edge model but adds expand-in-place (Phase 1) and multiple heterogeneous node types (Phase 2).
The expand mechanic (Phase 1 crux)
- Initial graph = the current search's
srcField → dstFieldconnections — identical to the Connections type, rendered via the shared engine. - Expand a node X → call
/api/connectionsfiltered on X (X.exp == value), same src/dst field pair, current time range.- This is what lets the graph reach entities not in the original query — that's the pivot.
- (Open question: whether expand is "free" —
X.exp == valuealone — or AND-ed with the base expression. Proposed default: free pivot within the active time range.)
- Merge the response into the live graph:
/api/connectionsreturns{ nodes, links }where linksource/targetare array indices. The merge layer must remap those indices to global node ids and dedupe by node id (combine weights/sessions) and by edge (combine values).- Mark X as expanded (so it's visually distinct and not trivially re-expanded).
- Prune if over the cap (below).
Shared force-graph engine (the refactor)
Extract from ConnectionsGraph.vue into a reusable engine (ForceGraph.vue / useForceGraph composable):
Moves into the engine (shared): D3 force simulation (link/charge/center/collision), SVG node+link render, drag, zoom/pan, text-size, lock/unlock, export PNG, Node/Link popups + legend. Takes nodes[] + links[] + render config; emits node/link interactions.
Stays type-specific:
- Connections: src/dst field controls,
minConn, weight,baselineDatecompare, its/api/connectionsload. - Pivot: expand → merge → prune, expanded-node marking, pin-to-protect, the Expand action added to
NodePopup.
⚠️ Safety net asymmetry. api-connections.t snapshot-tests the /api/connections backend, which Phase 1 Pivot reuses unchanged — so the backend stays covered. But the engine extraction is frontend D3 with no automated tests → Connections must be visually re-verified after the refactor (it's a behavior-preserving change to a proven 1,700-line file).
Performance: hard cap + prune
- Global cap on total nodes (default ~500; configurable via a control and/or user setting).
- When an expansion would exceed the cap, prune unpinned, non-just-expanded nodes by lowest weight / least-recently-touched until under the cap.
- Protect: pinned/locked nodes (reuse the existing lock concept) and the nodes from the current expansion.
- Feedback: show "showing N of M" and surface what was pruned (so removal isn't silent — see open questions).
- Per-expansion volume is still bounded by the Connections
lengthcap before the global prune runs.
SPIGraph registration
- Add
'pivot'tographTypeOptionsinSpigraph.vue(['default','heatmap','pie','table','treemap','sankey','connections','pivot']). - i18n
spigraph.graphType-pivot(e.g. "Pivot") across all 11 locales. - Dispatch like Connections: render
<arkime-pivot-graph>whenspiGraphType === 'pivot'; it owns its own data so SPIGraph's periodic refresh is stopped (add the matching branch tochangeSpiGraphType+ the connections-style guards in the load/route watchers). - Reuse the Connections src/dst field controls (Pivot uses the same field pair), via the existing controls-anchor pattern.
Components (Phase 1)
- New
viewer/.../connections/ForceGraph.vue(+ optional composable) — extracted shared D3 engine. - Refactor
ConnectionsGraph.vue— render viaForceGraph; keep its connections-specific controls/data. - New
viewer/.../spigraph/PivotGraph.vue— feedsForceGraph; owns expand/merge/prune + pin state. - Change
NodePopup.vue— add an Expand action (emitexpand); Connections can hide it, Pivot wires it. - Change
Spigraph.vue— register + dispatch thepivottype. - i18n —
graphType-pivot+ any pivot-specific control strings.
Phasing
Phase 1 — multi-hop pivot (expand-in-place):
- Extract the shared engine from
ConnectionsGraph; refactor Connections onto it; visually verify Connections is unchanged (backendapi-connections.tstill green). - Register the
pivottype; render the initial connections graph through the shared engine. - Expand-in-place — Expand action →
/api/connectionsfiltered on the node → merge (index remap + dedupe) → mark expanded. - Cap + prune + pinning + pruned-node feedback.
- Polish — expanded/pinned visual states, expand-scope behavior, UI review, i18n.
Phase 2 — multi-field / heterogeneous nodes (the other half of #147):
6. Allow multiple field types (host, ip, port, …) as node types in one graph, so a node can be a host, an IP, a port, etc. — and edges connect across types (e.g. ip → port, host → ip).
7. This resurfaces the connections backend work #147 flags: the current /api/connections is callback-based and single-pair; heterogeneous/multi-field support likely needs the callbacks → async/await rewrite of /api/connections (which Phase 1 deliberately dodges by reusing the endpoint as-is).
8. Heterogeneous session nodes (a session as a first-class node type) fold in here.
Out of scope for Phase 1
- Heterogeneous / cross-field pivot and session nodes — deferred to Phase 2 above (the multi-field half of #147). Reviving it resurfaces the
/api/connectionscallbacks→async/await rewrite, which Phase 1 dodges by reusing the endpoint as-is. - Shareable / URL-persisted pivot sessions (the accreted graph state isn't encoded in the query).
- A new backend endpoint for Phase 1 —
/api/connectionsis reused as-is (Phase 2 may extend it).
Remaining open questions (small)
- Expand scope: free pivot (
X.exp == valuealone) vs. AND-ed with the current base expression. (Proposed: free, within the active time range.) - Expand trigger: double-click node, a NodePopup "Expand" button, or both.
- Merge dedupe semantics: sum vs. max for node weight/sessions when a node reappears; how to combine duplicate edge values.
- Prune metric + notice: lowest-weight vs. least-recently-touched (vs. combined), and whether to show a "pruned N nodes" notice so removal isn't silent.
- Default cap + where configured: the ~500 number, and whether it's a graph control, a user setting, or both.
- Multi-field model (Phase 2): how heterogeneous node types + cross-type edges are configured and queried, and the shape of the
/api/connectionsrewrite.
Source: arkime/arkime