2026-05-18 17:11:36 +02:00
from __future__ import annotations
def graph_explorer_page ( ) - > str :
return """ <!doctype html>
< html lang = " en " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< title > Fabric Map < / title >
< style >
: root {
color - scheme : light ;
font - family : Inter , ui - sans - serif , system - ui , - apple - system , BlinkMacSystemFont , " Segoe UI " , sans - serif ;
- - bg : #f7f8fa;
- - panel : #ffffff;
- - line : #d6dce5;
- - text : #172033;
- - muted : #667085;
- - accent : #0f766e;
- - accent - 2 : #2563eb;
- - warn : #b45309;
}
* { box - sizing : border - box ; }
html , body { height : 100 % ; margin : 0 ; }
body { background : var ( - - bg ) ; color : var ( - - text ) ; font - size : 14 px ; }
. shell { display : grid ; grid - template - rows : auto 1 fr ; height : 100 vh ; min - height : 620 px ; }
. toolbar {
display : grid ;
2026-05-18 23:58:58 +02:00
grid - template - columns : minmax ( 180 px , 1.2 fr ) repeat ( 7 , minmax ( 108 px , .55 fr ) ) auto ;
2026-05-18 17:11:36 +02:00
gap : 8 px ;
align - items : center ;
padding : 10 px 12 px ;
border - bottom : 1 px solid var ( - - line ) ;
background : var ( - - panel ) ;
}
. main { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 340 px ; min - height : 0 ; }
. canvas - wrap { position : relative ; min - width : 0 ; min - height : 0 ; }
2026-05-24 17:02:12 +02:00
#graph-canvas { position: absolute; inset: 0; z-index: 0; }
. zone - overlay {
position : absolute ;
inset : 0 ;
z - index : 2 ;
overflow : hidden ;
pointer - events : none ;
}
. zone - boundary {
position : absolute ;
border : 2 px solid var ( - - zone - color , #2563eb);
border - radius : 8 px ;
background : var ( - - zone - fill , rgba ( 37 , 99 , 235 , .06 ) ) ;
box - shadow : inset 0 0 0 1 px rgba ( 255 , 255 , 255 , .74 ) , 0 10 px 32 px rgba ( 23 , 32 , 51 , .08 ) ;
min - height : 56 px ;
min - width : 90 px ;
pointer - events : none ;
}
. zone - boundary . selected {
border - width : 3 px ;
box - shadow : inset 0 0 0 1 px rgba ( 255 , 255 , 255 , .82 ) , 0 14 px 36 px rgba ( 23 , 32 , 51 , .14 ) ;
}
. zone - label {
position : absolute ;
top : 8 px ;
left : 10 px ;
z - index : 1 ;
min - height : 26 px ;
max - width : calc ( 100 % - 20 px ) ;
2026-05-25 01:09:05 +02:00
border : 0 ;
2026-05-24 17:02:12 +02:00
color : var ( - - zone - color , #2563eb);
2026-05-25 01:09:05 +02:00
background : transparent ;
box - shadow : none ;
cursor : grab ;
2026-05-24 17:02:12 +02:00
font - size : 12 px ;
font - weight : 700 ;
overflow : hidden ;
2026-05-25 01:09:05 +02:00
padding : 0 ;
2026-05-24 17:02:12 +02:00
pointer - events : auto ;
text - overflow : ellipsis ;
2026-05-25 01:09:05 +02:00
text - shadow : 0 1 px 0 rgba ( 255 , 255 , 255 , .78 ) , 0 0 8 px rgba ( 255 , 255 , 255 , .72 ) ;
2026-05-24 17:02:12 +02:00
}
2026-05-25 01:09:05 +02:00
. zone - label . dragging { cursor : grabbing ; }
2026-05-24 17:02:12 +02:00
. zone - label : focus - visible {
outline : 2 px solid var ( - - zone - color , #2563eb);
outline - offset : 2 px ;
}
2026-05-18 17:11:36 +02:00
. side {
border - left : 1 px solid var ( - - line ) ;
background : var ( - - panel ) ;
min - height : 0 ;
overflow : auto ;
padding : 14 px ;
}
. side h1 { font - size : 17 px ; line - height : 1.2 ; margin : 0 0 10 px ; }
. section { border - top : 1 px solid var ( - - line ) ; padding - top : 12 px ; margin - top : 12 px ; }
. section : first - child { border - top : 0 ; padding - top : 0 ; margin - top : 0 ; }
2026-05-18 23:58:58 +02:00
label , . field { color : var ( - - muted ) ; display : grid ; gap : 4 px ; font - size : 12 px ; }
2026-05-18 17:11:36 +02:00
input , select , button {
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
color : var ( - - text ) ;
font : inherit ;
min - height : 34 px ;
padding : 6 px 8 px ;
background : #ffffff;
}
button {
cursor : pointer ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
white - space : nowrap ;
}
button . primary { background : var ( - - accent ) ; border - color : var ( - - accent ) ; color : #ffffff; }
button : disabled { color : #98a2b3; cursor: default; }
2026-05-19 00:51:38 +02:00
. field - label {
display : inline - flex ;
align - items : center ;
gap : 5 px ;
}
. help - tip {
width : 18 px ;
height : 18 px ;
min - height : 18 px ;
padding : 0 ;
border - radius : 50 % ;
color : var ( - - muted ) ;
font - size : 11 px ;
line - height : 1 ;
background : #f8fafc;
}
. help - tip : hover , . help - tip : focus - visible {
border - color : var ( - - accent ) ;
color : var ( - - accent ) ;
outline : none ;
}
. help - popup {
position : fixed ;
z - index : 30 ;
display : none ;
max - width : 300 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .16 ) ;
color : var ( - - text ) ;
padding : 10 px ;
pointer - events : none ;
}
. help - popup strong { display : block ; margin - bottom : 4 px ; }
. help - popup p { margin : 0 ; color : var ( - - muted ) ; font - size : 12 px ; line - height : 1.35 ; }
2026-05-18 23:58:58 +02:00
. filter - menu {
position : relative ;
min - width : 0 ;
}
. filter - menu summary {
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
color : var ( - - text ) ;
cursor : pointer ;
list - style : none ;
min - height : 34 px ;
overflow : hidden ;
padding : 6 px 8 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
background : #ffffff;
}
. filter - menu summary : : - webkit - details - marker { display : none ; }
. filter - menu [ open ] summary { border - color : var ( - - accent ) ; }
. check - list {
position : absolute ;
z - index : 8 ;
display : grid ;
gap : 4 px ;
width : max - content ;
min - width : 210 px ;
max - width : 280 px ;
max - height : 300 px ;
overflow : auto ;
margin - top : 4 px ;
padding : 8 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .14 ) ;
}
. check - list label {
grid - template - columns : auto 1 fr ;
align - items : center ;
color : var ( - - text ) ;
font - size : 13 px ;
gap : 8 px ;
min - height : 26 px ;
}
. check - list input {
min - height : 0 ;
padding : 0 ;
}
. map - controls {
position : absolute ;
top : 12 px ;
right : 12 px ;
z - index : 3 ;
display : flex ;
2026-05-19 00:27:09 +02:00
align - items : end ;
2026-05-19 00:51:38 +02:00
flex - wrap : wrap ;
justify - content : flex - end ;
2026-05-18 23:58:58 +02:00
gap : 6 px ;
2026-05-19 00:51:38 +02:00
max - width : min ( 460 px , calc ( 100 % - 24 px ) ) ;
2026-05-18 23:58:58 +02:00
padding : 6 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : rgba ( 255 , 255 , 255 , .94 ) ;
box - shadow : 0 12 px 30 px rgba ( 23 , 32 , 51 , .12 ) ;
}
. map - controls button { min - width : 54 px ; }
2026-05-19 00:27:09 +02:00
. map - control - field {
display : grid ;
gap : 2 px ;
color : var ( - - muted ) ;
font - size : 11 px ;
line - height : 1 ;
}
. map - control - field select {
min - height : 30 px ;
min - width : 84 px ;
padding : 4 px 6 px ;
}
2026-05-24 17:02:12 +02:00
. map - control - field input [ type = " checkbox " ] {
width : 18 px ;
height : 18 px ;
min - height : 0 ;
margin : 5 px 0 ;
padding : 0 ;
accent - color : var ( - - accent ) ;
}
2026-05-19 00:16:57 +02:00
. selection - anchor {
position : absolute ;
z - index : 5 ;
display : none ;
width : 0 ;
height : 0 ;
pointer - events : none ;
transform : translate ( - 50 % , - 100 % ) ;
}
. selection - anchor : : before {
content : " " ;
position : absolute ;
left : - 1 px ;
top : - 36 px ;
width : 2 px ;
height : 28 px ;
border - radius : 999 px ;
background : #111827;
box - shadow : 0 1 px 2 px rgba ( 23 , 32 , 51 , .28 ) ;
}
. selection - anchor : : after {
content : " " ;
position : absolute ;
left : - 6 px ;
top : - 10 px ;
width : 0 ;
height : 0 ;
border - left : 6 px solid transparent ;
border - right : 6 px solid transparent ;
border - top : 10 px solid #111827;
filter : drop - shadow ( 0 1 px 1 px rgba ( 23 , 32 , 51 , .24 ) ) ;
}
. selection - anchor - label {
position : absolute ;
left : 12 px ;
top : - 48 px ;
max - width : 220 px ;
overflow : hidden ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : rgba ( 255 , 255 , 255 , .96 ) ;
box - shadow : 0 12 px 30 px rgba ( 23 , 32 , 51 , .12 ) ;
color : var ( - - text ) ;
font - size : 12 px ;
line - height : 1.25 ;
padding : 6 px 8 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
2026-05-18 17:11:36 +02:00
. button - row { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
. meta { color : var ( - - muted ) ; font - size : 12 px ; margin : 0 ; }
. pill {
display : inline - flex ;
align - items : center ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
color : var ( - - muted ) ;
min - height : 24 px ;
padding : 2 px 8 px ;
margin : 0 4 px 4 px 0 ;
font - size : 12 px ;
}
. detail - list { display : grid ; gap : 6 px ; margin : 8 px 0 0 ; padding : 0 ; list - style : none ; }
. detail - list li { overflow - wrap : anywhere ; }
2026-05-19 01:46:23 +02:00
. orientation - list { gap : 8 px ; }
. orientation - item {
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #fbfcfe;
padding : 8 px ;
}
. orientation - label {
color : var ( - - muted ) ;
display : block ;
font - size : 11 px ;
margin - bottom : 3 px ;
text - transform : uppercase ;
}
. orientation - value { color : var ( - - text ) ; line - height : 1.35 ; }
. orientation - path { font - family : ui - monospace , SFMono - Regular , Menlo , Consolas , monospace ; font - size : 12 px ; }
. orientation - warning { border - color : #f59e0b; background: #fffbeb; }
. orientation - good { border - color : #5eead4; background: #f0fdfa; }
2026-05-19 01:16:18 +02:00
. rule - panel {
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #fbfcfe;
}
. rule - panel summary {
cursor : pointer ;
list - style : none ;
padding : 10 px ;
}
. rule - panel summary : : - webkit - details - marker { display : none ; }
. rule - editor {
display : grid ;
gap : 8 px ;
padding : 0 10 px 10 px ;
}
. rule - grid {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 8 px ;
}
. rule - list {
display : grid ;
gap : 8 px ;
margin : 10 px 0 0 ;
padding : 0 ;
list - style : none ;
}
. rule - item {
display : grid ;
gap : 6 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
padding : 8 px ;
}
. rule - title {
align - items : center ;
display : flex ;
gap : 6 px ;
justify - content : space - between ;
}
. rule - actions { display : flex ; flex - wrap : wrap ; gap : 6 px ; }
. rule - actions button { min - height : 28 px ; padding : 4 px 7 px ; }
2026-05-18 17:11:36 +02:00
. popup {
position : absolute ;
z - index : 4 ;
max - width : 260 px ;
display : none ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .14 ) ;
padding : 10 px ;
pointer - events : none ;
}
. popup strong { display : block ; margin - bottom : 4 px ; }
. notice { color : var ( - - warn ) ; padding : 14 px ; }
2026-05-19 00:51:38 +02:00
. canvas - notice {
position : absolute ;
inset : 0 ;
display : grid ;
place - items : center ;
color : var ( - - muted ) ;
padding : 24 px ;
text - align : center ;
}
2026-05-18 17:11:36 +02:00
@media ( max - width : 900 px ) {
. shell { min - height : 760 px ; }
. toolbar { grid - template - columns : 1 fr 1 fr ; }
. main { grid - template - columns : 1 fr ; grid - template - rows : minmax ( 420 px , 1 fr ) 330 px ; }
. side { border - left : 0 ; border - top : 1 px solid var ( - - line ) ; }
2026-05-19 00:51:38 +02:00
. map - controls { top : 8 px ; right : 8 px ; max - width : calc ( 100 % - 16 px ) ; }
2026-05-19 01:16:18 +02:00
. rule - grid { grid - template - columns : 1 fr ; }
2026-05-18 17:11:36 +02:00
}
< / style >
< / head >
< body >
< section class = " shell " >
< div class = " toolbar " >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Search < button type = " button " class = " help-tip " aria - label = " Search help " data - help - title = " Search " data - help = " Search matches labels, ids, repos, node types, edge types, and descriptions. It hides non-matching nodes and edges without redrawing the layout. " > ? < / button > < / span >
< input id = " search " autocomplete = " off " aria - label = " Search graph " >
< / div >
< div class = " field " >
< span class = " field-label " > Mode < button type = " button " class = " help-tip " aria - label = " Mode help " data - help - title = " Mode " data - help = " Modes are predefined map views. Some modes use the selected item as context; changing modes changes which graph entities are visible, but keeps the layout controls separate. " > ? < / button > < / span >
< select id = " mode-select " aria - label = " Graph mode " > < / select >
< / div >
< div class = " field " >
< span class = " field-label " > Layout < button type = " button " class = " help-tip " aria - label = " Layout help " data - help - title = " Layout " data - help = " Layout redraws the map arrangement. Cose uses relationship strength and repo affinity; circle, grid, concentric, and breadthfirst are simpler alternate arrangements. " > ? < / button > < / span >
< select id = " layout-select " aria - label = " Graph layout " >
2026-05-18 18:31:25 +02:00
< option value = " cose " > Cose < / option >
< option value = " concentric " > Concentric < / option >
< option value = " circle " > Circle < / option >
< option value = " grid " > Grid < / option >
< option value = " breadthfirst " > Breadthfirst < / option >
2026-05-19 00:51:38 +02:00
< / select >
< / div >
< div class = " field " > < span class = " field-label " > Node Types < button type = " button " class = " help-tip " aria - label = " Node types help " data - help - title = " Node Types " data - help = " Nodes are the entities drawn on the map: repositories, services, deployments, servers, capabilities, interfaces, dependencies, bindings, and libraries. This filter hides unchecked node types while preserving the current layout. " > ? < / button > < / span >
2026-05-18 23:58:58 +02:00
< details id = " node-type-menu " class = " filter-menu " >
< summary id = " node-type-summary " > All node types < / summary >
< div id = " node-type-filter " class = " check-list " > < / div >
< / details >
< / div >
2026-05-19 00:51:38 +02:00
< div class = " field " > < span class = " field-label " > Edge Types < button type = " button " class = " help-tip " aria - label = " Edge types help " data - help - title = " Edge Types " data - help = " Edges are relationships between nodes, such as provides, consumes, runs_on, or binds. This filter hides unchecked relationship types while preserving the current layout. " > ? < / button > < / span >
2026-05-18 23:58:58 +02:00
< details id = " edge-type-menu " class = " filter-menu " >
< summary id = " edge-type-summary " > All edge types < / summary >
< div id = " edge-type-filter " class = " check-list " > < / div >
< / details >
< / div >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Review < button type = " button " class = " help-tip " aria - label = " Review help " data - help - title = " Review " data - help = " Review state applies only to entities that carry review metadata. Accepted means trusted data; candidate means registered or inferred data that still needs context. " > ? < / button > < / span >
< select id = " review-filter " aria - label = " Review state filter " > < option value = " " > Any < / option > < option > accepted < / option > < option > candidate < / option > < / select >
< / div >
< div class = " field " >
< span class = " field-label " > Unresolved < button type = " button " class = " help-tip " aria - label = " Unresolved help " data - help - title = " Unresolved " data - help = " Unresolved marks missing provider bindings or registered repositories without accepted graph snapshots. It only applies to node and edge types that can express those gaps. " > ? < / button > < / span >
< select id = " unresolved-filter " aria - label = " Unresolved filter " > < option value = " " > Any < / option > < option value = " true " > Only < / option > < / select >
< / div >
< div class = " field " >
2026-05-19 01:16:18 +02:00
< span class = " field-label " > Profile < button type = " button " class = " help-tip " aria - label = " Saved views help " data - help - title = " Saved Views " data - help = " Saved views store the current search, mode, layout, label density, filters, rules, and manual visibility overrides in this browser. Copy State creates a shareable URL for the current view. " > ? < / button > < / span >
2026-05-19 00:51:38 +02:00
< select id = " profile-select " aria - label = " Saved view profile " disabled > < option > Unsaved exploration < / option > < / select >
< / div >
2026-05-18 17:11:36 +02:00
< span id = " counts " class = " meta " > < / span >
< / div >
< main class = " main " >
< div class = " canvas-wrap " >
2026-05-19 00:51:38 +02:00
< div id = " graph-canvas " role = " img " aria - label = " Interactive Fabric graph " aria - live = " polite " > < p class = " canvas-notice " > Loading graph . . . < / p > < / div >
2026-05-24 17:02:12 +02:00
< div id = " zone-overlay " class = " zone-overlay " aria - label = " Deployment zone boundaries " > < / div >
2026-05-18 23:58:58 +02:00
< div class = " map-controls " aria - label = " Map navigation controls " >
2026-05-24 17:02:12 +02:00
< label class = " map-control-field " > < span class = " field-label " > Zones < button type = " button " class = " help-tip " aria - label = " Zone boundary help " data - help - title = " Zones " data - help = " Zone boundaries draw labeled rectangles around visible nodes that share deployment overlay fields. They are visual annotations only. " > ? < / button > < / span >
< input id = " zone-boundary-toggle " type = " checkbox " aria - label = " Show zone boundaries " checked >
< / label >
< label class = " map-control-field " > < span class = " field-label " > Zone By < button type = " button " class = " help-tip " aria - label = " Zone grouping help " data - help - title = " Zone By " data - help = " Environment groups nodes as dev-tegwick, test, or prod. Access Zone groups by intended reachability such as private-dev or production-public. " > ? < / button > < / span >
< select id = " zone-group-select " title = " Choose zone boundary grouping " >
< option value = " deploymentEnvironment " > Environment < / option >
< option value = " accessZone " > Access Zone < / option >
< / select >
< / label >
2026-05-25 03:17:23 +02:00
< label class = " map-control-field " > < span class = " field-label " > Zone Layout < button type = " button " class = " help-tip " aria - label = " Zone layout help " data - help - title = " Zone Layout " data - help = " Zone layout changes how each zone arranges its assigned visible nodes inside the stable zone rectangle. It does not change the underlying Fabric graph. " > ? < / button > < / span >
< select id = " zone-layout-select " title = " Choose zone-local layout algorithm " >
< option value = " compact-grid " > Grid < / option >
< option value = " circle " > Circle < / option >
< / select >
< / label >
2026-05-19 00:51:38 +02:00
< label class = " map-control-field " > < span class = " field-label " > Labels < button type = " button " class = " help-tip " aria - label = " Labels help " data - help - title = " Labels " data - help = " Label density changes only text visibility. Auto hides low-priority labels when the map is dense; Key keeps repositories, services, deployments, servers, and issue markers visible. " > ? < / button > < / span >
2026-05-19 00:27:09 +02:00
< select id = " label-select " title = " Control node label density " >
< option value = " auto " > Auto < / option >
< option value = " key " > Key < / option >
< option value = " all " > All < / option >
< option value = " none " > None < / option >
< / select >
< / label >
2026-05-19 00:51:38 +02:00
< button type = " button " data - action = " fit " title = " Fit graph to view " data - help - title = " Fit " data - help = " Fit pans and zooms the current visible graph into the map viewport. It does not change filters, labels, or layout. " > Fit < / button >
< button type = " button " data - action = " focus " title = " Focus selected neighborhood " data - help - title = " Focus " data - help = " Focus narrows the map to the selected entity and its nearby context. It changes visibility, not the underlying registry data. " > Focus < / button >
< button type = " button " data - action = " clear " class = " primary " title = " Reset map controls " data - help - title = " Reset " data - help = " Reset clears search, mode, node and edge type filters, label density, unresolved/review filters, focus, and manual overrides. " > Reset < / button >
2026-05-18 23:58:58 +02:00
< / div >
2026-05-19 00:16:57 +02:00
< div id = " selection-anchor " class = " selection-anchor " aria - hidden = " true " >
< span id = " selection-anchor-label " class = " selection-anchor-label " > < / span >
< / div >
2026-05-18 17:11:36 +02:00
< div id = " popup " class = " popup " > < / div >
2026-05-19 00:51:38 +02:00
< div id = " help-popup " class = " help-popup " role = " tooltip " > < / div >
2026-05-18 17:11:36 +02:00
< / div >
< aside class = " side " >
< section class = " section " >
< h1 id = " detail-title " > Fabric Map < / h1 >
< p id = " detail-summary " class = " meta " > No selection < / p >
< div id = " detail-pills " > < / div >
< ul id = " detail-list " class = " detail-list " > < / ul >
< / section >
2026-05-18 18:36:15 +02:00
< section class = " section " >
< p id = " orientation-title " class = " meta " > Select a service , interface , dependency , or registered - only repo . < / p >
2026-05-19 01:46:23 +02:00
< ul id = " orientation-list " class = " detail-list orientation-list " > < / ul >
< div id = " orientation-actions " class = " button-row " > < / div >
2026-05-18 18:36:15 +02:00
< / section >
2026-05-18 17:11:36 +02:00
< section class = " section " >
< div class = " button-row " >
2026-05-19 00:51:38 +02:00
< button type = " button " data - override = " show " data - help - title = " Show " data - help = " Show pins the selected entity visible even when other filters would hide it. It does not redraw the layout. " > Show < / button >
< button type = " button " data - override = " blur " data - help - title = " Blur " data - help = " Blur keeps the selected entity in place but reduces emphasis. It is useful for context that should stay visible without competing for attention. " > Blur < / button >
< button type = " button " data - override = " hide " data - help - title = " Hide " data - help = " Hide makes the selected entity invisible while preserving layout context. Remove is reserved for the upcoming rule panel and will redraw the graph without the entity. " > Hide < / button >
< button type = " button " data - action = " reset-overrides " data - help - title = " Clear Overrides " data - help = " Clear Overrides removes manual Show, Blur, and Hide choices. Filters, mode, layout, and label density stay as they are. " > Clear Overrides < / button >
2026-05-18 17:11:36 +02:00
< / div >
< / section >
2026-05-19 01:16:18 +02:00
< section class = " section " >
< details id = " rule-panel " class = " rule-panel " >
< summary > < span class = " field-label " > Rules < button type = " button " class = " help-tip " aria - label = " Rule builder help " data - help - title = " Rule Builder " data - help = " Rules select nodes or edges by type and available attributes, then apply a modifier. Hide preserves the current layout; Remove redraws the map without matching entities. " > ? < / button > < / span > < / summary >
< div class = " rule-editor " >
< div class = " rule-grid " >
< label > Target
< select id = " rule-target " >
< option value = " node " > Nodes < / option >
< option value = " edge " > Edges < / option >
< / select >
< / label >
< label > Type
< select id = " rule-type " > < / select >
< / label >
< label > Attribute
< select id = " rule-attribute " > < / select >
< / label >
< label > Value
< select id = " rule-value " > < / select >
< / label >
< label > Modifier
< select id = " rule-action " >
< option value = " highlight " > Highlight < / option >
< option value = " show " > Show < / option >
< option value = " blur " > Blur < / option >
< option value = " hide " > Hide < / option >
< option value = " remove " > Remove and redraw < / option >
< / select >
< / label >
< / div >
< p id = " rule-context " class = " meta " > Rules are applied top to bottom ; later matching rules refine earlier ones . < / p >
< div class = " button-row " >
< button type = " button " data - rule - action = " save " class = " primary " > Add Rule < / button >
< button type = " button " data - rule - action = " cancel " > Cancel Edit < / button >
< button type = " button " data - rule - action = " reset " > Reset Rules < / button >
< / div >
< ul id = " rule-list " class = " rule-list " > < / ul >
< / div >
< / details >
< / section >
2026-05-18 18:31:25 +02:00
< section class = " section " >
< p id = " profile-summary " class = " meta " > Profile persistence unavailable for this host . < / p >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Profile Name < button type = " button " class = " help-tip " aria - label = " Profile name help " data - help - title = " Profile Name " data - help = " Profile Name labels a saved browser-local map view. It does not rename graph entities or registry data. " > ? < / button > < / span >
< input id = " profile-name " autocomplete = " off " aria - label = " Profile name " disabled >
< / div >
2026-05-18 18:31:25 +02:00
< div class = " button-row " >
2026-05-19 01:16:18 +02:00
< button type = " button " data - profile - action = " save " data - help - title = " Save " data - help = " Save stores the current browser-local view, including filters, rules, layout, label density, and manual overrides. " disabled > Save < / button >
2026-05-19 00:51:38 +02:00
< button type = " button " data - profile - action = " duplicate " data - help - title = " Duplicate " data - help = " Duplicate creates a new saved view from the currently loaded profile and current map state. " disabled > Duplicate < / button >
< button type = " button " data - profile - action = " delete " data - help - title = " Delete " data - help = " Delete removes the selected browser-local saved view. It does not delete graph data. " disabled > Delete < / button >
2026-05-19 01:16:18 +02:00
< button type = " button " data - profile - action = " copy " data - help - title = " Copy State " data - help = " Copy State copies a URL that can restore this map state, including filters, rules, and manual overrides. " disabled > Copy State < / button >
2026-05-18 18:31:25 +02:00
< / div >
< / section >
2026-05-18 17:11:36 +02:00
< section class = " section " >
2026-05-19 00:51:38 +02:00
< p class = " meta " > < span id = " hidden-summary " > Hidden 0 < / span > < button type = " button " class = " help-tip " aria - label = " Hidden entities help " data - help - title = " Hidden Entities " data - help = " Hidden counts entities currently not shown because of filters, mode, or manual Hide overrides. Hidden keeps layout context; Remove will redraw without the entity in the rule panel. " > ? < / button > < / p >
2026-05-18 17:11:36 +02:00
< div id = " legend " > < / div >
< / section >
< / aside >
< / main >
< / section >
< script src = " https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js " > < / script >
< script >
( ( ) = > {
const manifestUrl = " /exports/graph-explorer/manifest " ;
const graphUrl = " /exports/graph-explorer " ;
const canvas = document . getElementById ( " graph-canvas " ) ;
2026-05-24 17:02:12 +02:00
const zoneOverlay = document . getElementById ( " zone-overlay " ) ;
2026-05-18 17:11:36 +02:00
const popup = document . getElementById ( " popup " ) ;
2026-05-19 00:51:38 +02:00
const helpPopup = document . getElementById ( " help-popup " ) ;
2026-05-19 00:16:57 +02:00
const selectionAnchor = document . getElementById ( " selection-anchor " ) ;
const selectionAnchorLabel = document . getElementById ( " selection-anchor-label " ) ;
2026-05-18 17:11:36 +02:00
const searchInput = document . getElementById ( " search " ) ;
2026-05-18 18:31:25 +02:00
const modeSelect = document . getElementById ( " mode-select " ) ;
const layoutSelect = document . getElementById ( " layout-select " ) ;
2026-05-19 00:27:09 +02:00
const labelSelect = document . getElementById ( " label-select " ) ;
2026-05-24 17:02:12 +02:00
const zoneBoundaryToggle = document . getElementById ( " zone-boundary-toggle " ) ;
const zoneGroupSelect = document . getElementById ( " zone-group-select " ) ;
2026-05-25 03:17:23 +02:00
const zoneLayoutSelect = document . getElementById ( " zone-layout-select " ) ;
2026-05-18 23:58:58 +02:00
const nodeTypeFilter = document . getElementById ( " node-type-filter " ) ;
const nodeTypeSummary = document . getElementById ( " node-type-summary " ) ;
const edgeTypeFilter = document . getElementById ( " edge-type-filter " ) ;
const edgeTypeSummary = document . getElementById ( " edge-type-summary " ) ;
2026-05-18 17:11:36 +02:00
const reviewFilter = document . getElementById ( " review-filter " ) ;
const unresolvedFilter = document . getElementById ( " unresolved-filter " ) ;
2026-05-19 01:16:18 +02:00
const ruleTarget = document . getElementById ( " rule-target " ) ;
const ruleType = document . getElementById ( " rule-type " ) ;
const ruleAttribute = document . getElementById ( " rule-attribute " ) ;
const ruleValue = document . getElementById ( " rule-value " ) ;
const ruleAction = document . getElementById ( " rule-action " ) ;
const ruleContext = document . getElementById ( " rule-context " ) ;
const ruleList = document . getElementById ( " rule-list " ) ;
2026-05-18 18:31:25 +02:00
const profileSelect = document . getElementById ( " profile-select " ) ;
2026-05-18 20:14:07 +02:00
const profileNameInput = document . getElementById ( " profile-name " ) ;
2026-05-18 18:31:25 +02:00
const profileSummary = document . getElementById ( " profile-summary " ) ;
2026-05-18 17:11:36 +02:00
const counts = document . getElementById ( " counts " ) ;
const hiddenSummary = document . getElementById ( " hidden-summary " ) ;
const detailTitle = document . getElementById ( " detail-title " ) ;
const detailSummary = document . getElementById ( " detail-summary " ) ;
const detailPills = document . getElementById ( " detail-pills " ) ;
const detailList = document . getElementById ( " detail-list " ) ;
2026-05-18 18:36:15 +02:00
const orientationTitle = document . getElementById ( " orientation-title " ) ;
const orientationList = document . getElementById ( " orientation-list " ) ;
2026-05-19 01:46:23 +02:00
const orientationActions = document . getElementById ( " orientation-actions " ) ;
2026-05-18 17:11:36 +02:00
const legend = document . getElementById ( " legend " ) ;
2026-05-18 20:14:07 +02:00
const profileStorageKey = " railiance.fabric.graphExplorer.profiles " ;
2026-05-18 17:11:36 +02:00
let cy = null ;
let selected = null ;
2026-05-19 00:16:57 +02:00
let selectedAnchor = null ;
2026-05-18 17:11:36 +02:00
let focusSet = null ;
let manualOverrides = { } ;
let layerColors = { } ;
2026-05-18 23:58:58 +02:00
let nodeTypeLabels = { } ;
let allNodeTypes = [ ] ;
let allEdgeTypes = [ ] ;
2026-05-18 18:31:25 +02:00
let activeMode = " full " ;
2026-05-19 00:27:09 +02:00
let activeLabelMode = " auto " ;
2026-05-24 17:02:12 +02:00
let activeZoneGrouping = " deploymentEnvironment " ;
2026-05-25 00:27:10 +02:00
let activeZoneDefinitionSet = " fabric-default " ;
2026-05-25 03:17:23 +02:00
let activeZoneLayoutAlgorithm = " compact-grid " ;
2026-05-18 20:14:07 +02:00
let profilePersistence = " none " ;
let profiles = [ ] ;
let currentProfileId = " " ;
2026-05-19 01:16:18 +02:00
let filterRules = [ ] ;
let editingRuleId = " " ;
2026-05-19 01:46:23 +02:00
let orientationContext = null ;
2026-05-24 17:02:12 +02:00
let selectedZoneId = " " ;
let zoneOverlayFrame = 0 ;
let zoneSummaries = new Map ( ) ;
2026-05-25 02:08:45 +02:00
let zoneContainerState = new Map ( ) ;
2026-05-25 01:09:05 +02:00
let zoneDragState = null ;
let suppressZoneClick = false ;
2026-05-25 00:40:13 +02:00
let collapsedZoneSnapshots = new Map ( ) ;
const zoneCollapsePrefix = " zone-collapse: " ;
2026-05-18 17:11:36 +02:00
const escapeHtml = ( value ) = > String ( value ? ? " " )
. replaceAll ( " & " , " & " ) . replaceAll ( " < " , " < " )
. replaceAll ( " > " , " > " ) . replaceAll ( ' " ' , " " " ) ;
2026-05-19 00:51:38 +02:00
const showHelp = ( target ) = > {
if ( ! helpPopup | | ! target ? . dataset ? . help ) return ;
const bounds = target . getBoundingClientRect ( ) ;
const title = target . dataset . helpTitle | | target . textContent | | " Help " ;
helpPopup . innerHTML = ` < strong > $ { escapeHtml ( title ) } < / strong > < p > $ { escapeHtml ( target . dataset . help ) } < / p > ` ;
helpPopup . style . display = " block " ;
const width = Math . min ( helpPopup . offsetWidth | | 300 , 300 ) ;
const left = Math . min ( Math . max ( 12 , bounds . left ) , window . innerWidth - width - 12 ) ;
const top = bounds . bottom + 8 < window . innerHeight - 80
? bounds . bottom + 8
: Math . max ( 12 , bounds . top - helpPopup . offsetHeight - 8 ) ;
helpPopup . style . left = ` $ { left } px ` ;
helpPopup . style . top = ` $ { top } px ` ;
} ;
const hideHelp = ( ) = > {
if ( ! helpPopup ) return ;
helpPopup . style . display = " none " ;
} ;
2026-05-18 17:11:36 +02:00
const elementText = ( data ) = > [
data . id , data . stableKey , data . label , data . name , data . description ,
2026-05-24 15:55:05 +02:00
data . repo , data . domain , data . kind , data . layer , data . edgeType ,
data . deploymentEnvironment , data . deploymentScenario , data . routingAuthority ,
data . accessZone , data . policyAuthority , data . exposureClass ,
data . routeHost , data . routeHostname , data . routePort , data . routeProtocol , data . route
2026-05-18 17:11:36 +02:00
] . join ( " " ) . toLowerCase ( ) ;
2026-05-18 20:14:07 +02:00
const overrideKey = ( element ) = > element . data ( " stableKey " ) | | element . id ( ) ;
const manualOverrideFor = ( element ) = >
manualOverrides [ overrideKey ( element ) ] | | manualOverrides [ element . id ( ) ] ;
const hasManualOverrides = ( ) = > Object . keys ( manualOverrides ) . length > 0 ;
const supportsLocalProfiles = ( ) = > profilePersistence == = " local " ;
2026-05-18 23:58:58 +02:00
const humanize = ( value ) = > String ( value | | " " )
. replaceAll ( " _ " , " " )
. replaceAll ( " : " , " : " )
. replace ( / \\b \\w / g , ( letter ) = > letter . toUpperCase ( ) ) ;
const parseListParam = ( value ) = > String ( value | | " " )
. split ( " , " )
. map ( ( item ) = > item . trim ( ) )
. filter ( Boolean ) ;
const checkedValues = ( container ) = > new Set (
Array . from ( container . querySelectorAll ( " input[type= ' checkbox ' ] " ) )
. filter ( ( input ) = > input . checked )
. map ( ( input ) = > input . value )
) ;
const setCheckedValues = ( container , values = null ) = > {
const explicit = Array . isArray ( values ) ;
const selectedValues = new Set ( values | | [ ] ) ;
container . querySelectorAll ( " input[type= ' checkbox ' ] " ) . forEach ( ( input ) = > {
input . checked = explicit ? selectedValues . has ( input . value ) : true ;
} ) ;
} ;
const selectedNodeTypes = ( ) = > checkedValues ( nodeTypeFilter ) ;
const selectedEdgeTypes = ( ) = > checkedValues ( edgeTypeFilter ) ;
2026-05-24 15:55:05 +02:00
const hasValue = ( value ) = > value != = undefined & & value != = null & & value != = " " ;
const overlayRuleAttributes = [
" deploymentEnvironment " ,
" deploymentScenario " ,
" routingAuthority " ,
" accessZone " ,
" policyAuthority " ,
" exposureClass " ,
" routeHost " ,
" routeHostname " ,
" routePort " ,
" routeProtocol " ,
] ;
const zoneModeFields = {
" by-fabric " : [ " domain " , " repo " , " kind " ] ,
" by-deployment-environment " : [ " deploymentEnvironment " ] ,
" by-deployment-scenario " : [ " deploymentScenario " ] ,
" by-routing-authority " : [ " routingAuthority " ] ,
" by-access-zone " : [ " accessZone " ] ,
} ;
const zoneModeLabels = {
" by-fabric " : " Fabric " ,
" by-deployment-environment " : " Deployment Environment " ,
" by-deployment-scenario " : " Deployment Scenario " ,
" by-routing-authority " : " Routing Authority " ,
" by-access-zone " : " Access Zone " ,
} ;
2026-05-18 23:58:58 +02:00
const summarizeSelection = ( selected , allValues , labels , noun ) = > {
if ( selected . size == = 0 ) return ` No $ { noun } ` ;
if ( selected . size == = allValues . length ) return ` All $ { noun } ` ;
const names = allValues
. filter ( ( value ) = > selected . has ( value ) )
. map ( ( value ) = > labels [ value ] | | humanize ( value ) ) ;
return names . length < = 2 ? names . join ( " , " ) : ` $ { names . length } $ { noun } ` ;
} ;
const syncFilterSummaries = ( ) = > {
nodeTypeSummary . textContent = summarizeSelection (
selectedNodeTypes ( ) , allNodeTypes , nodeTypeLabels , " node types "
) ;
edgeTypeSummary . textContent = summarizeSelection (
selectedEdgeTypes ( ) , allEdgeTypes , { } , " edge types "
) ;
} ;
const renderChecklist = ( container , values , labels , name ) = > {
container . innerHTML = values . map ( ( value ) = > {
const label = labels [ value ] | | humanize ( value ) ;
return ` < label > < input type = " checkbox " name = " $ { escapeHtml(name)} " value = " $ { escapeHtml(value)} " checked > $ { escapeHtml ( label ) } < / label > ` ;
} ) . join ( " " ) ;
} ;
2026-05-19 01:16:18 +02:00
const ruleActionLabels = {
show : " Show " ,
hide : " Hide " ,
blur : " Blur " ,
highlight : " Highlight " ,
remove : " Remove and redraw " ,
} ;
const ruleAttributeLabels = {
any : " Any attribute " ,
repo : " Repo " ,
kind : " Kind " ,
reviewState : " Review state " ,
unresolved : " Unresolved " ,
lifecycle : " Lifecycle " ,
strength : " Strength " ,
sameRepo : " Same repo " ,
layoutAffinity : " Layout affinity " ,
2026-05-24 15:55:05 +02:00
deploymentEnvironment : " Deployment Environment " ,
deploymentScenario : " Deployment Scenario " ,
routingAuthority : " Routing Authority " ,
accessZone : " Access Zone " ,
policyAuthority : " Policy Authority " ,
exposureClass : " Exposure Class " ,
routeHost : " Route Host " ,
routeHostname : " Route Hostname " ,
routePort : " Route Port " ,
routeProtocol : " Route Protocol " ,
2026-05-19 01:16:18 +02:00
} ;
const ruleAttributeCandidates = {
2026-05-24 15:55:05 +02:00
node : [ " any " , " repo " , " kind " , " reviewState " , " unresolved " , " lifecycle " , . . . overlayRuleAttributes ] ,
edge : [ " any " , " repo " , " kind " , " reviewState " , " unresolved " , " strength " , " sameRepo " , " layoutAffinity " , . . . overlayRuleAttributes ] ,
2026-05-19 01:16:18 +02:00
} ;
const renderOptions = ( select , options , current = " " ) = > {
select . innerHTML = options . map ( ( option ) = >
` < option value = " $ { escapeHtml(option.value)} " > $ { escapeHtml ( option . label ) } < / option > `
) . join ( " " ) ;
if ( options . some ( ( option ) = > option . value == = current ) ) select . value = current ;
} ;
const ruleElementType = ( element , target ) = >
target == = " edge " ? element . data ( " edgeType " ) : element . data ( " layer " ) ;
const currentRuleElements = ( ) = > {
if ( ! cy ) return [ ] ;
const target = ruleTarget . value | | " node " ;
const type = ruleType . value | | " " ;
const elements = target == = " edge " ? cy . edges ( ) : cy . nodes ( ) ;
return elements . filter ( ( element ) = > ! type | | ruleElementType ( element , target ) == = type ) . toArray ( ) ;
} ;
const valuesForRuleAttribute = ( attribute ) = > {
if ( attribute == = " any " ) return [ { value : " " , label : " Any " } ] ;
const values = Array . from ( new Set ( currentRuleElements ( )
. map ( ( element ) = > element . data ( attribute ) )
. filter ( ( value ) = > value != = undefined & & value != = null & & value != = " " )
. map ( ( value ) = > String ( value ) )
) ) . sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
return values . length
? values . map ( ( value ) = > ( { value , label : humanize ( value ) } ) )
: [ { value : " " , label : " No values " } ] ;
} ;
const availableRuleAttributes = ( ) = > {
const elements = currentRuleElements ( ) ;
return ( ruleAttributeCandidates [ ruleTarget . value | | " node " ] | | [ " any " ] )
. filter ( ( attribute ) = > attribute == = " any " | | elements . some ( ( element ) = > {
const value = element . data ( attribute ) ;
return value != = undefined & & value != = null & & value != = " " ;
} ) )
. map ( ( attribute ) = > ( { value : attribute , label : ruleAttributeLabels [ attribute ] | | humanize ( attribute ) } ) ) ;
} ;
const refreshRuleBuilder = ( state = { } ) = > {
const target = state . target | | ruleTarget . value | | " node " ;
ruleTarget . value = target ;
const types = target == = " edge " ? allEdgeTypes : allNodeTypes ;
const labels = target == = " edge " ? { } : nodeTypeLabels ;
renderOptions ( ruleType , [
{ value : " " , label : target == = " edge " ? " All edge types " : " All node types " } ,
. . . types . map ( ( type ) = > ( { value : type , label : labels [ type ] | | humanize ( type ) } ) ) ,
] , state . type ? ? ruleType . value ) ;
renderOptions ( ruleAttribute , availableRuleAttributes ( ) , state . attribute ? ? ruleAttribute . value ) ;
renderOptions ( ruleValue , valuesForRuleAttribute ( ruleAttribute . value | | " any " ) , state . value ? ? ruleValue . value ) ;
if ( state . action & & ruleActionLabels [ state . action ] ) ruleAction . value = state . action ;
ruleValue . disabled = ( ruleAttribute . value | | " any " ) == = " any " | | ruleValue . options . length == = 0 ;
const attributeNames = Array . from ( ruleAttribute . options ) . map ( ( option ) = > option . textContent ) . join ( " , " ) ;
ruleContext . textContent = attributeNames
? ` Available here : $ { attributeNames } . Rules are applied top to bottom ; later matches refine earlier ones . `
: " No attributes are available for this type yet. " ;
} ;
const normalizeRules = ( rules ) = > Array . isArray ( rules )
? rules
. filter ( ( rule ) = > rule & & ( rule . target == = " node " | | rule . target == = " edge " ) & & ruleActionLabels [ rule . action ] )
. map ( ( rule , index ) = > ( {
id : String ( rule . id | | ` rule - $ { Date . now ( ) . toString ( 36 ) } - $ { index } ` ) ,
target : rule . target ,
type : String ( rule . type | | " " ) ,
attribute : String ( rule . attribute | | " any " ) ,
value : String ( rule . value | | " " ) ,
action : rule . action ,
} ) )
: [ ] ;
const ruleDescription = ( rule ) = > {
const target = rule . target == = " edge " ? " edges " : " nodes " ;
const typeLabel = rule . type
? ( rule . target == = " edge " ? humanize ( rule . type ) : nodeTypeLabels [ rule . type ] | | humanize ( rule . type ) )
: ` all $ { target } ` ;
const attribute = rule . attribute & & rule . attribute != = " any "
? ` $ { ruleAttributeLabels [ rule . attribute ] | | humanize ( rule . attribute ) } = $ { humanize ( rule . value ) } `
: " any attribute " ;
return ` $ { ruleActionLabels [ rule . action ] } $ { typeLabel } where $ { attribute } ` ;
} ;
const renderRules = ( ) = > {
const saveButton = document . querySelector ( " [data-rule-action= ' save ' ] " ) ;
const cancelButton = document . querySelector ( " [data-rule-action= ' cancel ' ] " ) ;
saveButton . textContent = editingRuleId ? " Update Rule " : " Add Rule " ;
cancelButton . disabled = ! editingRuleId ;
ruleList . innerHTML = filterRules . length
? filterRules . map ( ( rule , index ) = > `
< li class = " rule-item " data - rule - id = " $ { escapeHtml(rule.id)} " >
< div class = " rule-title " > < strong > $ { escapeHtml ( ruleDescription ( rule ) ) } < / strong > < span class = " meta " > $ { index + 1 } < / span > < / div >
< div class = " rule-actions " >
< button type = " button " data - rule - list - action = " up " $ { index == = 0 ? " disabled " : " " } > Up < / button >
< button type = " button " data - rule - list - action = " down " $ { index == = filterRules . length - 1 ? " disabled " : " " } > Down < / button >
< button type = " button " data - rule - list - action = " edit " > Edit < / button >
< button type = " button " data - rule - list - action = " delete " > Delete < / button >
< / div >
< / li >
` ) . join ( " " )
: ' <li class= " meta " >No rules yet.</li> ' ;
} ;
const matchesRule = ( element , rule ) = > {
if ( rule . target == = " node " & & ! element . isNode ( ) ) return false ;
if ( rule . target == = " edge " & & ! element . isEdge ( ) ) return false ;
if ( rule . type & & ruleElementType ( element , rule . target ) != = rule . type ) return false ;
if ( ! rule . attribute | | rule . attribute == = " any " ) return true ;
const value = element . data ( rule . attribute ) ;
return value != = undefined & & value != = null & & String ( value ) == = String ( rule . value ) ;
} ;
const ruleActionFor = ( element ) = > {
let action = " " ;
filterRules . forEach ( ( rule ) = > {
if ( matchesRule ( element , rule ) ) action = rule . action ;
} ) ;
return action ;
} ;
const ruleRemovalSignature = ( ) = > cy
? cy . elements ( )
. filter ( ( element ) = > element . data ( " displayState " ) == = " remove " )
. map ( ( element ) = > element . id ( ) )
. sort ( )
. join ( " | " )
: " " ;
2026-05-19 00:16:57 +02:00
const isFinitePoint = ( position ) = >
position & & Number . isFinite ( position . x ) & & Number . isFinite ( position . y ) ;
const elementLabel = ( element ) = > {
if ( ! element ) return " " ;
const data = element . data ( ) ;
return data . name | | data . label | | data . edgeType | | data . id ;
} ;
2026-05-19 01:46:23 +02:00
const nodeById = ( id ) = > {
if ( ! cy | | ! id ) return null ;
const node = cy . getElementById ( id ) ;
return node & & node . length > 0 ? node : null ;
} ;
const nodeName = ( id ) = > {
const node = nodeById ( id ) ;
return node ? elementLabel ( node ) : id ;
} ;
const addContextElement = ( ids , element ) = > {
if ( ! element | | element . length == = 0 ) return ;
ids . add ( element . id ( ) ) ;
} ;
const addContextNode = ( ids , id ) = > {
const node = nodeById ( id ) ;
if ( node ) ids . add ( node . id ( ) ) ;
} ;
const addContextEdge = ( ids , edge ) = > {
addContextElement ( ids , edge ) ;
if ( ! edge | | edge . length == = 0 ) return ;
addContextNode ( ids , edge . data ( " source " ) ) ;
addContextNode ( ids , edge . data ( " target " ) ) ;
} ;
const edgeCollection = ( source = " " , target = " " , type = " " ) = > cy ? cy . edges ( ) . filter ( ( edge ) = >
( ! source | | edge . data ( " source " ) == = source ) & &
( ! target | | edge . data ( " target " ) == = target ) & &
( ! type | | edge . data ( " edgeType " ) == = type )
) : [ ] ;
2026-05-24 15:55:05 +02:00
const routeLabel = ( data ) = > {
if ( hasValue ( data . route ) ) return data . route ;
const host = data . routeHost | | data . routeHostname | | " " ;
const protocol = data . routeProtocol | | " " ;
const port = data . routePort | | " " ;
if ( ! hasValue ( host ) & & ! hasValue ( port ) ) return " " ;
const authority = hasValue ( port ) ? ` $ { host } : $ { port } ` : host ;
return [ protocol , authority ] . filter ( hasValue ) . join ( " " ) ;
} ;
const hasRouteEvidence = ( data ) = >
hasValue ( data . route ) | |
hasValue ( data . routeHost ) | |
hasValue ( data . routeHostname ) | |
hasValue ( data . routePort ) ;
const zoneWarningsForData = ( data ) = > {
const warnings = [ ] ;
if ( hasRouteEvidence ( data ) & & ! hasValue ( data . policyAuthority ) ) {
warnings . push ( " route without policy authority " ) ;
}
return warnings ;
} ;
2026-05-24 17:02:12 +02:00
const zonePalette = {
" deploymentEnvironment:dev-tegwick " : { color : " #0f766e " , fill : " rgba(15, 118, 110, .07) " } ,
" deploymentEnvironment:test " : { color : " #2563eb " , fill : " rgba(37, 99, 235, .07) " } ,
" deploymentEnvironment:prod " : { color : " #be123c " , fill : " rgba(190, 18, 60, .07) " } ,
} ;
const fallbackZonePalette = [
{ color : " #7c3aed " , fill : " rgba(124, 58, 237, .07) " } ,
{ color : " #b45309 " , fill : " rgba(180, 83, 9, .08) " } ,
{ color : " #0891b2 " , fill : " rgba(8, 145, 178, .08) " } ,
{ color : " #4f46e5 " , fill : " rgba(79, 70, 229, .07) " } ,
] ;
const hashString = ( value ) = > String ( value | | " " )
. split ( " " )
. reduce ( ( total , character ) = > ( total * 31 + character . charCodeAt ( 0 ) ) >> > 0 , 0 ) ;
2026-05-25 00:00:28 +02:00
const normalizeDeploymentZoneValue = ( value ) = > {
const raw = String ( value | | " " ) . trim ( ) ;
const normalized = raw . toLowerCase ( ) ;
if ( ! normalized | | normalized == = " all " ) return " " ;
if ( normalized == = " dev " ) return " dev-tegwick " ;
if ( normalized == = " test " | | normalized == = " staging " ) return " test " ;
if ( normalized == = " prod " ) return " prod " ;
return raw ;
} ;
const defaultZoneDefinitions = {
deploymentEnvironment : [
{
id : " deploymentEnvironment:dev-tegwick " ,
label : " dev-tegwick " ,
field : " deploymentEnvironment " ,
value : " dev-tegwick " ,
rank : 0 ,
membership : {
field : " deploymentEnvironment " ,
op : " equals " ,
value : " dev-tegwick " ,
normalize : " deploymentEnvironment " ,
} ,
presentation : { height : 10 , color : " #0f766e " , fill : " rgba(15, 118, 110, .07) " } ,
} ,
{
id : " deploymentEnvironment:test " ,
label : " test " ,
field : " deploymentEnvironment " ,
value : " test " ,
rank : 1 ,
membership : {
field : " deploymentEnvironment " ,
op : " equals " ,
value : " test " ,
normalize : " deploymentEnvironment " ,
} ,
presentation : { height : 20 , color : " #2563eb " , fill : " rgba(37, 99, 235, .07) " } ,
} ,
{
id : " deploymentEnvironment:prod " ,
label : " prod " ,
field : " deploymentEnvironment " ,
value : " prod " ,
rank : 2 ,
membership : {
field : " deploymentEnvironment " ,
op : " equals " ,
value : " prod " ,
normalize : " deploymentEnvironment " ,
} ,
presentation : { height : 30 , color : " #be123c " , fill : " rgba(190, 18, 60, .07) " } ,
} ,
] ,
} ;
2026-05-25 00:27:10 +02:00
const zoneDefinitionSets = {
" fabric-default " : {
id : " fabric-default " ,
label : " Fabric defaults " ,
groupings : [ " deploymentEnvironment " , " accessZone " ] ,
} ,
} ;
2026-05-25 03:17:23 +02:00
const zoneLayoutAlgorithms = new Set ( [ " compact-grid " , " circle " ] ) ;
const normalizeZoneLayoutAlgorithm = ( value ) = >
zoneLayoutAlgorithms . has ( String ( value | | " " ) ) ? String ( value ) : " compact-grid " ;
2026-05-25 00:00:28 +02:00
const zoneElementData = ( element ) = > {
if ( ! element ) return { } ;
if ( typeof element . data == = " function " ) return element . data ( ) ;
return element . data & & typeof element . data == = " object " ? element . data : element ;
} ;
const zoneFieldValue = ( data , field ) = > {
if ( ! field ) return " " ;
let current = data | | { } ;
String ( field ) . split ( " . " ) . forEach ( ( part ) = > {
current = current & & typeof current == = " object " ? current [ part ] : undefined ;
} ) ;
return current ;
} ;
const zoneRuleValue = ( data , rule ) = > {
const value = zoneFieldValue ( data , rule . field ) ;
if ( rule . normalize == = " deploymentEnvironment " ) return normalizeDeploymentZoneValue ( value ) ;
return hasValue ( value ) ? String ( value ) . trim ( ) : " " ;
} ;
const zoneRuleMatches = ( data , rule , emptyMatches = false ) = > {
if ( ! rule | | ! Object . keys ( rule ) . length ) return emptyMatches ;
if ( Array . isArray ( rule . all ) ) {
return rule . all . every ( ( child ) = > zoneRuleMatches ( data , child ) ) ;
}
if ( Array . isArray ( rule . any ) ) {
return rule . any . some ( ( child ) = > zoneRuleMatches ( data , child ) ) ;
}
if ( Array . isArray ( rule . rules ) ) {
return rule . rules . every ( ( child ) = > zoneRuleMatches ( data , child ) ) ;
}
const actual = zoneRuleValue ( data , rule ) ;
if ( rule . op == = " exists " ) return hasValue ( actual ) ;
if ( rule . op == = " in " ) {
const expected = Array . isArray ( rule . value ) ? rule . value : [ rule . value ] ;
return expected . map ( ( value ) = > String ( value ) ) . includes ( String ( actual ) ) ;
}
return String ( actual ) == = String ( rule . value ? ? " " ) ;
} ;
2026-05-25 02:08:45 +02:00
const isTrueish = ( value ) = > value == = true | | String ( value ) . toLowerCase ( ) == = " true " ;
const isZoneContextOnlyEdge = ( element ) = > {
const data = zoneElementData ( element ) ;
const edgeType = String ( data . edgeType | | data . dependencyType | | " " ) . trim ( ) ;
return isTrueish ( data . displayOnly ) | | edgeType == = " declares " ;
} ;
const isZoneConnectivityEdge = ( element ) = >
element & & element . isEdge & & element . isEdge ( ) & & ! isZoneContextOnlyEdge ( element ) ;
2026-05-25 00:00:28 +02:00
const zoneDescriptorFromDefinition = ( definition ) = > ( {
field : definition . field ,
id : definition . id ,
label : definition . label | | definition . id ,
value : definition . value | | definition . label | | definition . id ,
rank : definition . rank ,
2026-05-25 02:08:45 +02:00
layout : definition . layout | | { } ,
2026-05-25 00:00:28 +02:00
presentation : definition . presentation | | { } ,
membership : definition . membership | | { } ,
diagnostics : [ ] ,
} ) ;
const accessZoneDefinition = ( value ) = > {
const label = String ( value | | " " ) . trim ( ) ;
if ( ! label ) return null ;
return {
id : ` accessZone : $ { label } ` ,
label ,
field : " accessZone " ,
value : label ,
rank : 10 + hashString ( label ) % 20 ,
membership : { field : " accessZone " , op : " equals " , value : label } ,
presentation : { } ,
} ;
} ;
2026-05-25 00:09:18 +02:00
const zoneDiagnosticCodes = {
emptySeedSet : " ZONE_EMPTY_SEED_SET " ,
nodeSeededByMultipleZones : " ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES " ,
nodeAttractedByMultipleZones : " ZONE_NODE_ATTRACTED_BY_MULTIPLE_ZONES " ,
attractionDepthLimitReached : " ZONE_ATTRACTION_DEPTH_LIMIT_REACHED " ,
edgeCrossesZoneBoundary : " ZONE_EDGE_CROSSES_ZONE_BOUNDARY " ,
} ;
2026-05-25 00:00:28 +02:00
const zoneDefinitionsForData = ( data , grouping = activeZoneGrouping ) = > {
2026-05-25 00:27:10 +02:00
if ( ! zoneDefinitionSets [ activeZoneDefinitionSet ] ) activeZoneDefinitionSet = " fabric-default " ;
2026-05-25 00:00:28 +02:00
if ( grouping == = " deploymentEnvironment " ) return defaultZoneDefinitions . deploymentEnvironment ;
if ( grouping == = " accessZone " ) {
const definition = accessZoneDefinition ( zoneFieldValue ( data , " accessZone " ) ) ;
return definition ? [ definition ] : [ ] ;
}
return [ ] ;
} ;
const zoneDefinitionsForElements = ( elements , grouping = activeZoneGrouping ) = > {
2026-05-25 00:27:10 +02:00
if ( ! zoneDefinitionSets [ activeZoneDefinitionSet ] ) activeZoneDefinitionSet = " fabric-default " ;
2026-05-25 00:00:28 +02:00
if ( grouping == = " deploymentEnvironment " ) return defaultZoneDefinitions . deploymentEnvironment ;
if ( grouping == = " accessZone " ) {
const values = new Set ( ) ;
elements . forEach ( ( element ) = > {
const value = String ( zoneFieldValue ( zoneElementData ( element ) , " accessZone " ) | | " " ) . trim ( ) ;
if ( value ) values . add ( value ) ;
} ) ;
return Array . from ( values )
. sort ( ( left , right ) = > left . localeCompare ( right ) )
. map ( accessZoneDefinition )
. filter ( Boolean ) ;
}
return [ ] ;
} ;
const chooseZoneCandidate = ( candidates ) = > candidates
. slice ( )
. sort ( ( left , right ) = > {
const leftHeight = Number ( left . definition . presentation ? . height ) | | 0 ;
const rightHeight = Number ( right . definition . presentation ? . height ) | | 0 ;
return rightHeight - leftHeight | | left . index - right . index | | left . definition . id . localeCompare ( right . definition . id ) ;
} ) [ 0 ] ;
const resolveZoneInstances = ( elements , definitions ) = > {
const zones = new Map ( ) ;
definitions . forEach ( ( definition , index ) = > {
zones . set ( definition . id , {
. . . zoneDescriptorFromDefinition ( definition ) ,
definition ,
definitionIndex : index ,
nodes : [ ] ,
elements : [ ] ,
nodeIds : new Set ( ) ,
elementIds : new Set ( ) ,
diagnostics : [ ] ,
} ) ;
} ) ;
const assignments = new Map ( ) ;
const addElementToZone = ( zoneId , element ) = > {
const zone = zones . get ( zoneId ) ;
if ( ! zone ) return ;
const elementId = element . id ? element . id ( ) : zoneElementData ( element ) . id ;
if ( elementId & & zone . elementIds . has ( elementId ) ) return ;
if ( elementId ) zone . elementIds . add ( elementId ) ;
zone . elements . push ( element ) ;
} ;
elements . filter ( ( element ) = > element . isNode & & element . isNode ( ) ) . forEach ( ( node ) = > {
const data = zoneElementData ( node ) ;
const candidates = definitions
. map ( ( definition , index ) = > ( { definition , index } ) )
. filter ( ( candidate ) = > zoneRuleMatches ( data , candidate . definition . membership ) ) ;
if ( ! candidates . length ) return ;
if ( candidates . length > 1 ) {
const diagnostic = {
severity : " warning " ,
2026-05-25 00:09:18 +02:00
code : zoneDiagnosticCodes . nodeSeededByMultipleZones ,
2026-05-25 00:00:28 +02:00
message : ` $ { elementLabel ( node ) } matched multiple zone definitions . ` ,
} ;
candidates . forEach ( ( candidate ) = > zones . get ( candidate . definition . id ) ? . diagnostics . push ( diagnostic ) ) ;
}
const chosen = chooseZoneCandidate ( candidates ) . definition ;
const zone = zones . get ( chosen . id ) ;
if ( ! zone ) return ;
assignments . set ( node . id ( ) , chosen . id ) ;
zone . nodeIds . add ( node . id ( ) ) ;
zone . nodes . push ( node ) ;
addElementToZone ( chosen . id , node ) ;
} ) ;
2026-05-25 00:09:18 +02:00
zones . forEach ( ( zone ) = > {
if ( ! zone . nodes . length ) {
zone . diagnostics . push ( {
severity : " warning " ,
code : zoneDiagnosticCodes . emptySeedSet ,
message : ` $ { zone . label } has no visible seed nodes . ` ,
} ) ;
}
} ) ;
2026-05-25 00:00:28 +02:00
elements . filter ( ( element ) = > element . isEdge & & element . isEdge ( ) ) . forEach ( ( edge ) = > {
2026-05-25 02:08:45 +02:00
const isConnectivity = isZoneConnectivityEdge ( edge ) ;
2026-05-25 00:00:28 +02:00
const data = zoneElementData ( edge ) ;
const zoneIds = new Set (
2026-05-25 02:08:45 +02:00
isConnectivity
? definitions
. filter ( ( definition ) = > zoneRuleMatches ( data , definition . membership ) )
. map ( ( definition ) = > definition . id )
: [ ]
2026-05-25 00:00:28 +02:00
) ;
const sourceZoneId = assignments . get ( edge . data ( " source " ) ) ;
const targetZoneId = assignments . get ( edge . data ( " target " ) ) ;
2026-05-25 02:08:45 +02:00
if ( isConnectivity & & ( sourceZoneId | | targetZoneId ) & & sourceZoneId != = targetZoneId ) {
2026-05-25 00:09:18 +02:00
const diagnostic = {
severity : " info " ,
code : zoneDiagnosticCodes . edgeCrossesZoneBoundary ,
message : ` $ { elementLabel ( edge ) } crosses a zone boundary . ` ,
} ;
[ sourceZoneId , targetZoneId ]
. filter ( Boolean )
. forEach ( ( zoneId ) = > zones . get ( zoneId ) ? . diagnostics . push ( diagnostic ) ) ;
}
2026-05-25 02:08:45 +02:00
if ( isConnectivity & & sourceZoneId ) zoneIds . add ( sourceZoneId ) ;
if ( isConnectivity & & targetZoneId ) zoneIds . add ( targetZoneId ) ;
2026-05-25 00:00:28 +02:00
zoneIds . forEach ( ( zoneId ) = > addElementToZone ( zoneId , edge ) ) ;
} ) ;
return zones ;
} ;
2026-05-24 17:02:12 +02:00
const zoneStyle = ( zone ) = >
2026-05-25 00:00:28 +02:00
zone . presentation ? . color
? { color : zone . presentation . color , fill : zone . presentation . fill | | zonePalette [ zone . id ] ? . fill | | " rgba(37, 99, 235, .07) " }
: zonePalette [ zone . id ] | | fallbackZonePalette [ hashString ( zone . id ) % fallbackZonePalette . length ] ;
2026-05-24 17:02:12 +02:00
const zoneRank = ( zone ) = > {
2026-05-25 00:00:28 +02:00
if ( Number . isFinite ( zone . rank ) ) return zone . rank ;
2026-05-24 17:02:12 +02:00
if ( zone . id == = " deploymentEnvironment:dev-tegwick " ) return 0 ;
if ( zone . id == = " deploymentEnvironment:test " ) return 1 ;
if ( zone . id == = " deploymentEnvironment:prod " ) return 2 ;
return 10 + hashString ( zone . id ) % 20 ;
} ;
const zoneLabelRank = ( zone , index ) = > {
if ( zone . id == = " deploymentEnvironment:test " ) return 0 ;
if ( zone . id == = " deploymentEnvironment:prod " ) return 1 ;
if ( zone . id == = " deploymentEnvironment:dev-tegwick " ) return 2 ;
return index % 4 ;
} ;
const zoneLabelTop = ( zone , bounds , index ) = > {
2026-05-25 01:09:05 +02:00
return 8 ;
2026-05-24 17:02:12 +02:00
} ;
const zoneForData = ( data , grouping = activeZoneGrouping ) = > {
2026-05-25 00:00:28 +02:00
const definition = zoneDefinitionsForData ( data , grouping )
. find ( ( candidate ) = > zoneRuleMatches ( data , candidate . membership ) ) ;
return definition ? zoneDescriptorFromDefinition ( definition ) : null ;
2026-05-24 17:02:12 +02:00
} ;
const renderedNodeBox = ( node ) = > {
try {
const box = node . renderedBoundingBox ( { includeLabels : true , includeOverlays : false } ) ;
if ( [ box . x1 , box . y1 , box . x2 , box . y2 ] . every ( Number . isFinite ) ) return box ;
} catch {
/ / Fall back to the rendered center point below .
}
const position = node . renderedPosition ( ) ;
const radius = ( Number ( node . data ( " visualSize " ) ) | | 44 ) / 2 ;
return {
x1 : position . x - radius ,
y1 : position . y - radius ,
x2 : position . x + radius ,
y2 : position . y + radius ,
} ;
} ;
const zoneBoundsForNodes = ( nodes ) = > {
if ( ! nodes . length ) return null ;
const bounds = nodes . reduce ( ( box , node ) = > {
const nodeBox = renderedNodeBox ( node ) ;
return {
x1 : Math . min ( box . x1 , nodeBox . x1 ) ,
y1 : Math . min ( box . y1 , nodeBox . y1 ) ,
x2 : Math . max ( box . x2 , nodeBox . x2 ) ,
y2 : Math . max ( box . y2 , nodeBox . y2 ) ,
} ;
} , { x1 : Infinity , y1 : Infinity , x2 : - Infinity , y2 : - Infinity } ) ;
if ( ! [ bounds . x1 , bounds . y1 , bounds . x2 , bounds . y2 ] . every ( Number . isFinite ) ) return null ;
const padding = 34 ;
let left = bounds . x1 - padding ;
let top = bounds . y1 - padding ;
let width = bounds . x2 - bounds . x1 + padding * 2 ;
let height = bounds . y2 - bounds . y1 + padding * 2 ;
const minWidth = 132 ;
const minHeight = 84 ;
if ( width < minWidth ) {
left - = ( minWidth - width ) / 2 ;
width = minWidth ;
}
if ( height < minHeight ) {
top - = ( minHeight - height ) / 2 ;
height = minHeight ;
}
return { left , top , width , height } ;
} ;
2026-05-25 02:08:45 +02:00
const zoneNodeSortKey = ( node ) = >
String ( node . data ( " stableKey " ) | | node . data ( " label " ) | | node . id ( ) ) ;
const zoneCenterForNodes = ( nodes ) = > {
const positions = nodes
. map ( ( node ) = > node . position ( ) )
. filter ( ( position ) = > Number . isFinite ( position . x ) & & Number . isFinite ( position . y ) ) ;
if ( ! positions . length ) return null ;
return {
x : positions . reduce ( ( total , position ) = > total + position . x , 0 ) / positions . length ,
y : positions . reduce ( ( total , position ) = > total + position . y , 0 ) / positions . length ,
} ;
} ;
const zonePreferredSize = ( zone ) = > {
const count = Math . max ( 1 , ( zone . nodes | | [ ] ) . length ) ;
const columns = Math . max ( 1 , Math . ceil ( Math . sqrt ( count * 1.35 ) ) ) ;
const rows = Math . max ( 1 , Math . ceil ( count / columns ) ) ;
const cellWidth = Number ( zone . layout ? . options ? . cellWidth ) | | 118 ;
const cellHeight = Number ( zone . layout ? . options ? . cellHeight ) | | 94 ;
const padding = Number ( zone . layout ? . options ? . padding ) | | 60 ;
return {
width : Math . max ( 180 , columns * cellWidth + padding * 2 ) ,
height : Math . max ( 128 , rows * cellHeight + padding * 2 ) ,
padding ,
} ;
} ;
const ensureZoneContainer = ( zone ) = > {
if ( ! zone | | ! ( zone . nodes | | [ ] ) . length ) return null ;
const preferred = zonePreferredSize ( zone ) ;
const existing = zoneContainerState . get ( zone . id ) ;
if ( existing ) {
existing . width = preferred . width ;
existing . height = preferred . height ;
existing . padding = preferred . padding ;
return existing ;
}
const center = zoneCenterForNodes ( zone . nodes ) | | { x : 0 , y : 0 } ;
const container = {
id : zone . id ,
x : center . x ,
y : center . y ,
width : preferred . width ,
height : preferred . height ,
padding : preferred . padding ,
userPlaced : false ,
} ;
zoneContainerState . set ( zone . id , container ) ;
return container ;
} ;
const packZoneContainers = ( zones ) = > {
const items = Array . from ( zones . values ( ) )
. filter ( ( zone ) = > ( zone . nodes | | [ ] ) . length > 0 )
. map ( ( zone ) = > ( { zone , container : ensureZoneContainer ( zone ) } ) )
. filter ( ( item ) = > item . container ) ;
if ( items . length < 2 | | items . some ( ( item ) = > item . container . userPlaced ) ) return ;
items . sort ( ( left , right ) = >
zoneRank ( left . zone ) - zoneRank ( right . zone ) | | left . zone . label . localeCompare ( right . zone . label )
) ;
const gap = 42 ;
const start = Math . min ( . . . items . map ( ( item ) = > item . container . x - item . container . width / 2 ) ) ;
const maxRowWidth = cy
? Math . max ( 720 , ( cy . width ( ) | | 960 ) / Math . max ( cy . zoom ( ) | | 1 , 0.2 ) - 96 )
: Infinity ;
let cursor = start ;
let top = Math . min ( . . . items . map ( ( item ) = > item . container . y - item . container . height / 2 ) ) ;
let rowHeight = 0 ;
items . forEach ( ( { container } ) = > {
if ( cursor > start & & cursor + container . width > start + maxRowWidth ) {
cursor = start ;
top + = rowHeight + gap ;
rowHeight = 0 ;
}
container . x = cursor + container . width / 2 ;
container . y = top + container . height / 2 ;
cursor + = container . width + gap ;
rowHeight = Math . max ( rowHeight , container . height ) ;
} ) ;
} ;
const syncZoneContainers = ( zones , options = { } ) = > {
let created = false ;
zones . forEach ( ( zone ) = > {
if ( ( zone . nodes | | [ ] ) . length > 0 & & ! zoneContainerState . has ( zone . id ) ) created = true ;
ensureZoneContainer ( zone ) ;
} ) ;
if ( options . packNew & & created ) packZoneContainers ( zones ) ;
} ;
const zoneContainerBounds = ( container ) = > ( {
left : container . x - container . width / 2 ,
top : container . y - container . height / 2 ,
width : container . width ,
height : container . height ,
} ) ;
const zoneRenderedBoundsFromContainer = ( container ) = > {
const bounds = zoneContainerBounds ( container ) ;
const zoom = cy ? cy . zoom ( ) | | 1 : 1 ;
const pan = cy ? cy . pan ( ) | | { x : 0 , y : 0 } : { x : 0 , y : 0 } ;
return {
left : bounds . left * zoom + pan . x ,
top : bounds . top * zoom + pan . y ,
width : bounds . width * zoom ,
height : bounds . height * zoom ,
} ;
} ;
const layoutZoneNodesInGrid = ( nodes , container ) = > {
const padding = Number ( container . padding ) | | 60 ;
const count = Math . max ( 1 , nodes . length ) ;
const columns = Math . max ( 1 , Math . ceil ( Math . sqrt ( count * 1.35 ) ) ) ;
const rows = Math . max ( 1 , Math . ceil ( count / columns ) ) ;
const innerWidth = Math . max ( 1 , container . width - padding * 2 ) ;
const innerHeight = Math . max ( 1 , container . height - padding * 2 ) ;
const cellWidth = innerWidth / columns ;
const cellHeight = innerHeight / rows ;
const left = container . x - container . width / 2 + padding ;
const top = container . y - container . height / 2 + padding ;
nodes . forEach ( ( node , index ) = > {
const column = index % columns ;
const row = Math . floor ( index / columns ) ;
node . position ( {
x : left + cellWidth * ( column + 0.5 ) ,
y : top + cellHeight * ( row + 0.5 ) ,
} ) ;
} ) ;
} ;
const layoutZoneNodesInCircle = ( nodes , container ) = > {
if ( nodes . length == = 1 ) {
nodes [ 0 ] . position ( { x : container . x , y : container . y } ) ;
return ;
}
const padding = Number ( container . padding ) | | 60 ;
const radius = Math . max ( 20 , Math . min ( container . width , container . height ) / 2 - padding ) ;
nodes . forEach ( ( node , index ) = > {
const angle = - Math . PI / 2 + ( Math . PI * 2 * index ) / nodes . length ;
node . position ( {
x : container . x + Math . cos ( angle ) * radius ,
y : container . y + Math . sin ( angle ) * radius ,
} ) ;
} ) ;
} ;
const layoutZoneSubgraph = ( zone , container ) = > {
const nodes = ( zone . nodes | | [ ] )
. slice ( )
. sort ( ( left , right ) = > zoneNodeSortKey ( left ) . localeCompare ( zoneNodeSortKey ( right ) ) ) ;
if ( ! nodes . length | | ! container ) return ;
2026-05-25 03:17:23 +02:00
const algorithm = normalizeZoneLayoutAlgorithm ( activeZoneLayoutAlgorithm | | zone . layout ? . algorithm ) ;
2026-05-25 02:08:45 +02:00
if ( algorithm == = " circle " ) {
layoutZoneNodesInCircle ( nodes , container ) ;
return ;
}
layoutZoneNodesInGrid ( nodes , container ) ;
} ;
const applyZoneContainerLayout = ( ) = > {
if ( ! cy | | zoneDragState ) return ;
const zones = collectZoneSummaries ( ) ;
syncZoneContainers ( zones , { packNew : true } ) ;
zones . forEach ( ( zone ) = > {
if ( ! ( zone . nodes | | [ ] ) . length ) return ;
layoutZoneSubgraph ( zone , ensureZoneContainer ( zone ) ) ;
} ) ;
zoneSummaries = zones ;
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ;
const fitVisibleGraph = ( ) = > {
if ( ! cy ) return ;
const visible = cy . elements ( ) . filter ( ( element ) = > element . style ( " display " ) != = " none " ) ;
if ( visible . length ) cy . fit ( visible , 72 ) ;
} ;
const serializedZoneContainers = ( ) = > {
const entries = Array . from ( zoneContainerState . entries ( ) )
. sort ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) )
. map ( ( [ id , container ] ) = > [ id , {
x : Math . round ( container . x * 100 ) / 100 ,
y : Math . round ( container . y * 100 ) / 100 ,
width : Math . round ( container . width * 100 ) / 100 ,
height : Math . round ( container . height * 100 ) / 100 ,
userPlaced : container . userPlaced == = true ,
} ] ) ;
return Object . fromEntries ( entries ) ;
} ;
const normalizedZoneContainers = ( containers ) = > {
const normalized = new Map ( ) ;
if ( ! containers | | typeof containers != = " object " ) return normalized ;
Object . entries ( containers ) . forEach ( ( [ id , value ] ) = > {
if ( ! value | | typeof value != = " object " ) return ;
const x = Number ( value . x ) ;
const y = Number ( value . y ) ;
const width = Number ( value . width ) ;
const height = Number ( value . height ) ;
if ( ! [ x , y , width , height ] . every ( Number . isFinite ) ) return ;
normalized . set ( id , {
id ,
x ,
y ,
width : Math . max ( 1 , width ) ,
height : Math . max ( 1 , height ) ,
padding : Number ( value . padding ) | | 60 ,
userPlaced : value . userPlaced == = true ,
} ) ;
} ) ;
return normalized ;
} ;
2026-05-24 17:02:12 +02:00
const collectZoneSummaries = ( ) = > {
2026-05-25 00:00:28 +02:00
if ( ! cy ) return new Map ( ) ;
const visibleElements = cy . elements ( )
. filter ( ( element ) = > element . style ( " display " ) != = " none " )
. toArray ( ) ;
return resolveZoneInstances (
visibleElements ,
zoneDefinitionsForElements ( visibleElements , activeZoneGrouping )
) ;
2026-05-24 17:02:12 +02:00
} ;
const zoneCountsForField = ( elements , field ) = > {
if ( field != = " deploymentEnvironment " & & field != = " accessZone " ) return groupCounts ( elements , field ) ;
2026-05-25 00:00:28 +02:00
const zones = resolveZoneInstances ( elements , zoneDefinitionsForElements ( elements , field ) ) ;
return Array . from ( zones . values ( ) )
. filter ( ( zone ) = > zone . elements . length > 0 )
. map ( ( zone ) = > [ zone . label , zone . elements . length ] )
2026-05-24 17:02:12 +02:00
. sort ( ( left , right ) = > right [ 1 ] - left [ 1 ] | | left [ 0 ] . localeCompare ( right [ 0 ] ) ) ;
} ;
const renderZoneDetails = ( zone ) = > {
selected = null ;
selectedZoneId = zone . id ;
const visibleElements = zone . elements | | [ ] ;
const visibleNodes = zone . nodes | | [ ] ;
2026-05-25 00:40:13 +02:00
const isCollapsed = collapsedZoneSnapshots . has ( zone . id ) ;
2026-05-24 17:02:12 +02:00
detailTitle . textContent = zone . label ;
detailSummary . textContent = ` $ { visibleNodes . length } visible node $ { visibleNodes . length == = 1 ? " " : " s " } in $ { ruleAttributeLabels [ zone . field ] | | humanize ( zone . field ) } ` ;
detailPills . innerHTML = [ zone . field , zone . value , activeZoneGrouping ]
. map ( ( value ) = > value ? ` < span class = " pill " > $ { escapeHtml ( value ) } < / span > ` : " " )
. join ( " " ) ;
const valuesFor = ( field ) = > groupCounts ( visibleElements , field )
. map ( ( [ value , count ] ) = > ` $ { value } ( $ { count } ) ` )
. join ( " , " ) ;
const warnings = visibleElements
. flatMap ( ( element ) = > zoneWarningsForData ( element . data ( ) ) . map ( ( warning ) = >
` $ { elementLabel ( element ) } : $ { warning } `
) ) ;
2026-05-25 00:09:18 +02:00
const diagnostics = ( zone . diagnostics | | [ ] )
. map ( ( diagnostic ) = > ` $ { diagnostic . code } : $ { diagnostic . message } ` ) ;
2026-05-24 17:02:12 +02:00
const rows = [
[ " visible nodes " , String ( visibleNodes . length ) ] ,
[ " deployment environments " , valuesFor ( " deploymentEnvironment " ) ] ,
[ " deployment scenarios " , valuesFor ( " deploymentScenario " ) ] ,
[ " access zones " , valuesFor ( " accessZone " ) ] ,
[ " routing authorities " , valuesFor ( " routingAuthority " ) ] ,
[ " policy authorities " , valuesFor ( " policyAuthority " ) ] ,
2026-05-25 00:09:18 +02:00
. . . diagnostics . slice ( 0 , 8 ) . map ( ( diagnostic ) = > [ " diagnostic " , diagnostic ] ) ,
2026-05-24 17:02:12 +02:00
. . . warnings . slice ( 0 , 8 ) . map ( ( warning ) = > [ " warning " , warning ] ) ,
] ;
2026-05-25 00:09:18 +02:00
if ( diagnostics . length > 8 ) rows . push ( [ " diagnostic " , ` $ { diagnostics . length - 8 } additional zone diagnostics ` ] ) ;
2026-05-24 17:02:12 +02:00
if ( warnings . length > 8 ) rows . push ( [ " warning " , ` $ { warnings . length - 8 } additional route warnings ` ] ) ;
detailList . innerHTML = rows
. filter ( ( [ , value ] ) = > value )
2026-05-25 00:09:18 +02:00
. map ( ( [ key , value ] ) = > ` < li class = " $ { key === " warning " || key === " diagnostic " ? " orientation - warning " : " " } " > < strong > $ { escapeHtml ( key ) } < / strong > $ { escapeHtml ( value ) } < / li > ` )
2026-05-24 17:02:12 +02:00
. join ( " " ) ;
const contextIds = new Set ( visibleNodes . map ( ( node ) = > node . id ( ) ) ) ;
visibleElements . filter ( ( element ) = > element . isEdge ( ) ) . forEach ( ( edge ) = > addContextEdge ( contextIds , edge ) ) ;
orientationContext = {
ids : Array . from ( contextIds ) ,
profileName : ` Zone : $ { zone . label } ` ,
} ;
orientationTitle . textContent = ` Zone context ( $ { orientationContext . ids . length } items ) ` ;
orientationList . innerHTML = [
{ label : " grouping " , value : ruleAttributeLabels [ zone . field ] | | humanize ( zone . field ) , state : " good " } ,
{ label : " zone " , value : zone . label } ,
2026-05-25 00:09:18 +02:00
{ label : " diagnostics " , value : diagnostics . length ? String ( diagnostics . length ) : " none " , state : diagnostics . length ? " warning " : " good " } ,
2026-05-24 17:02:12 +02:00
{ label : " warnings " , value : warnings . length ? String ( warnings . length ) : " none " , state : warnings . length ? " warning " : " good " } ,
] . map ( ( row ) = > `
< li class = " orientation-item $ { orientationStateClass(row.state)} " >
< span class = " orientation-label " > $ { escapeHtml ( row . label ) } < / span >
< span class = " orientation-value " > $ { escapeHtml ( row . value ) } < / span >
< / li >
` ) . join ( " " ) ;
orientationActions . innerHTML = `
2026-05-25 00:40:13 +02:00
< button type = " button " data - orientation - action = " $ { isCollapsed ? " expand - zone " : " collapse - zone " } " data - zone - id = " $ { escapeHtml(zone.id)} " > $ { isCollapsed ? " Expand Zone " : " Collapse Zone " } < / button >
2026-05-24 17:02:12 +02:00
< button type = " button " data - orientation - action = " focus " > Focus Context < / button >
< button type = " button " data - orientation - action = " highlight " > Highlight Context < / button >
< button type = " button " data - orientation - action = " hide-other " > Hide Other < / button >
< button type = " button " data - orientation - action = " remove-other " > Remove Other < / button >
< button type = " button " data - orientation - action = " name-view " > Name View < / button >
` ;
updateLabelVisibility ( ) ;
updateSelectionAnchor ( ) ;
} ;
2026-05-25 00:40:13 +02:00
const zoneCollapseNodeId = ( zoneId ) = > ` $ { zoneCollapsePrefix } node : $ { zoneId } ` ;
const isZoneCollapseElement = ( element ) = > String ( element . id ( ) ) . startsWith ( zoneCollapsePrefix ) ;
const removeZoneCollapseElements = ( ) = > {
if ( ! cy ) return ;
if ( selected & & isZoneCollapseElement ( selected ) ) selected = null ;
cy . elements ( ) . filter ( isZoneCollapseElement ) . remove ( ) ;
} ;
const zoneCollapsePosition = ( nodes ) = > {
const positions = nodes . map ( ( node ) = > node . position ( ) ) ;
if ( ! positions . length ) return { x : 0 , y : 0 } ;
return {
x : positions . reduce ( ( total , position ) = > total + position . x , 0 ) / positions . length ,
y : positions . reduce ( ( total , position ) = > total + position . y , 0 ) / positions . length ,
} ;
} ;
const zoneCollapseSnapshot = ( zone ) = > {
const nodeIds = new Set ( ( zone . nodes | | [ ] ) . map ( ( node ) = > node . id ( ) ) ) ;
const boundaryEdges = [ ] ;
let internalEdgeCount = 0 ;
( zone . elements | | [ ] )
2026-05-25 02:08:45 +02:00
. filter ( isZoneConnectivityEdge )
2026-05-25 00:40:13 +02:00
. forEach ( ( edge ) = > {
const source = edge . data ( " source " ) ;
const target = edge . data ( " target " ) ;
const sourceInside = nodeIds . has ( source ) ;
const targetInside = nodeIds . has ( target ) ;
if ( sourceInside & & targetInside ) {
internalEdgeCount + = 1 ;
} else if ( sourceInside | | targetInside ) {
boundaryEdges . push ( {
id : edge . id ( ) ,
source ,
target ,
edgeType : edge . data ( " edgeType " ) | | " zone_boundary " ,
sourceInside ,
targetInside ,
} ) ;
}
} ) ;
return {
id : zone . id ,
label : zone . label ,
field : zone . field ,
value : zone . value ,
nodeIds : Array . from ( nodeIds ) ,
boundaryEdges ,
internalEdgeCount ,
position : zoneCollapsePosition ( zone . nodes | | [ ] ) ,
} ;
} ;
const addZoneCollapseElements = ( ) = > {
if ( ! cy | | ! collapsedZoneSnapshots . size ) return ;
collapsedZoneSnapshots . forEach ( ( snapshot ) = > {
const zoneNodeId = zoneCollapseNodeId ( snapshot . id ) ;
if ( ! cy . getElementById ( zoneNodeId ) . length ) {
cy . add ( {
group : " nodes " ,
data : {
id : zoneNodeId ,
stableKey : zoneNodeId ,
kind : " Zone " ,
layer : " zone " ,
label : snapshot . label ,
name : ` $ { snapshot . label } zone ` ,
description : ` Collapsed zone with $ { snapshot . nodeIds . length } hidden node $ { snapshot . nodeIds . length == = 1 ? " " : " s " } . ` ,
displayState : " show " ,
visualSize : 72 ,
zoneCollapse : true ,
collapsedZoneId : snapshot . id ,
collapsedZoneLabel : snapshot . label ,
containedNodeCount : snapshot . nodeIds . length ,
containedInternalEdgeCount : snapshot . internalEdgeCount ,
boundaryEdgeCount : snapshot . boundaryEdges . length ,
} ,
classes : " zone-collapse-node " ,
position : snapshot . position ,
} ) ;
}
snapshot . boundaryEdges . forEach ( ( edge , index ) = > {
const source = edge . sourceInside ? zoneNodeId : edge . source ;
const target = edge . targetInside ? zoneNodeId : edge . target ;
if ( source == = target | | ! cy . getElementById ( source ) . length | | ! cy . getElementById ( target ) . length ) return ;
const edgeId = ` $ { zoneCollapsePrefix } edge : $ { snapshot . id } : $ { index } : $ { edge . id } ` ;
if ( cy . getElementById ( edgeId ) . length ) return ;
cy . add ( {
group : " edges " ,
data : {
id : edgeId ,
stableKey : edgeId ,
kind : " edge " ,
layer : " zone " ,
label : edge . edgeType ,
source ,
target ,
edgeType : edge . edgeType ,
strength : " medium " ,
edgeWidth : 3 ,
displayState : " show " ,
zoneCollapse : true ,
collapsedZoneId : snapshot . id ,
originalEdgeId : edge . id ,
} ,
classes : " zone-collapse-edge " ,
} ) ;
} ) ;
} ) ;
} ;
const applyZoneCollapseVisibility = ( hiddenNodes ) = > {
if ( ! cy | | ! collapsedZoneSnapshots . size ) return ;
collapsedZoneSnapshots . forEach ( ( snapshot ) = > {
snapshot . nodeIds . forEach ( ( nodeId ) = > {
const node = cy . getElementById ( nodeId ) ;
if ( ! node . length ) return ;
node . data ( " displayState " , " hide " ) ;
node . style ( " display " , " none " ) ;
hiddenNodes . add ( nodeId ) ;
} ) ;
} ) ;
} ;
const collapseZone = ( zone ) = > {
if ( ! cy | | ! zone | | ! ( zone . nodes | | [ ] ) . length ) return ;
collapsedZoneSnapshots . set ( zone . id , zoneCollapseSnapshot ( zone ) ) ;
selected = null ;
selectedZoneId = " " ;
applyFilters ( { redrawOnRemove : true } ) ;
runLayout ( ) ;
updateProfileSummary ( ` Collapsed zone " $ {zone.label} " . ` ) ;
updateUrlState ( ) ;
} ;
const expandZone = ( zoneId ) = > {
if ( ! cy | | ! zoneId ) return ;
const snapshot = collapsedZoneSnapshots . get ( zoneId ) ;
collapsedZoneSnapshots . delete ( zoneId ) ;
selected = null ;
selectedZoneId = " " ;
applyFilters ( { redrawOnRemove : true } ) ;
runLayout ( ) ;
updateProfileSummary ( snapshot ? ` Expanded zone " $ {snapshot.label} " . ` : " " ) ;
updateUrlState ( ) ;
} ;
2026-05-24 17:02:12 +02:00
const renderZoneOverlay = ( ) = > {
if ( ! zoneOverlay | | ! cy ) return ;
const enabled = zoneBoundaryToggle ? zoneBoundaryToggle . checked : true ;
if ( ! enabled ) {
zoneSummaries = new Map ( ) ;
selectedZoneId = " " ;
zoneOverlay . innerHTML = " " ;
return ;
}
zoneSummaries = collectZoneSummaries ( ) ;
2026-05-25 02:08:45 +02:00
syncZoneContainers ( zoneSummaries ) ;
2026-05-24 17:02:12 +02:00
const boundaries = Array . from ( zoneSummaries . values ( ) )
. filter ( ( zone ) = > zone . nodes . length > 0 )
. sort ( ( left , right ) = > zoneRank ( left ) - zoneRank ( right ) | | left . label . localeCompare ( right . label ) )
. map ( ( zone , index ) = > {
2026-05-25 02:08:45 +02:00
const container = ensureZoneContainer ( zone ) ;
const bounds = container ? zoneRenderedBoundsFromContainer ( container ) : zoneBoundsForNodes ( zone . nodes ) ;
2026-05-24 17:02:12 +02:00
if ( ! bounds ) return " " ;
const style = zoneStyle ( zone ) ;
const selectedClass = selectedZoneId == = zone . id ? " selected " : " " ;
const labelTop = zoneLabelTop ( zone , bounds , index ) ;
return `
< div class = " zone-boundary$ {selectedClass} " data - zone - id = " $ { escapeHtml(zone.id)} " style = " --zone-color:$ {style.color} ;--zone-fill:$ {style.fill} ;left:$ { Math.round(bounds.left)}px;top:$ { Math.round(bounds.top)}px;width:$ { Math.round(bounds.width)}px;height:$ { Math.round(bounds.height)}px " >
< button type = " button " class = " zone-label " data - zone - id = " $ { escapeHtml(zone.id)} " style = " --zone-color:$ {style.color} ;top:$ {labelTop} px " title = " $ { escapeHtml(zone.label)} " > $ { escapeHtml ( zone . label ) } < / button >
< / div >
` ;
} ) ;
zoneOverlay . innerHTML = boundaries . join ( " " ) ;
if ( selectedZoneId & & zoneSummaries . has ( selectedZoneId ) ) {
renderZoneDetails ( zoneSummaries . get ( selectedZoneId ) ) ;
} else if ( selectedZoneId ) {
selectedZoneId = " " ;
}
} ;
2026-05-25 01:09:05 +02:00
const startZoneDrag = ( event , zoneId ) = > {
const zone = zoneSummaries . get ( zoneId ) ;
if ( ! cy | | ! zone | | ! ( zone . nodes | | [ ] ) . length ) return ;
2026-05-25 02:08:45 +02:00
const container = ensureZoneContainer ( zone ) ;
if ( ! container ) return ;
2026-05-25 01:09:05 +02:00
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
selected = null ;
selectedZoneId = zone . id ;
const handle = event . target . closest ( " .zone-label " ) ;
handle ? . classList . add ( " dragging " ) ;
handle ? . setPointerCapture ? . ( event . pointerId ) ;
zoneDragState = {
zoneId : zone . id ,
pointerId : event . pointerId ,
startX : event . clientX ,
startY : event . clientY ,
moved : false ,
handle ,
2026-05-25 02:08:45 +02:00
container : { . . . container } ,
2026-05-25 01:09:05 +02:00
nodes : zone . nodes . map ( ( node ) = > ( {
node ,
position : { . . . node . position ( ) } ,
} ) ) ,
} ;
renderZoneDetails ( zone ) ;
} ;
const moveZoneDrag = ( event ) = > {
if ( ! zoneDragState | | event . pointerId != = zoneDragState . pointerId | | ! cy ) return ;
event . preventDefault ( ) ;
const zoom = cy . zoom ( ) | | 1 ;
const dx = ( event . clientX - zoneDragState . startX ) / zoom ;
const dy = ( event . clientY - zoneDragState . startY ) / zoom ;
if ( Math . abs ( event . clientX - zoneDragState . startX ) + Math . abs ( event . clientY - zoneDragState . startY ) > 3 ) {
zoneDragState . moved = true ;
suppressZoneClick = true ;
}
zoneDragState . nodes . forEach ( ( { node , position } ) = > {
if ( ! node | | ! node . length ) return ;
node . position ( { x : position . x + dx , y : position . y + dy } ) ;
} ) ;
2026-05-25 02:08:45 +02:00
const container = zoneContainerState . get ( zoneDragState . zoneId ) ;
if ( container & & zoneDragState . container ) {
container . x = zoneDragState . container . x + dx ;
container . y = zoneDragState . container . y + dy ;
container . userPlaced = true ;
}
2026-05-25 01:09:05 +02:00
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ;
const finishZoneDrag = ( event ) = > {
if ( ! zoneDragState | | event . pointerId != = zoneDragState . pointerId ) return ;
zoneDragState . handle ? . classList . remove ( " dragging " ) ;
zoneDragState . handle ? . releasePointerCapture ? . ( event . pointerId ) ;
const draggedZoneId = zoneDragState . zoneId ;
const moved = zoneDragState . moved ;
zoneDragState = null ;
renderZoneOverlay ( ) ;
if ( moved ) {
const zone = zoneSummaries . get ( draggedZoneId ) ;
if ( zone ) renderZoneDetails ( zone ) ;
updateProfileSummary ( ` Moved zone " $ { zone?.label || draggedZoneId} " . ` ) ;
2026-05-25 02:08:45 +02:00
updateProfileControls ( ) ;
updateUrlState ( ) ;
2026-05-25 01:09:05 +02:00
window . setTimeout ( ( ) = > { suppressZoneClick = false ; } , 0 ) ;
}
} ;
2026-05-24 17:02:12 +02:00
const scheduleZoneOverlayUpdate = ( ) = > {
if ( ! zoneOverlay | | zoneOverlayFrame ) return ;
zoneOverlayFrame = window . requestAnimationFrame ( ( ) = > {
zoneOverlayFrame = 0 ;
renderZoneOverlay ( ) ;
} ) ;
} ;
2026-05-24 15:55:05 +02:00
const groupCounts = ( elements , field ) = > {
const counts = new Map ( ) ;
elements . forEach ( ( element ) = > {
const value = element . data ( field ) ;
if ( hasValue ( value ) ) counts . set ( String ( value ) , ( counts . get ( String ( value ) ) | | 0 ) + 1 ) ;
} ) ;
return Array . from ( counts . entries ( ) )
. sort ( ( left , right ) = > right [ 1 ] - left [ 1 ] | | left [ 0 ] . localeCompare ( right [ 0 ] ) ) ;
} ;
const renderMapOverview = ( ) = > {
const visibleElements = cy
? cy . elements ( ) . filter ( ( element ) = > element . style ( " display " ) != = " none " ) . toArray ( )
: [ ] ;
const visibleNodes = visibleElements . filter ( ( element ) = > element . isNode ( ) ) . length ;
const visibleEdges = visibleElements . filter ( ( element ) = > element . isEdge ( ) ) . length ;
const fields = zoneModeFields [ activeMode ] | | [ ] ;
const title = zoneModeLabels [ activeMode ] | | " Full " ;
detailTitle . textContent = activeMode == = " full " ? " Fabric Map " : ` $ { title } View ` ;
detailSummary . textContent = ` $ { visibleNodes } nodes / $ { visibleEdges } edges visible ` ;
detailPills . innerHTML = activeMode == = " full " ? " " : ` < span class = " pill " > $ { escapeHtml ( activeMode ) } < / span > ` ;
const rows = [ { label : " visible " , value : ` $ { visibleNodes } nodes / $ { visibleEdges } edges ` } ] ;
fields . forEach ( ( field ) = > {
2026-05-24 17:02:12 +02:00
const counts = zoneCountsForField ( visibleElements , field ) ;
2026-05-24 15:55:05 +02:00
rows . push ( {
label : ruleAttributeLabels [ field ] | | humanize ( field ) ,
value : counts . length
? counts . slice ( 0 , 8 ) . map ( ( [ value , count ] ) = > ` $ { value } ( $ { count } ) ` ) . join ( " , " )
: " no annotated visible entities " ,
state : counts . length ? " " : " warning " ,
} ) ;
} ) ;
const warnings = visibleElements
. flatMap ( ( element ) = > zoneWarningsForData ( element . data ( ) ) . map ( ( warning ) = >
` $ { elementLabel ( element ) } : $ { warning } `
) ) ;
warnings . slice ( 0 , 6 ) . forEach ( ( warning ) = > rows . push ( { label : " warning " , value : warning , state : " warning " } ) ) ;
if ( warnings . length > 6 ) {
rows . push ( { label : " warning " , value : ` $ { warnings . length - 6 } additional route warnings ` , state : " warning " } ) ;
}
detailList . innerHTML = rows
. filter ( ( row ) = > row . value )
. map ( ( row ) = > ` < li class = " $ { orientationStateClass(row.state)} " > < strong > $ { escapeHtml ( row . label ) } < / strong > $ { escapeHtml ( row . value ) } < / li > ` )
. join ( " " ) ;
} ;
2026-05-19 01:46:23 +02:00
const collectionArray = ( collection ) = > Array . from ( collection | | [ ] ) ;
const orientationStateClass = ( state ) = >
state == = " warning " ? " orientation-warning " : state == = " good " ? " orientation-good " : " " ;
2026-05-19 00:16:57 +02:00
const renderedElementPosition = ( element ) = > {
if ( ! element | | ! cy ) return null ;
if ( element . isNode & & element . isNode ( ) ) return element . renderedPosition ( ) ;
if ( element . isEdge & & element . isEdge ( ) ) {
if ( typeof element . renderedMidpoint == = " function " ) {
const midpoint = element . renderedMidpoint ( ) ;
if ( isFinitePoint ( midpoint ) ) return midpoint ;
}
const source = cy . getElementById ( element . data ( " source " ) ) ;
const target = cy . getElementById ( element . data ( " target " ) ) ;
if ( source . length > 0 & & target . length > 0 ) {
const sourcePosition = source . renderedPosition ( ) ;
const targetPosition = target . renderedPosition ( ) ;
if ( isFinitePoint ( sourcePosition ) & & isFinitePoint ( targetPosition ) ) {
return {
x : ( sourcePosition . x + targetPosition . x ) / 2 ,
y : ( sourcePosition . y + targetPosition . y ) / 2 ,
} ;
}
}
}
return null ;
} ;
const updateSelectionAnchor = ( ) = > {
if ( ! selected | | ! selectionAnchor ) {
selectedAnchor = null ;
selectionAnchor . style . display = " none " ;
return ;
}
const position = renderedElementPosition ( selected ) ;
if ( isFinitePoint ( position ) ) {
selectedAnchor = { x : position . x , y : position . y , label : elementLabel ( selected ) } ;
}
if ( ! selectedAnchor ) {
selectionAnchor . style . display = " none " ;
return ;
}
selectionAnchor . style . left = ` $ { selectedAnchor . x } px ` ;
selectionAnchor . style . top = ` $ { selectedAnchor . y } px ` ;
selectionAnchorLabel . textContent = selectedAnchor . label ;
selectionAnchor . style . display = " block " ;
} ;
2026-05-19 00:27:09 +02:00
const isKeyLabelNode = ( node ) = > {
const layer = node . data ( " layer " ) ;
return (
[ " repository " , " server " , " deployment " , " service " ] . includes ( layer ) | |
node . data ( " unresolved " ) == = true | |
node . data ( " lifecycle " ) == = " registered-only "
) ;
} ;
const updateLabelVisibility = ( ) = > {
if ( ! cy ) return ;
const visibleNodes = cy . nodes ( ) . filter ( ( node ) = > node . style ( " display " ) != = " none " ) ;
const mode = activeLabelMode == = " auto " & & visibleNodes . length > 70 ? " key " : activeLabelMode ;
cy . nodes ( ) . forEach ( ( node ) = > {
let showLabel = mode == = " all " | | mode == = " auto " ;
if ( mode == = " key " ) showLabel = isKeyLabelNode ( node ) ;
if ( mode == = " none " ) showLabel = false ;
if ( selected & & node . id ( ) == = selected . id ( ) ) showLabel = false ;
node . toggleClass ( " label-hidden " , ! showLabel ) ;
} ) ;
} ;
2026-05-25 00:27:10 +02:00
const currentZoneViewState = ( ) = > {
const visible = zoneBoundaryToggle ? zoneBoundaryToggle . checked : true ;
const grouping = zoneGroupSelect ? zoneGroupSelect . value | | " deploymentEnvironment " : " deploymentEnvironment " ;
2026-05-25 03:17:23 +02:00
const layoutAlgorithm = normalizeZoneLayoutAlgorithm (
zoneLayoutSelect ? zoneLayoutSelect . value : activeZoneLayoutAlgorithm
) ;
2026-05-25 00:27:10 +02:00
const definitionSet = zoneDefinitionSets [ activeZoneDefinitionSet ]
? activeZoneDefinitionSet
: " fabric-default " ;
return {
visible ,
grouping ,
definitionSet ,
2026-05-25 03:17:23 +02:00
layout : {
algorithm : layoutAlgorithm ,
} ,
2026-05-25 00:27:10 +02:00
presentation : {
boundaries : visible ,
labels : true ,
} ,
2026-05-25 02:08:45 +02:00
containers : serializedZoneContainers ( ) ,
2026-05-25 00:27:10 +02:00
} ;
} ;
const currentViewState = ( ) = > {
const zone = currentZoneViewState ( ) ;
return {
search : searchInput . value ,
mode : modeSelect . value | | " full " ,
layout : layoutSelect . value | | " cose " ,
labelMode : labelSelect . value | | " auto " ,
nodeTypes : Array . from ( selectedNodeTypes ( ) ) ,
edgeTypes : Array . from ( selectedEdgeTypes ( ) ) ,
review : reviewFilter . value ,
unresolved : unresolvedFilter . value ,
zone ,
zoneBoundaries : zone . visible ,
zoneGrouping : zone . grouping ,
zoneDefinitionSet : zone . definitionSet ,
2026-05-25 03:17:23 +02:00
zoneLayoutAlgorithm : zone . layout . algorithm ,
2026-05-25 00:27:10 +02:00
rules : filterRules . map ( ( rule ) = > ( { . . . rule } ) ) ,
manualOverrides : { . . . manualOverrides } ,
} ;
} ;
2026-05-18 20:14:07 +02:00
const encodeStateBlob = ( state ) = > {
const bytes = new TextEncoder ( ) . encode ( JSON . stringify ( state ) ) ;
let binary = " " ;
bytes . forEach ( ( byte ) = > { binary + = String . fromCharCode ( byte ) ; } ) ;
return btoa ( binary ) ;
} ;
const decodeStateBlob = ( blob ) = > {
const bytes = Uint8Array . from ( atob ( blob ) , ( char ) = > char . charCodeAt ( 0 ) ) ;
return JSON . parse ( new TextDecoder ( ) . decode ( bytes ) ) ;
} ;
const readUrlState = ( ) = > {
const params = new URLSearchParams ( window . location . search ) ;
const state = { } ;
if ( params . has ( " search " ) ) state . search = params . get ( " search " ) | | " " ;
if ( params . has ( " mode " ) ) state . mode = params . get ( " mode " ) | | " " ;
if ( params . has ( " layout " ) ) state . layout = params . get ( " layout " ) | | " " ;
2026-05-19 00:27:09 +02:00
if ( params . has ( " labelMode " ) ) state . labelMode = params . get ( " labelMode " ) | | " " ;
2026-05-18 23:58:58 +02:00
if ( params . has ( " nodeTypes " ) ) state . nodeTypes = parseListParam ( params . get ( " nodeTypes " ) ) ;
if ( params . has ( " edgeTypes " ) ) state . edgeTypes = parseListParam ( params . get ( " edgeTypes " ) ) ;
if ( params . has ( " layer " ) ) state . nodeTypes = parseListParam ( params . get ( " layer " ) ) ;
2026-05-18 20:14:07 +02:00
if ( params . has ( " review " ) ) state . review = params . get ( " review " ) | | " " ;
if ( params . has ( " unresolved " ) ) state . unresolved = params . get ( " unresolved " ) | | " " ;
2026-05-24 17:02:12 +02:00
if ( params . has ( " zoneBoundaries " ) ) state . zoneBoundaries = params . get ( " zoneBoundaries " ) != = " 0 " ;
if ( params . has ( " zoneGrouping " ) ) state . zoneGrouping = params . get ( " zoneGrouping " ) | | " " ;
2026-05-25 00:27:10 +02:00
if ( params . has ( " zoneDefinitionSet " ) ) state . zoneDefinitionSet = params . get ( " zoneDefinitionSet " ) | | " " ;
2026-05-25 03:17:23 +02:00
if ( params . has ( " zoneLayout " ) ) state . zoneLayoutAlgorithm = params . get ( " zoneLayout " ) | | " " ;
2026-05-18 20:14:07 +02:00
if ( params . has ( " profile " ) ) state . profile = params . get ( " profile " ) | | " " ;
if ( params . has ( " state " ) ) {
try {
Object . assign ( state , decodeStateBlob ( params . get ( " state " ) | | " " ) ) ;
} catch ( error ) {
profileSummary . textContent = ` Could not load copied map state : $ { error . message } ` ;
}
}
return state ;
} ;
const viewUrl = ( includeStateBlob = false ) = > {
const params = new URLSearchParams ( ) ;
const state = currentViewState ( ) ;
if ( state . search ) params . set ( " search " , state . search ) ;
if ( state . mode & & state . mode != = " full " ) params . set ( " mode " , state . mode ) ;
if ( state . layout & & state . layout != = " cose " ) params . set ( " layout " , state . layout ) ;
2026-05-19 00:27:09 +02:00
if ( state . labelMode & & state . labelMode != = " auto " ) params . set ( " labelMode " , state . labelMode ) ;
2026-05-18 23:58:58 +02:00
if ( state . nodeTypes . length != = allNodeTypes . length ) params . set ( " nodeTypes " , state . nodeTypes . join ( " , " ) ) ;
if ( state . edgeTypes . length != = allEdgeTypes . length ) params . set ( " edgeTypes " , state . edgeTypes . join ( " , " ) ) ;
2026-05-18 20:14:07 +02:00
if ( state . review ) params . set ( " review " , state . review ) ;
if ( state . unresolved ) params . set ( " unresolved " , state . unresolved ) ;
2026-05-24 17:02:12 +02:00
if ( state . zoneBoundaries == = false ) params . set ( " zoneBoundaries " , " 0 " ) ;
if ( state . zoneGrouping & & state . zoneGrouping != = " deploymentEnvironment " ) params . set ( " zoneGrouping " , state . zoneGrouping ) ;
2026-05-25 00:27:10 +02:00
if ( state . zoneDefinitionSet & & state . zoneDefinitionSet != = " fabric-default " ) params . set ( " zoneDefinitionSet " , state . zoneDefinitionSet ) ;
2026-05-25 03:17:23 +02:00
if ( state . zoneLayoutAlgorithm & & state . zoneLayoutAlgorithm != = " compact-grid " ) params . set ( " zoneLayout " , state . zoneLayoutAlgorithm ) ;
2026-05-18 20:14:07 +02:00
if ( currentProfileId ) params . set ( " profile " , currentProfileId ) ;
2026-05-19 01:16:18 +02:00
if ( includeStateBlob | | hasManualOverrides ( ) | | filterRules . length > 0 ) {
params . set ( " state " , encodeStateBlob ( state ) ) ;
}
2026-05-18 20:14:07 +02:00
const query = params . toString ( ) ;
return ` $ { window . location . pathname } $ { query ? ` ? $ { query } ` : " " } $ { window . location . hash } ` ;
} ;
const updateUrlState = ( ) = > {
if ( ! window . history | | ! window . history . replaceState ) return ;
window . history . replaceState ( { } , " " , viewUrl ( false ) ) ;
} ;
const optionExists = ( select , value ) = >
Array . from ( select . options ) . some ( ( option ) = > option . value == = value ) ;
2026-05-25 00:27:10 +02:00
const normalizeZoneViewState = ( state ) = > {
const nested = state . zone & & typeof state . zone == = " object " ? state . zone : { } ;
const presentation = nested . presentation & & typeof nested . presentation == = " object "
? nested . presentation
: { } ;
2026-05-25 02:08:45 +02:00
const containers = nested . containers & & typeof nested . containers == = " object "
? nested . containers
: state . zoneContainers ;
2026-05-25 03:17:23 +02:00
const layout = nested . layout & & typeof nested . layout == = " object " ? nested . layout : { } ;
2026-05-25 00:27:10 +02:00
const grouping = nested . grouping | | state . zoneGrouping | | " deploymentEnvironment " ;
const definitionSet = zoneDefinitionSets [ nested . definitionSet | | state . zoneDefinitionSet ]
? nested . definitionSet | | state . zoneDefinitionSet
: " fabric-default " ;
const visible = " visible " in nested
? nested . visible != = false
: " zoneBoundaries " in state
? state . zoneBoundaries != = false
: true ;
return {
visible ,
grouping : zoneGroupSelect & & optionExists ( zoneGroupSelect , grouping ) ? grouping : " deploymentEnvironment " ,
definitionSet ,
2026-05-25 03:17:23 +02:00
layout : {
algorithm : normalizeZoneLayoutAlgorithm ( layout . algorithm | | state . zoneLayoutAlgorithm ) ,
} ,
2026-05-25 00:27:10 +02:00
presentation : {
boundaries : " boundaries " in presentation ? presentation . boundaries != = false : visible ,
labels : " labels " in presentation ? presentation . labels != = false : true ,
} ,
2026-05-25 02:08:45 +02:00
containers : normalizedZoneContainers ( containers ) ,
2026-05-25 00:27:10 +02:00
} ;
} ;
2026-05-18 20:14:07 +02:00
const applyViewState = ( state , options = { } ) = > {
if ( " search " in state ) searchInput . value = state . search | | " " ;
if ( " mode " in state ) modeSelect . value = optionExists ( modeSelect , state . mode ) ? state . mode : " full " ;
if ( " layout " in state ) layoutSelect . value = optionExists ( layoutSelect , state . layout ) ? state . layout : " cose " ;
2026-05-19 00:27:09 +02:00
if ( " labelMode " in state ) labelSelect . value = optionExists ( labelSelect , state . labelMode ) ? state . labelMode : " auto " ;
2026-05-18 23:58:58 +02:00
if ( " nodeTypes " in state ) setCheckedValues ( nodeTypeFilter , ( state . nodeTypes | | [ ] ) . filter ( ( value ) = > allNodeTypes . includes ( value ) ) ) ;
if ( " layer " in state ) setCheckedValues ( nodeTypeFilter , allNodeTypes . includes ( state . layer ) ? [ state . layer ] : null ) ;
if ( " edgeTypes " in state ) setCheckedValues ( edgeTypeFilter , ( state . edgeTypes | | [ ] ) . filter ( ( value ) = > allEdgeTypes . includes ( value ) ) ) ;
2026-05-18 20:14:07 +02:00
if ( " review " in state ) reviewFilter . value = optionExists ( reviewFilter , state . review ) ? state . review : " " ;
if ( " unresolved " in state ) unresolvedFilter . value = optionExists ( unresolvedFilter , state . unresolved ) ? state . unresolved : " " ;
2026-05-25 03:17:23 +02:00
if ( " zone " in state | | " zoneBoundaries " in state | | " zoneGrouping " in state | | " zoneDefinitionSet " in state | | " zoneLayoutAlgorithm " in state ) {
2026-05-25 00:27:10 +02:00
const zone = normalizeZoneViewState ( state ) ;
activeZoneDefinitionSet = zone . definitionSet ;
if ( zoneBoundaryToggle ) zoneBoundaryToggle . checked = zone . visible ;
if ( zoneGroupSelect ) zoneGroupSelect . value = zone . grouping ;
2026-05-25 03:17:23 +02:00
activeZoneLayoutAlgorithm = zone . layout . algorithm ;
if ( zoneLayoutSelect ) zoneLayoutSelect . value = activeZoneLayoutAlgorithm ;
2026-05-25 02:08:45 +02:00
zoneContainerState = zone . containers ;
2026-05-24 17:02:12 +02:00
}
2026-05-18 20:14:07 +02:00
if ( " manualOverrides " in state & & state . manualOverrides & & typeof state . manualOverrides == = " object " ) {
manualOverrides = { . . . state . manualOverrides } ;
}
2026-05-19 01:16:18 +02:00
if ( " rules " in state ) {
filterRules = normalizeRules ( state . rules ) ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
}
2026-05-18 20:14:07 +02:00
activeMode = modeSelect . value | | " full " ;
2026-05-19 00:27:09 +02:00
activeLabelMode = labelSelect . value | | " auto " ;
2026-05-24 17:02:12 +02:00
activeZoneGrouping = zoneGroupSelect ? zoneGroupSelect . value | | " deploymentEnvironment " : " deploymentEnvironment " ;
2026-05-25 03:17:23 +02:00
activeZoneLayoutAlgorithm = normalizeZoneLayoutAlgorithm (
zoneLayoutSelect ? zoneLayoutSelect . value : activeZoneLayoutAlgorithm
) ;
2026-05-24 17:02:12 +02:00
selectedZoneId = " " ;
2026-05-25 00:40:13 +02:00
collapsedZoneSnapshots = new Map ( ) ;
2026-05-18 20:14:07 +02:00
focusSet = null ;
2026-05-18 23:58:58 +02:00
syncFilterSummaries ( ) ;
2026-05-18 20:14:07 +02:00
applyFilters ( ) ;
if ( ! options . skipLayout ) runLayout ( ) ;
if ( ! options . skipUrl ) updateUrlState ( ) ;
} ;
const loadProfiles = ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return [ ] ;
try {
const parsed = JSON . parse ( window . localStorage . getItem ( profileStorageKey ) | | " [] " ) ;
return Array . isArray ( parsed )
? parsed . filter ( ( profile ) = > profile & & typeof profile . id == = " string " )
: [ ] ;
} catch {
return [ ] ;
}
} ;
const persistProfiles = ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return ;
window . localStorage . setItem ( profileStorageKey , JSON . stringify ( profiles ) ) ;
} ;
const selectedProfile = ( ) = > profiles . find ( ( profile ) = > profile . id == = currentProfileId ) | | null ;
2026-05-19 01:46:23 +02:00
const viewStateSummary = ( ) = > {
const parts = [ ] ;
if ( searchInput . value . trim ( ) ) parts . push ( " search " ) ;
if ( ( modeSelect . value | | " full " ) != = " full " ) parts . push ( modeSelect . options [ modeSelect . selectedIndex ] ? . textContent | | " mode " ) ;
if ( ( labelSelect . value | | " auto " ) != = " auto " ) parts . push ( ` $ { labelSelect . value } labels ` ) ;
if ( selectedNodeTypes ( ) . size != = allNodeTypes . length ) parts . push ( " node filter " ) ;
if ( selectedEdgeTypes ( ) . size != = allEdgeTypes . length ) parts . push ( " edge filter " ) ;
if ( reviewFilter . value ) parts . push ( ` $ { reviewFilter . value } review ` ) ;
if ( unresolvedFilter . value ) parts . push ( " unresolved only " ) ;
2026-05-24 17:02:12 +02:00
if ( zoneBoundaryToggle & & ! zoneBoundaryToggle . checked ) parts . push ( " zones off " ) ;
if ( zoneGroupSelect & & ( zoneGroupSelect . value | | " deploymentEnvironment " ) != = " deploymentEnvironment " ) {
parts . push ( " access zone boundaries " ) ;
}
2026-05-25 03:17:23 +02:00
if ( zoneLayoutSelect & & ( zoneLayoutSelect . value | | " compact-grid " ) != = " compact-grid " ) {
parts . push ( ` $ { zoneLayoutSelect . options [ zoneLayoutSelect . selectedIndex ] ? . textContent | | zoneLayoutSelect . value } zone layout ` ) ;
}
2026-05-19 01:46:23 +02:00
if ( filterRules . length ) parts . push ( ` $ { filterRules . length } rule $ { filterRules . length == = 1 ? " " : " s " } ` ) ;
const overrideCount = Object . keys ( manualOverrides ) . length ;
if ( overrideCount ) parts . push ( ` $ { overrideCount } override $ { overrideCount == = 1 ? " " : " s " } ` ) ;
return parts . length ? parts . join ( " , " ) : " no filters " ;
} ;
2026-05-18 20:14:07 +02:00
const updateProfileSummary = ( message = " " ) = > {
if ( ! supportsLocalProfiles ( ) ) {
profileSummary . textContent = " Profile persistence unavailable for this host. " ;
return ;
}
const profile = selectedProfile ( ) ;
const prefix = profile ? ` Loaded " $ {profile.name} " . ` : " Unsaved exploration. " ;
2026-05-19 01:46:23 +02:00
profileSummary . textContent = message | | ` $ { prefix } Current state : $ { viewStateSummary ( ) } . $ { profiles . length } saved view $ { profiles . length == = 1 ? " " : " s " } . ` ;
2026-05-18 20:14:07 +02:00
} ;
const updateProfileControls = ( ) = > {
const enabled = supportsLocalProfiles ( ) ;
profileSelect . disabled = ! enabled ;
profileNameInput . disabled = ! enabled ;
document . querySelectorAll ( " [data-profile-action] " ) . forEach ( ( button ) = > {
const action = button . dataset . profileAction ;
button . disabled = ! enabled | | ( ( action == = " duplicate " | | action == = " delete " ) & & ! currentProfileId ) ;
} ) ;
} ;
const renderProfiles = ( ) = > {
profileSelect . innerHTML = " " ;
const unsaved = document . createElement ( " option " ) ;
unsaved . value = " " ;
unsaved . textContent = " Unsaved exploration " ;
profileSelect . appendChild ( unsaved ) ;
profiles
. slice ( )
. sort ( ( left , right ) = > String ( left . name ) . localeCompare ( String ( right . name ) ) )
. forEach ( ( profile ) = > {
const option = document . createElement ( " option " ) ;
option . value = profile . id ;
option . textContent = profile . name | | profile . id ;
profileSelect . appendChild ( option ) ;
} ) ;
if ( ! profiles . some ( ( profile ) = > profile . id == = currentProfileId ) ) currentProfileId = " " ;
profileSelect . value = currentProfileId ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
} ;
const saveCurrentProfile = ( name , sourceProfile = null ) = > {
const now = new Date ( ) . toISOString ( ) ;
2026-05-19 01:46:23 +02:00
const profileName = name . trim ( ) | | sourceProfile ? . name | | orientationContext ? . profileName | | ` Fabric view $ { profiles . length + 1 } ` ;
2026-05-18 20:14:07 +02:00
if ( sourceProfile ) {
const profile = {
. . . sourceProfile ,
id : ` local - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
name : profileName ,
createdAt : now ,
updatedAt : now ,
} ;
profiles = [ . . . profiles , profile ] ;
currentProfileId = profile . id ;
} else if ( currentProfileId ) {
profiles = profiles . map ( ( profile ) = > profile . id == = currentProfileId
? { . . . profile , name : profileName , state : currentViewState ( ) , updatedAt : now }
: profile ) ;
} else {
const profile = {
id : ` local - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
name : profileName ,
state : currentViewState ( ) ,
createdAt : now ,
updatedAt : now ,
} ;
profiles = [ . . . profiles , profile ] ;
currentProfileId = profile . id ;
}
persistProfiles ( ) ;
renderProfiles ( ) ;
updateUrlState ( ) ;
} ;
2026-05-18 17:11:36 +02:00
const matchesFilters = ( element ) = > {
const data = element . data ( ) ;
2026-05-18 18:31:25 +02:00
const modeSet = visibleSetForMode ( ) ;
if ( modeSet & & ! modeSet . has ( element . id ( ) ) ) return false ;
2026-05-18 17:11:36 +02:00
const text = searchInput . value . trim ( ) . toLowerCase ( ) ;
if ( text & & ! elementText ( data ) . includes ( text ) ) return false ;
2026-05-18 23:58:58 +02:00
if ( element . isNode ( ) & & ! selectedNodeTypes ( ) . has ( data . layer ) ) return false ;
if ( element . isEdge ( ) & & ! selectedEdgeTypes ( ) . has ( data . edgeType ) ) return false ;
2026-05-18 17:11:36 +02:00
if ( reviewFilter . value & & data . reviewState != = reviewFilter . value ) return false ;
if ( unresolvedFilter . value == = " true " & & data . unresolved != = true ) return false ;
if ( focusSet & & ! focusSet . has ( element . id ( ) ) ) return false ;
return true ;
} ;
2026-05-18 18:31:25 +02:00
const visibleSetForMode = ( ) = > {
if ( ! cy ) return null ;
if ( focusSet ) return focusSet ;
if ( activeMode == = " full " ) return null ;
if ( activeMode == = " onboarding-gaps " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = >
element . data ( " lifecycle " ) == = " registered-only " | | element . data ( " unresolved " ) == = true
) . map ( ( element ) = > element . id ( ) ) ) ;
}
if ( activeMode == = " unresolved " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = > element . data ( " unresolved " ) == = true ) . map ( ( element ) = > element . id ( ) ) ) ;
}
2026-05-24 15:55:05 +02:00
const zoneFields = zoneModeFields [ activeMode ] | | [ ] ;
if ( zoneFields . length & & activeMode != = " by-fabric " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = >
zoneFields . some ( ( field ) = > hasValue ( element . data ( field ) ) )
) . map ( ( element ) = > element . id ( ) ) ) ;
}
2026-05-18 18:31:25 +02:00
if ( ! selected ) return null ;
if ( activeMode == = " selected-path " ) {
const collection = selected . union ( selected . predecessors ( ) ) . union ( selected . successors ( ) ) ;
return new Set ( collection . map ( ( element ) = > element . id ( ) ) ) ;
}
if ( activeMode == = " neighborhood " ) {
const collection = selected . union ( selected . neighborhood ( ) ) . union ( selected . neighborhood ( ) . neighborhood ( ) ) ;
return new Set ( collection . map ( ( element ) = > element . id ( ) ) ) ;
}
return null ;
} ;
2026-05-19 01:16:18 +02:00
const applyFilters = ( options = { } ) = > {
2026-05-18 17:11:36 +02:00
if ( ! cy ) return ;
2026-05-25 00:40:13 +02:00
removeZoneCollapseElements ( ) ;
2026-05-19 01:16:18 +02:00
const previousRemoved = options . redrawOnRemove ? ruleRemovalSignature ( ) : " " ;
2026-05-18 23:58:58 +02:00
syncFilterSummaries ( ) ;
2026-05-18 17:11:36 +02:00
const hiddenNodes = new Set ( ) ;
2026-05-19 01:16:18 +02:00
const removedNodes = new Set ( ) ;
2026-05-18 17:11:36 +02:00
cy . nodes ( ) . forEach ( ( node ) = > {
let state = matchesFilters ( node ) ? " show " : " hide " ;
2026-05-19 01:16:18 +02:00
const ruleAction = ruleActionFor ( node ) ;
if ( ruleAction ) state = ruleAction ;
if ( state != = " remove " ) {
const override = manualOverrideFor ( node ) ;
if ( override ) state = override ;
}
2026-05-18 17:11:36 +02:00
node . data ( " displayState " , state ) ;
node . toggleClass ( " display-blur " , state == = " blur " ) ;
2026-05-19 01:16:18 +02:00
node . toggleClass ( " rule-highlight " , state == = " highlight " ) ;
node . style ( " display " , state == = " hide " | | state == = " remove " ? " none " : " element " ) ;
if ( state == = " remove " ) removedNodes . add ( node . id ( ) ) ;
2026-05-18 17:11:36 +02:00
if ( state == = " hide " ) hiddenNodes . add ( node . id ( ) ) ;
} ) ;
2026-05-25 00:40:13 +02:00
applyZoneCollapseVisibility ( hiddenNodes ) ;
2026-05-18 17:11:36 +02:00
cy . edges ( ) . forEach ( ( edge ) = > {
let state = matchesFilters ( edge ) ? " show " : " hide " ;
2026-05-19 01:16:18 +02:00
const ruleAction = ruleActionFor ( edge ) ;
if ( ruleAction ) state = ruleAction ;
if ( removedNodes . has ( edge . data ( " source " ) ) | | removedNodes . has ( edge . data ( " target " ) ) ) {
state = " remove " ;
} else if ( hiddenNodes . has ( edge . data ( " source " ) ) | | hiddenNodes . has ( edge . data ( " target " ) ) ) {
state = " hide " ;
}
if ( state != = " remove " ) {
const override = manualOverrideFor ( edge ) ;
if ( override ) state = override ;
}
2026-05-18 17:11:36 +02:00
edge . data ( " displayState " , state ) ;
edge . toggleClass ( " display-blur " , state == = " blur " ) ;
2026-05-19 01:16:18 +02:00
edge . toggleClass ( " rule-highlight " , state == = " highlight " ) ;
edge . style ( " display " , state == = " hide " | | state == = " remove " ? " none " : " element " ) ;
2026-05-18 17:11:36 +02:00
} ) ;
2026-05-25 00:40:13 +02:00
addZoneCollapseElements ( ) ;
updateLabelVisibility ( ) ;
2026-05-18 17:11:36 +02:00
const visibleNodes = cy . nodes ( ) . filter ( ( node ) = > node . style ( " display " ) != = " none " ) . length ;
const visibleEdges = cy . edges ( ) . filter ( ( edge ) = > edge . style ( " display " ) != = " none " ) . length ;
2026-05-19 01:16:18 +02:00
const removed = cy . elements ( ) . filter ( ( element ) = > element . data ( " displayState " ) == = " remove " ) . length ;
const hidden = cy . elements ( ) . length - visibleNodes - visibleEdges - removed ;
2026-05-18 17:11:36 +02:00
counts . textContent = ` $ { visibleNodes } nodes / $ { visibleEdges } edges ` ;
2026-05-19 01:16:18 +02:00
hiddenSummary . textContent = removed ? ` Hidden $ { hidden } / Removed $ { removed } ` : ` Hidden $ { hidden } ` ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-24 17:02:12 +02:00
scheduleZoneOverlayUpdate ( ) ;
2026-05-24 15:55:05 +02:00
if ( ! selected ) renderMapOverview ( ) ;
2026-05-19 01:16:18 +02:00
if ( options . redrawOnRemove & & previousRemoved != = ruleRemovalSignature ( ) ) runLayout ( ) ;
2026-05-18 17:11:36 +02:00
} ;
const showDetails = ( element ) = > {
selected = element | | null ;
2026-05-24 17:02:12 +02:00
selectedZoneId = " " ;
scheduleZoneOverlayUpdate ( ) ;
2026-05-18 17:11:36 +02:00
if ( ! element ) {
2026-05-19 01:46:23 +02:00
orientationContext = null ;
2026-05-24 15:55:05 +02:00
renderMapOverview ( ) ;
2026-05-18 18:36:15 +02:00
orientationTitle . textContent = " Select a service, interface, dependency, or registered-only repo. " ;
orientationList . innerHTML = " " ;
2026-05-19 01:46:23 +02:00
orientationActions . innerHTML = " " ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-18 17:11:36 +02:00
return ;
}
const data = element . data ( ) ;
detailTitle . textContent = data . name | | data . label | | data . id ;
detailSummary . textContent = data . description | | data . id ;
2026-05-18 23:58:58 +02:00
const nodeType = data . layer ? nodeTypeLabels [ data . layer ] | | humanize ( data . layer ) : " " ;
2026-05-23 14:00:59 +02:00
detailPills . innerHTML = [ data . kind , nodeType , data . canonCategory | | data . canonicalType , data . evidenceState , data . repo , data . reviewState , data . displayState ]
2026-05-18 23:58:58 +02:00
. map ( ( value ) = > value ? ` < span class = " pill " > $ { escapeHtml ( value ) } < / span > ` : " " )
2026-05-18 17:11:36 +02:00
. join ( " " ) ;
const links = data . deepLinks | | { } ;
const refs = data . sourceReferences | | [ ] ;
const rows = [
[ " id " , data . id ] ,
[ " source " , data . source ] ,
[ " target " , data . target ] ,
[ " edge " , data . edgeType ] ,
2026-05-23 14:00:59 +02:00
[ " canonical " , data . canonicalType | | data . canonCategory ] ,
[ " evidence " , data . evidenceState ] ,
[ " mapping " , data . mappingFit ] ,
[ " display only " , data . displayOnly == = true ? " yes " : " " ] ,
2026-05-18 17:11:36 +02:00
[ " strength " , data . strength ] ,
2026-05-25 00:40:13 +02:00
[ " collapsed zone " , data . collapsedZoneLabel ] ,
[ " contained nodes " , data . containedNodeCount ] ,
[ " internal edges " , data . containedInternalEdgeCount ] ,
[ " boundary edges " , data . boundaryEdgeCount ] ,
2026-05-24 15:55:05 +02:00
[ " deployment environment " , data . deploymentEnvironment ] ,
[ " deployment scenario " , data . deploymentScenario ] ,
[ " routing authority " , data . routingAuthority ] ,
[ " access zone " , data . accessZone ] ,
[ " policy authority " , data . policyAuthority ] ,
[ " exposure " , data . exposureClass ] ,
[ " route " , routeLabel ( data ) ] ,
. . . zoneWarningsForData ( data ) . map ( ( warning ) = > [ " warning " , warning ] ) ,
2026-05-18 17:11:36 +02:00
. . . Object . entries ( links ) ,
. . . refs . map ( ( ref ) = > [ ref . label | | ref . kind | | " source " , ref . path | | ref . url | | ref . ref | | " " ] )
] ;
detailList . innerHTML = rows
. filter ( ( [ , value ] ) = > value )
. map ( ( [ key , value ] ) = > ` < li > < strong > $ { escapeHtml ( key ) } < / strong > $ { escapeHtml ( value ) } < / li > ` )
. join ( " " ) ;
2026-05-18 18:36:15 +02:00
renderOrientation ( element ) ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-18 18:36:15 +02:00
} ;
const renderOrientation = ( element ) = > {
const data = element . data ( ) ;
2026-05-19 01:46:23 +02:00
const contextIds = new Set ( [ element . id ( ) ] ) ;
2026-05-18 18:36:15 +02:00
const rows = [ ] ;
2026-05-25 00:40:13 +02:00
const extraOrientationActions = [ ] ;
2026-05-19 01:46:23 +02:00
let title = " Graph context " ;
let profileName = ` Fabric context : $ { elementLabel ( element ) } ` ;
2026-05-25 00:40:13 +02:00
if ( data . kind == = " Zone " & & data . zoneCollapse == = true ) {
title = " Collapsed zone " ;
profileName = ` Zone : $ { data . collapsedZoneLabel | | elementLabel ( element ) } ` ;
const neighborhood = element . neighborhood ( ) ;
neighborhood . forEach ( ( item ) = > addContextElement ( contextIds , item ) ) ;
rows . push (
{ label : " zone " , value : data . collapsedZoneLabel | | data . id , state : " good " } ,
{ label : " contained nodes " , value : String ( data . containedNodeCount | | 0 ) } ,
{ label : " internal edges " , value : String ( data . containedInternalEdgeCount | | 0 ) } ,
{ label : " boundary edges " , value : String ( data . boundaryEdgeCount | | 0 ) }
) ;
extraOrientationActions . push (
` < button type = " button " data - orientation - action = " expand-zone " data - zone - id = " $ { escapeHtml(data.collapsedZoneId)} " > Expand Zone < / button > `
) ;
} else if ( data . kind == = " Repository " & & data . lifecycle == = " registered-only " ) {
2026-05-19 01:46:23 +02:00
title = " Onboarding gap " ;
profileName = ` Onboarding gap : $ { data . repo | | elementLabel ( element ) } ` ;
rows . push (
{ label : " repo " , value : data . repo | | data . id } ,
{ label : " status " , value : " registered without an accepted Fabric graph snapshot " , state : " warning " } ,
{ label : " next " , value : " add Fabric declarations or run registry sync once a snapshot exists " }
) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " InterfaceDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Interface consumers and impact " ;
profileName = ` Interface impact : $ { elementLabel ( element ) } ` ;
const attributes = data . attributes | | { } ;
if ( attributes . service_id ) {
addContextNode ( contextIds , attributes . service_id ) ;
rows . push ( { label : " provider service " , value : nodeName ( attributes . service_id ) , state : " good " } ) ;
}
edgeCollection ( " " , data . id , " available_via " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
const capability = edge . data ( " source " ) ;
edgeCollection ( " " , capability , " provides " ) . forEach ( ( providerEdge ) = > {
addContextEdge ( contextIds , providerEdge ) ;
rows . push ( {
label : " provider surface " ,
value : ` $ { nodeName ( providerEdge . data ( " source " ) ) } - > $ { nodeName ( capability ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
state : " good " ,
} ) ;
} ) ;
} ) ;
let consumerCount = 0 ;
edgeCollection ( " " , data . id , " uses_interface " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const dependency = edge . data ( " source " ) ;
2026-05-19 01:46:23 +02:00
const dependencyNode = nodeById ( dependency ) ;
edgeCollection ( " " , dependency , " consumes " ) . forEach ( ( consumerEdge ) = > {
consumerCount + = 1 ;
addContextEdge ( contextIds , consumerEdge ) ;
rows . push ( {
label : " consumer " ,
value : ` $ { nodeName ( consumerEdge . data ( " source " ) ) } - > $ { nodeName ( dependency ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
state : dependencyNode ? . data ( " unresolved " ) == = true ? " warning " : " " ,
} ) ;
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
2026-05-19 01:46:23 +02:00
if ( consumerCount == = 0 ) rows . push ( { label : " consumer " , value : " no accepted consumers in current graph " } ) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " ServiceDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Service dependency chain " ;
profileName = ` Service map : $ { elementLabel ( element ) } ` ;
let dependencyCount = 0 ;
edgeCollection ( data . id , " " , " consumes " ) . forEach ( ( edge ) = > {
dependencyCount + = 1 ;
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const dependency = edge . data ( " target " ) ;
const providerEdges = cy . edges ( ) . filter ( ( candidate ) = >
2026-05-19 01:46:23 +02:00
candidate . data ( " source " ) == = dependency & & (
String ( candidate . data ( " edgeType " ) ) . startsWith ( " binds: " ) | |
candidate . data ( " edgeType " ) == = " uses_interface "
)
2026-05-18 18:36:15 +02:00
) ;
if ( providerEdges . length == = 0 ) {
2026-05-19 01:46:23 +02:00
rows . push ( {
label : " requires " ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( dependency ) } - > unresolved ` ,
path : true ,
state : " warning " ,
} ) ;
2026-05-18 18:36:15 +02:00
return ;
}
providerEdges . forEach ( ( providerEdge ) = > {
2026-05-19 01:46:23 +02:00
addContextEdge ( contextIds , providerEdge ) ;
2026-05-18 18:36:15 +02:00
const target = providerEdge . data ( " target " ) ;
2026-05-19 01:46:23 +02:00
rows . push ( {
label : " requires " ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( dependency ) } - > $ { nodeName ( target ) } ( $ { providerEdge . data ( " edgeType " ) } ) ` ,
path : true ,
state : String ( providerEdge . data ( " edgeType " ) ) . includes ( " missing " ) ? " warning " : " good " ,
} ) ;
} ) ;
} ) ;
if ( dependencyCount == = 0 ) rows . push ( { label : " requires " , value : " no declared dependencies " } ) ;
edgeCollection ( data . id , " " , " deployed_as " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
const deployment = edge . data ( " target " ) ;
const runtimeEdges = edgeCollection ( deployment , " " , " runs_on " ) ;
if ( runtimeEdges . length == = 0 ) {
rows . push ( { label : " deployment " , value : ` $ { nodeName ( deployment ) } - > no server binding ` , path : true , state : " warning " } ) ;
return ;
}
runtimeEdges . forEach ( ( runtimeEdge ) = > {
addContextEdge ( contextIds , runtimeEdge ) ;
rows . push ( {
label : " runtime " ,
value : ` $ { nodeName ( deployment ) } - > $ { nodeName ( runtimeEdge . data ( " target " ) ) } ` ,
path : true ,
state : " good " ,
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
} ) ;
} else if ( data . kind == = " DependencyDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Dependency binding " ;
profileName = ` Dependency binding : $ { elementLabel ( element ) } ` ;
edgeCollection ( " " , data . id , " consumes " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( {
label : " consumer " ,
value : ` $ { nodeName ( edge . data ( " source " ) ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
} ) ;
} ) ;
2026-05-18 18:36:15 +02:00
cy . edges ( ) . filter ( ( edge ) = >
edge . data ( " source " ) == = data . id & & (
String ( edge . data ( " edgeType " ) ) . startsWith ( " binds: " ) | |
edge . data ( " edgeType " ) == = " uses_interface "
)
) . forEach ( ( edge ) = > {
2026-05-19 01:46:23 +02:00
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const target = edge . data ( " target " ) ;
2026-05-19 01:46:23 +02:00
rows . push ( {
label : edge . data ( " edgeType " ) ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( target ) } ` ,
path : true ,
state : String ( edge . data ( " edgeType " ) ) . includes ( " missing " ) ? " warning " : " good " ,
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
2026-05-19 01:46:23 +02:00
if ( rows . length == = 0 ) rows . push ( { label : " binding " , value : " no provider binding in current graph " , state : " warning " } ) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " CapabilityDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Provider surface " ;
profileName = ` Provider surface : $ { elementLabel ( element ) } ` ;
edgeCollection ( " " , data . id , " provides " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( { label : " service " , value : ` $ { nodeName ( edge . data ( " source " ) ) } - > $ { elementLabel ( element ) } ` , path : true , state : " good " } ) ;
} ) ;
edgeCollection ( data . id , " " , " available_via " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( { label : " interface " , value : ` $ { elementLabel ( element ) } - > $ { nodeName ( edge . data ( " target " ) ) } ` , path : true , state : " good " } ) ;
} ) ;
if ( rows . length == = 0 ) rows . push ( { label : " surface " , value : " no provider surface in current graph " } ) ;
2026-05-18 18:36:15 +02:00
} else {
2026-05-19 01:46:23 +02:00
const neighborhood = element . neighborhood ( ) ;
neighborhood . forEach ( ( item ) = > addContextElement ( contextIds , item ) ) ;
rows . push ( { label : " neighbors " , value : ` $ { neighborhood . nodes ( ) . length } connected nodes ` } ) ;
2026-05-18 18:36:15 +02:00
}
2026-05-19 01:46:23 +02:00
orientationContext = {
ids : Array . from ( contextIds ) ,
profileName ,
} ;
orientationTitle . textContent = ` $ { title } ( $ { orientationContext . ids . length } items ) ` ;
2026-05-18 18:36:15 +02:00
orientationList . innerHTML = rows
2026-05-19 01:46:23 +02:00
. map ( ( row ) = > `
< li class = " orientation-item $ { orientationStateClass(row.state)} " >
< span class = " orientation-label " > $ { escapeHtml ( row . label ) } < / span >
< span class = " orientation-value $ { row.path ? " orientation - path " : " " } " > $ { escapeHtml ( row . value ) } < / span >
< / li >
` )
2026-05-18 18:36:15 +02:00
. join ( " " ) ;
2026-05-19 01:46:23 +02:00
orientationActions . innerHTML = `
2026-05-25 00:40:13 +02:00
$ { extraOrientationActions . join ( " " ) }
2026-05-19 01:46:23 +02:00
< button type = " button " data - orientation - action = " focus " > Focus Context < / button >
< button type = " button " data - orientation - action = " highlight " > Highlight Context < / button >
< button type = " button " data - orientation - action = " hide-other " > Hide Other < / button >
< button type = " button " data - orientation - action = " remove-other " > Remove Other < / button >
< button type = " button " data - orientation - action = " name-view " > Name View < / button >
` ;
} ;
const applyOrientationContext = ( action ) = > {
if ( ! cy | | ! orientationContext ) return ;
const contextIds = new Set ( orientationContext . ids ) ;
if ( action == = " focus " ) {
focusSet = contextIds ;
modeSelect . value = " neighborhood " ;
activeMode = " neighborhood " ;
} else if ( action == = " highlight " ) {
contextIds . forEach ( ( id ) = > { manualOverrides [ id ] = " highlight " ; } ) ;
} else if ( action == = " hide-other " | | action == = " remove-other " ) {
const otherState = action == = " remove-other " ? " remove " : " hide " ;
cy . elements ( ) . forEach ( ( item ) = > {
manualOverrides [ item . id ( ) ] = contextIds . has ( item . id ( ) ) ? " show " : otherState ;
} ) ;
} else if ( action == = " name-view " ) {
if ( supportsLocalProfiles ( ) ) {
profileNameInput . value = orientationContext . profileName ;
updateProfileSummary ( ` Prepared view name " $ {orientationContext.profileName} " . ` ) ;
}
return ;
}
currentProfileId = " " ;
profileSelect . value = " " ;
applyFilters ( { redrawOnRemove : action == = " remove-other " } ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
const applyFocus = ( ) = > {
if ( ! cy | | ! selected ) return ;
const collection = selected . union ( selected . neighborhood ( ) ) . union ( selected . neighborhood ( ) . neighborhood ( ) ) ;
focusSet = new Set ( collection . map ( ( item ) = > item . id ( ) ) ) ;
2026-05-18 18:31:25 +02:00
modeSelect . value = " neighborhood " ;
activeMode = " neighborhood " ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
2026-05-18 22:39:12 +02:00
const edgeIdealLength = ( edge ) = > Number ( edge . data ( " layoutIdealLength " ) ) | | 110 ;
const edgeElasticity = ( edge ) = > Number ( edge . data ( " layoutElasticity " ) ) | | 80 ;
2026-05-18 18:31:25 +02:00
const runLayout = ( ) = > {
if ( ! cy ) return ;
cy . elements ( ) . stop ( ) ;
2026-05-19 01:16:18 +02:00
const layoutElements = cy . elements ( ) . filter ( ( element ) = > element . data ( " displayState " ) != = " remove " ) ;
2026-05-18 18:31:25 +02:00
const name = layoutSelect . value | | " cose " ;
const options = name == = " breadthfirst "
? { name , directed : true , padding : 48 , animate : false }
2026-05-18 22:39:12 +02:00
: name == = " cose "
? {
name ,
padding : 48 ,
animate : false ,
randomize : false ,
nodeOverlap : 12 ,
idealEdgeLength : edgeIdealLength ,
edgeElasticity ,
nodeRepulsion : 5000 ,
gravity : 1 ,
numIter : 1400 ,
}
: { name , padding : 48 , animate : false } ;
2026-05-19 01:16:18 +02:00
layoutElements . layout ( options ) . run ( ) ;
2026-05-25 02:08:45 +02:00
applyZoneContainerLayout ( ) ;
fitVisibleGraph ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-24 17:02:12 +02:00
scheduleZoneOverlayUpdate ( ) ;
2026-05-18 18:31:25 +02:00
} ;
2026-05-18 17:11:36 +02:00
const renderLegend = ( layers ) = > {
2026-05-19 00:27:09 +02:00
const nodeTypes = layers . map ( ( layer ) = >
2026-05-18 17:11:36 +02:00
` < span class = " pill " > < span style = " display:inline-block;width:10px;height:10px;border-radius:50 % ;background:$ { escapeHtml(layer.color || " #667085")}"></span>${escapeHtml(layer.label)}</span>`
2026-05-19 00:27:09 +02:00
) ;
const statusItems = [
' <span class= " pill " ><span style= " display:inline-block;width:12px;height:12px;border:2px dashed #b45309;border-radius:50 % " ></span>Unresolved</span> ' ,
' <span class= " pill " ><span style= " display:inline-block;width:12px;height:12px;border:2px dashed #be123c;border-radius:3px " ></span>Registered only</span> ' ,
' <span class= " pill " ><span style= " display:inline-block;width:18px;border-top:2px dotted #98a2b3 " ></span>Weak edge</span> ' ,
] ;
legend . innerHTML = [ . . . nodeTypes , . . . statusItems ] . join ( " " ) ;
2026-05-18 17:11:36 +02:00
} ;
const boot = async ( ) = > {
const [ manifest , payload ] = await Promise . all ( [
fetch ( manifestUrl ) . then ( ( response ) = > response . json ( ) ) ,
fetch ( graphUrl ) . then ( ( response ) = > response . json ( ) )
] ) ;
2026-05-18 18:31:25 +02:00
( manifest . modes | | [ { id : " full " , label : " Full " } ] ) . forEach ( ( mode ) = > {
const option = document . createElement ( " option " ) ;
option . value = mode . id ;
option . textContent = mode . label ;
modeSelect . appendChild ( option ) ;
} ) ;
2026-05-18 20:14:07 +02:00
profilePersistence = manifest . profile_persistence | | " none " ;
2026-05-18 17:11:36 +02:00
layerColors = Object . fromEntries ( ( manifest . layers | | [ ] ) . map ( ( layer ) = > [ layer . id , layer . color ] ) ) ;
2026-05-18 23:58:58 +02:00
nodeTypeLabels = Object . fromEntries ( ( manifest . layers | | [ ] ) . map ( ( layer ) = > [ layer . id , layer . label ] ) ) ;
allNodeTypes = ( manifest . layers | | [ ] ) . map ( ( layer ) = > layer . id ) ;
allEdgeTypes = Array . from ( new Set ( ( payload . elements | | [ ] )
. map ( ( element ) = > element . data | | { } )
. filter ( ( data ) = > data . source & & data . target & & data . edgeType )
. map ( ( data ) = > data . edgeType )
) ) . sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
renderChecklist ( nodeTypeFilter , allNodeTypes , nodeTypeLabels , " node-type " ) ;
renderChecklist ( edgeTypeFilter , allEdgeTypes , { } , " edge-type " ) ;
syncFilterSummaries ( ) ;
2026-05-18 17:11:36 +02:00
renderLegend ( manifest . layers | | [ ] ) ;
2026-05-18 20:14:07 +02:00
profiles = loadProfiles ( ) ;
renderProfiles ( ) ;
2026-05-18 17:11:36 +02:00
const elements = ( payload . elements | | [ ] ) . map ( ( element ) = > ( {
. . . element ,
data : { . . . element . data , color : layerColors [ element . data . layer ] | | " #667085 " }
} ) ) ;
2026-05-19 00:51:38 +02:00
if ( elements . length == = 0 ) {
canvas . innerHTML = " <p class= ' canvas-notice ' >No graph entities were returned by the registry.</p> " ;
counts . textContent = " 0 nodes / 0 edges " ;
hiddenSummary . textContent = " Hidden 0 " ;
return ;
}
canvas . innerHTML = " " ;
2026-05-18 17:11:36 +02:00
cy = cytoscape ( {
container : canvas ,
elements ,
2026-05-18 18:31:25 +02:00
layout : { name : layoutSelect . value | | " cose " , animate : false , randomize : false } ,
2026-05-18 17:11:36 +02:00
style : [
{ selector : " node " , style : {
" background-color " : " data(color) " ,
" border-color " : " #172033 " ,
" border-width " : 1 ,
" color " : " #172033 " ,
" font-size " : 11 ,
" height " : " data(visualSize) " ,
" label " : " data(label) " ,
" text-background-color " : " #ffffff " ,
" text-background-opacity " : .85 ,
" text-background-padding " : 2 ,
" text-wrap " : " wrap " ,
" text-max-width " : 130 ,
" width " : " data(visualSize) "
} } ,
2026-05-19 00:16:57 +02:00
{ selector : " node[layer = ' repository ' ] " , style : { " shape " : " round-rectangle " } } ,
{ selector : " node[layer = ' server ' ] " , style : { " shape " : " pentagon " } } ,
{ selector : " node[layer = ' deployment ' ] " , style : { " shape " : " diamond " } } ,
{ selector : " node[layer = ' service ' ] " , style : { " shape " : " hexagon " } } ,
{ selector : " node[layer = ' capability ' ] " , style : { " shape " : " vee " } } ,
{ selector : " node[layer = ' interface ' ] " , style : { " shape " : " rectangle " } } ,
{ selector : " node[layer = ' dependency ' ] " , style : { " shape " : " triangle " } } ,
{ selector : " node[layer = ' binding ' ] " , style : { " shape " : " rhomboid " } } ,
{ selector : " node[unresolved = true] " , style : { " border-color " : " #b45309 " , " border-style " : " dashed " , " border-width " : 3 } } ,
{ selector : " node[lifecycle = ' registered-only ' ] " , style : { " border-color " : " #be123c " , " border-style " : " dashed " , " border-width " : 3 } } ,
2026-05-25 00:40:13 +02:00
{ selector : " node[zoneCollapse = true] " , style : {
" shape " : " round-rectangle " ,
" background-color " : " #334155 " ,
" border-color " : " #0f766e " ,
" border-width " : 3 ,
" border-style " : " double " ,
" color " : " #172033 " ,
" font-weight " : 700 ,
" text-background-color " : " #ffffff " ,
" text-background-opacity " : .86 ,
" text-background-padding " : 3 ,
" width " : " data(visualSize) " ,
" height " : " data(visualSize) "
} } ,
2026-05-18 17:11:36 +02:00
{ selector : " edge " , style : {
" curve-style " : " bezier " ,
" line-color " : " #98a2b3 " ,
" target-arrow-color " : " #98a2b3 " ,
" target-arrow-shape " : " triangle " ,
" width " : " data(edgeWidth) "
} } ,
{ selector : " edge[strength = ' strong ' ] " , style : { " line-color " : " #344054 " , " target-arrow-color " : " #344054 " } } ,
{ selector : " edge[strength = ' weak ' ] " , style : { " line-style " : " dotted " } } ,
2026-05-25 00:40:13 +02:00
{ selector : " edge[zoneCollapse = true] " , style : { " line-style " : " dashed " , " line-color " : " #0f766e " , " target-arrow-color " : " #0f766e " } } ,
2026-05-19 01:16:18 +02:00
{ selector : " node.rule-highlight " , style : {
" border-color " : " #2563eb " ,
" border-width " : 3 ,
" z-index " : 3
} } ,
{ selector : " edge.rule-highlight " , style : {
" line-color " : " #2563eb " ,
" target-arrow-color " : " #2563eb " ,
" width " : 5 ,
" z-index " : 3
} } ,
2026-05-19 00:16:57 +02:00
{ selector : " edge.hover " , style : {
" label " : " data(edgeType) " ,
" font-size " : 10 ,
" text-background-color " : " #ffffff " ,
" text-background-opacity " : .9 ,
" text-background-padding " : 2 ,
" z-index " : 2
} } ,
2026-05-18 17:11:36 +02:00
{ selector : " .display-blur " , style : { " opacity " : .24 , " label " : " " } } ,
2026-05-19 00:27:09 +02:00
{ selector : " .display-blur.hover " , style : { " opacity " : .78 , " label " : " data(label) " } } ,
{ selector : " .label-hidden " , style : { " label " : " " } } ,
2026-05-19 00:16:57 +02:00
{ selector : " :selected " , style : { " overlay-opacity " : 0 } }
2026-05-18 17:11:36 +02:00
]
} ) ;
2026-05-19 01:16:18 +02:00
refreshRuleBuilder ( ) ;
renderRules ( ) ;
2026-05-18 17:11:36 +02:00
cy . on ( " tap " , " node, edge " , ( event ) = > showDetails ( event . target ) ) ;
cy . on ( " tap " , ( event ) = > { if ( event . target == = cy ) showDetails ( null ) ; } ) ;
2026-05-25 02:08:45 +02:00
cy . on ( " pan zoom resize render " , ( ) = > {
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ) ;
cy . on ( " layoutstop " , ( ) = > {
applyZoneContainerLayout ( ) ;
2026-05-24 17:02:12 +02:00
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ) ;
cy . on ( " position " , " node " , ( ) = > {
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ) ;
2026-05-18 17:11:36 +02:00
cy . on ( " mouseover " , " node, edge " , ( event ) = > {
event . target . addClass ( " hover " ) ;
const data = event . target . data ( ) ;
2026-05-18 23:58:58 +02:00
const nodeType = data . layer ? nodeTypeLabels [ data . layer ] | | humanize ( data . layer ) : " " ;
popup . innerHTML = ` < strong > $ { escapeHtml ( data . name | | data . label | | data . id ) } < / strong > < span class = " meta " > $ { escapeHtml ( data . kind ) } / $ { escapeHtml ( nodeType | | data . edgeType | | " " ) } < / span > ` ;
2026-05-18 17:11:36 +02:00
popup . style . left = ` $ { Math . min ( event . renderedPosition . x + 14 , canvas . clientWidth - 270 ) } px ` ;
popup . style . top = ` $ { Math . max ( 10 , event . renderedPosition . y + 14 ) } px ` ;
popup . style . display = " block " ;
} ) ;
cy . on ( " mouseout " , " node, edge " , ( event ) = > {
event . target . removeClass ( " hover " ) ;
popup . style . display = " none " ;
} ) ;
2026-05-18 20:14:07 +02:00
const urlState = readUrlState ( ) ;
if ( urlState . profile & & profiles . some ( ( profile ) = > profile . id == = urlState . profile ) ) {
currentProfileId = urlState . profile ;
profileSelect . value = currentProfileId ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
if ( profile ) applyViewState ( profile . state | | { } , { skipLayout : true , skipUrl : true } ) ;
}
applyViewState ( urlState , { skipUrl : true } ) ;
2026-05-18 18:31:25 +02:00
runLayout ( ) ;
2026-05-18 20:14:07 +02:00
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
2026-05-18 23:58:58 +02:00
const handleFilterChange = ( ) = > {
focusSet = null ;
currentProfileId = " " ;
profileSelect . value = " " ;
applyFilters ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ;
[ searchInput , reviewFilter , unresolvedFilter ] . forEach ( ( control ) = > {
control . addEventListener ( " input " , handleFilterChange ) ;
2026-05-18 17:11:36 +02:00
} ) ;
2026-05-18 23:58:58 +02:00
nodeTypeFilter . addEventListener ( " change " , handleFilterChange ) ;
edgeTypeFilter . addEventListener ( " change " , handleFilterChange ) ;
2026-05-18 18:31:25 +02:00
modeSelect . addEventListener ( " input " , ( ) = > {
activeMode = modeSelect . value | | " full " ;
focusSet = null ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 18:31:25 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
layoutSelect . addEventListener ( " input " , ( ) = > {
currentProfileId = " " ;
profileSelect . value = " " ;
runLayout ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 18:31:25 +02:00
} ) ;
2026-05-19 00:27:09 +02:00
labelSelect . addEventListener ( " input " , ( ) = > {
activeLabelMode = labelSelect . value | | " auto " ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateLabelVisibility ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-24 17:02:12 +02:00
zoneBoundaryToggle . addEventListener ( " input " , ( ) = > {
selectedZoneId = " " ;
currentProfileId = " " ;
profileSelect . value = " " ;
renderZoneOverlay ( ) ;
if ( ! selected ) renderMapOverview ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
zoneGroupSelect . addEventListener ( " input " , ( ) = > {
activeZoneGrouping = zoneGroupSelect . value | | " deploymentEnvironment " ;
selectedZoneId = " " ;
2026-05-25 00:40:13 +02:00
collapsedZoneSnapshots = new Map ( ) ;
2026-05-24 17:02:12 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
renderZoneOverlay ( ) ;
if ( ! selected ) renderMapOverview ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-25 03:17:23 +02:00
zoneLayoutSelect . addEventListener ( " input " , ( ) = > {
activeZoneLayoutAlgorithm = normalizeZoneLayoutAlgorithm ( zoneLayoutSelect . value ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
applyZoneContainerLayout ( ) ;
fitVisibleGraph ( ) ;
renderZoneOverlay ( ) ;
if ( ! selected ) renderMapOverview ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-24 17:02:12 +02:00
zoneOverlay . addEventListener ( " click " , ( event ) = > {
2026-05-25 01:09:05 +02:00
if ( suppressZoneClick ) {
suppressZoneClick = false ;
return ;
}
2026-05-24 17:02:12 +02:00
const button = event . target . closest ( " [data-zone-id] " ) ;
if ( ! button ) return ;
const zone = zoneSummaries . get ( button . dataset . zoneId ) ;
if ( ! zone ) return ;
event . stopPropagation ( ) ;
selectedZoneId = zone . id ;
renderZoneOverlay ( ) ;
} ) ;
2026-05-25 01:09:05 +02:00
zoneOverlay . addEventListener ( " pointerdown " , ( event ) = > {
const handle = event . target . closest ( " .zone-label[data-zone-id] " ) ;
if ( ! handle | | event . button != = 0 ) return ;
startZoneDrag ( event , handle . dataset . zoneId ) ;
} ) ;
window . addEventListener ( " pointermove " , moveZoneDrag ) ;
window . addEventListener ( " pointerup " , finishZoneDrag ) ;
window . addEventListener ( " pointercancel " , finishZoneDrag ) ;
2026-05-19 01:16:18 +02:00
ruleTarget . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( { target : ruleTarget . value } ) ) ;
ruleType . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( { target : ruleTarget . value , type : ruleType . value } ) ) ;
ruleAttribute . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( {
target : ruleTarget . value ,
type : ruleType . value ,
attribute : ruleAttribute . value ,
} ) ) ;
document . querySelector ( " [data-rule-action= ' save ' ] " ) . addEventListener ( " click " , ( ) = > {
const rule = {
id : editingRuleId | | ` rule - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
target : ruleTarget . value | | " node " ,
type : ruleType . value | | " " ,
attribute : ruleAttribute . value | | " any " ,
value : ruleAttribute . value == = " any " ? " " : ruleValue . value | | " " ,
action : ruleAction . value | | " highlight " ,
} ;
filterRules = editingRuleId
? filterRules . map ( ( existing ) = > existing . id == = editingRuleId ? rule : existing )
: [ . . . filterRules , rule ] ;
editingRuleId = " " ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
document . querySelector ( " [data-rule-action= ' cancel ' ] " ) . addEventListener ( " click " , ( ) = > {
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
} ) ;
document . querySelector ( " [data-rule-action= ' reset ' ] " ) . addEventListener ( " click " , ( ) = > {
filterRules = [ ] ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
ruleList . addEventListener ( " click " , ( event ) = > {
const button = event . target . closest ( " [data-rule-list-action] " ) ;
if ( ! button ) return ;
const item = button . closest ( " [data-rule-id] " ) ;
const index = filterRules . findIndex ( ( rule ) = > rule . id == = item ? . dataset ? . ruleId ) ;
if ( index < 0 ) return ;
const action = button . dataset . ruleListAction ;
if ( action == = " edit " ) {
const rule = filterRules [ index ] ;
editingRuleId = rule . id ;
refreshRuleBuilder ( rule ) ;
renderRules ( ) ;
return ;
}
if ( action == = " delete " ) {
filterRules = filterRules . filter ( ( _ , ruleIndex ) = > ruleIndex != = index ) ;
} else if ( action == = " up " & & index > 0 ) {
[ filterRules [ index - 1 ] , filterRules [ index ] ] = [ filterRules [ index ] , filterRules [ index - 1 ] ] ;
} else if ( action == = " down " & & index < filterRules . length - 1 ) {
[ filterRules [ index + 1 ] , filterRules [ index ] ] = [ filterRules [ index ] , filterRules [ index + 1 ] ] ;
}
editingRuleId = " " ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-19 01:46:23 +02:00
orientationActions . addEventListener ( " click " , ( event ) = > {
const button = event . target . closest ( " [data-orientation-action] " ) ;
if ( ! button ) return ;
2026-05-25 00:40:13 +02:00
if ( button . dataset . orientationAction == = " collapse-zone " ) {
const zone = zoneSummaries . get ( button . dataset . zoneId | | selectedZoneId ) ;
if ( zone ) collapseZone ( zone ) ;
return ;
}
if ( button . dataset . orientationAction == = " expand-zone " ) {
expandZone ( button . dataset . zoneId | | selected ? . data ( " collapsedZoneId " ) | | selectedZoneId ) ;
return ;
}
2026-05-19 01:46:23 +02:00
applyOrientationContext ( button . dataset . orientationAction ) ;
} ) ;
2026-05-18 17:11:36 +02:00
document . querySelector ( " [data-action= ' fit ' ] " ) . addEventListener ( " click " , ( ) = > cy & & cy . fit ( cy . elements ( " :visible " ) , 48 ) ) ;
document . querySelector ( " [data-action= ' focus ' ] " ) . addEventListener ( " click " , applyFocus ) ;
document . querySelector ( " [data-action= ' clear ' ] " ) . addEventListener ( " click " , ( ) = > {
searchInput . value = " " ;
2026-05-18 18:31:25 +02:00
modeSelect . value = " full " ;
activeMode = " full " ;
2026-05-19 00:27:09 +02:00
labelSelect . value = " auto " ;
activeLabelMode = " auto " ;
2026-05-24 17:02:12 +02:00
zoneBoundaryToggle . checked = true ;
zoneGroupSelect . value = " deploymentEnvironment " ;
activeZoneGrouping = " deploymentEnvironment " ;
2026-05-25 03:17:23 +02:00
zoneLayoutSelect . value = " compact-grid " ;
activeZoneLayoutAlgorithm = " compact-grid " ;
2026-05-25 00:27:10 +02:00
activeZoneDefinitionSet = " fabric-default " ;
2026-05-24 17:02:12 +02:00
selectedZoneId = " " ;
2026-05-25 00:40:13 +02:00
collapsedZoneSnapshots = new Map ( ) ;
2026-05-18 23:58:58 +02:00
setCheckedValues ( nodeTypeFilter ) ;
setCheckedValues ( edgeTypeFilter ) ;
2026-05-18 17:11:36 +02:00
reviewFilter . value = " " ;
unresolvedFilter . value = " " ;
focusSet = null ;
2026-05-18 20:14:07 +02:00
manualOverrides = { } ;
2026-05-19 01:16:18 +02:00
filterRules = [ ] ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-19 01:16:18 +02:00
applyFilters ( { redrawOnRemove : true } ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ) ;
document . querySelector ( " [data-action= ' reset-overrides ' ] " ) . addEventListener ( " click " , ( ) = > {
manualOverrides = { } ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ) ;
document . querySelectorAll ( " [data-override] " ) . forEach ( ( button ) = > {
button . addEventListener ( " click " , ( ) = > {
if ( ! selected ) return ;
2026-05-18 20:14:07 +02:00
manualOverrides [ overrideKey ( selected ) ] = button . dataset . override ;
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
} ) ;
profileSelect . addEventListener ( " input " , ( ) = > {
currentProfileId = profileSelect . value ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
if ( profile ) applyViewState ( profile . state | | { } ) ;
if ( ! profile ) updateUrlState ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
} ) ;
document . querySelectorAll ( " [data-profile-action] " ) . forEach ( ( button ) = > {
button . addEventListener ( " click " , async ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return ;
const action = button . dataset . profileAction ;
if ( action == = " save " ) {
saveCurrentProfile ( profileNameInput . value ) ;
} else if ( action == = " duplicate " ) {
const profile = selectedProfile ( ) ;
if ( ! profile ) return ;
saveCurrentProfile ( ` Copy of $ { profile . name } ` , { . . . profile , state : currentViewState ( ) } ) ;
} else if ( action == = " delete " ) {
profiles = profiles . filter ( ( profile ) = > profile . id != = currentProfileId ) ;
currentProfileId = " " ;
persistProfiles ( ) ;
renderProfiles ( ) ;
updateUrlState ( ) ;
} else if ( action == = " copy " ) {
const url = ` $ { window . location . origin } $ { viewUrl ( true ) } ` ;
if ( window . navigator ? . clipboard ? . writeText ) await window . navigator . clipboard . writeText ( url ) ;
updateProfileSummary ( " Copied the current map state URL. " ) ;
}
2026-05-18 17:11:36 +02:00
} ) ;
} ) ;
2026-05-19 00:51:38 +02:00
document . querySelectorAll ( " [data-help] " ) . forEach ( ( target ) = > {
target . addEventListener ( " mouseenter " , ( ) = > showHelp ( target ) ) ;
target . addEventListener ( " focus " , ( ) = > showHelp ( target ) ) ;
target . addEventListener ( " mouseleave " , hideHelp ) ;
target . addEventListener ( " blur " , hideHelp ) ;
} ) ;
document . addEventListener ( " keydown " , ( event ) = > {
if ( event . key == = " Escape " ) hideHelp ( ) ;
} ) ;
2026-05-18 17:11:36 +02:00
if ( ! window . cytoscape ) {
canvas . innerHTML = " <p class= ' notice ' >Cytoscape.js could not be loaded.</p> " ;
return ;
}
boot ( ) . catch ( ( error ) = > {
2026-05-19 00:51:38 +02:00
canvas . innerHTML = ` < p class = ' canvas-notice ' > Could not load graph explorer data : $ { escapeHtml ( error . message ) } < / p > ` ;
2026-05-18 17:11:36 +02:00
} ) ;
} ) ( ) ;
< / script >
< / body >
< / html >
"""