servicemap 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -85,7 +85,7 @@ function pageCss() {
|
|
|
85
85
|
.intro span { display: block; margin-top: 8px; color: #53665d; font-size: 14px; }
|
|
86
86
|
.edges { position: absolute; inset: 0; z-index: 26; width: 100%; height: 100%; pointer-events: none; }
|
|
87
87
|
.edge-line { transition: opacity 180ms ease, stroke 180ms ease; }
|
|
88
|
-
.edge-line.is-active { stroke: #7ee2b8; stroke-width: 2.4px; stroke-dasharray:
|
|
88
|
+
.edge-line.is-active { stroke: #7ee2b8; stroke-width: 2.4px; stroke-dasharray: 1 9; stroke-linecap: round; animation: servicemapDash 640ms linear infinite; filter: drop-shadow(0 0 5px rgba(126,226,184,.9)) drop-shadow(0 0 16px rgba(93,224,165,.42)); opacity: 1; }
|
|
89
89
|
.edge-line.is-muted { opacity: 0.12; }
|
|
90
90
|
.hub { position: absolute; left: 50%; top: 50%; z-index: 20; width: 132px; height: 132px; transform: translate(-50%, -50%); border-radius: 24px; border: 1px solid #315c47; background: #09120d; display: grid; place-items: center; box-shadow: 0 0 0 9px rgba(79,183,128,0.12), 0 0 42px rgba(100,255,174,0.16); }
|
|
91
91
|
button.hub { cursor: pointer; }
|
|
@@ -95,10 +95,11 @@ function pageCss() {
|
|
|
95
95
|
button.service-node { cursor: grab; touch-action: none; user-select: none; color: inherit; font: inherit; text-align: left; }
|
|
96
96
|
button.service-node:active { cursor: grabbing; }
|
|
97
97
|
.service-node img { pointer-events: none; }
|
|
98
|
-
.service-node:hover, .service-node.is-selected { border-color: #66efae; box-shadow: 0 0 0 2px rgba(101,240,173,0.42), 0 18px 44px rgba(0,0,0,0.38); }
|
|
98
|
+
.service-node:hover, .service-node.is-selected, .service-node.is-highlighted-child { border-color: #66efae; box-shadow: 0 0 0 2px rgba(101,240,173,0.42), 0 18px 44px rgba(0,0,0,0.38); }
|
|
99
|
+
.service-node.is-highlighted-child { box-shadow: 0 0 0 1px rgba(126,226,184,.36), 0 18px 44px rgba(0,0,0,0.38), 0 0 34px rgba(126,226,184,.18); }
|
|
99
100
|
.map.has-selection .service-node { opacity: 0.16; }
|
|
100
101
|
.map.has-selection .service-node.is-related { opacity: 0.48; }
|
|
101
|
-
.map.has-selection .service-node.is-selected { opacity: 1; }
|
|
102
|
+
.map.has-selection .service-node.is-selected, .map.has-selection .service-node.is-highlighted-child { opacity: 1; }
|
|
102
103
|
.service-node.is-selected { z-index: 27; }
|
|
103
104
|
.logo { width: 48px; height: 48px; flex: 0 0 auto; border-radius: 8px; display: grid; place-items: center; overflow: hidden; color: #fff; font-weight: 700; }
|
|
104
105
|
.logo img { width: 28px; height: 28px; object-fit: contain; filter: invert(1); }
|
|
@@ -165,7 +166,7 @@ function pageCss() {
|
|
|
165
166
|
.graph-3d__zoom button + button { border-top: 1px solid rgba(255,255,255,.08); }
|
|
166
167
|
.graph-3d__zoom button:hover { color: #fff; background: rgba(255,255,255,.08); }
|
|
167
168
|
.graph-3d__loading { position: absolute; inset: 0; display: grid; place-items: center; color: #75857c; font-size: 9px; }
|
|
168
|
-
@keyframes servicemapDash {
|
|
169
|
+
@keyframes servicemapDash { to { stroke-dashoffset: -10; } }
|
|
169
170
|
@media (prefers-reduced-motion: reduce) { .edge-line.is-active { animation: none; } }
|
|
170
171
|
@keyframes servicemapLinkFlow { to { background-position: 22px 0; } }
|
|
171
172
|
@media (max-width: 920px) {
|
|
@@ -339,15 +340,27 @@ function collectDescendantIds(nodes: ServiceNode[], selectedId: string | null) {
|
|
|
339
340
|
return ids;
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
function
|
|
343
|
+
function collectParentIds(nodes: ServiceNode[], selectedId: string | null) {
|
|
344
|
+
const ids = new Set<string>();
|
|
345
|
+
if (!selectedId) return ids;
|
|
346
|
+
nodes.forEach((node) => {
|
|
347
|
+
const parentId = node.parentId ?? 'project';
|
|
348
|
+
if (node.id === selectedId) ids.add(parentId);
|
|
349
|
+
});
|
|
350
|
+
return ids;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isActiveTreeEdge(sourceId: string, targetId: string, selectedId: string | null, parentIds: Set<string>, descendantIds: Set<string>) {
|
|
343
354
|
if (!selectedId) return false;
|
|
344
355
|
if (sourceId === selectedId && descendantIds.has(targetId)) return true;
|
|
345
|
-
|
|
356
|
+
if (descendantIds.has(sourceId) && descendantIds.has(targetId)) return true;
|
|
357
|
+
return parentIds.has(sourceId) && targetId === selectedId;
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
function Connections({ nodes, selectedId }: { nodes: ServiceNode[]; selectedId: string | null }) {
|
|
349
361
|
const ref = useRef<SVGSVGElement>(null);
|
|
350
362
|
const [size, setSize] = useState({ width: 0, height: 0, nodeHalfWidth: 125 });
|
|
363
|
+
const parentIds = useMemo(() => collectParentIds(nodes, selectedId), [nodes, selectedId]);
|
|
351
364
|
const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
|
|
352
365
|
useEffect(() => {
|
|
353
366
|
if (!ref.current) return;
|
|
@@ -372,7 +385,7 @@ function Connections({ nodes, selectedId }: { nodes: ServiceNode[]; selectedId:
|
|
|
372
385
|
const dy = Math.sign(y2 - y1);
|
|
373
386
|
const radius = Math.min(5, Math.abs(x2 - x1) / 4, Math.abs(y2 - y1) / 2);
|
|
374
387
|
const path = \`M \${x1} \${y1} H \${middle - dx * radius} Q \${middle} \${y1} \${middle} \${y1 + dy * radius} V \${y2 - dy * radius} Q \${middle} \${y2} \${middle + dx * radius} \${y2} H \${x2}\`;
|
|
375
|
-
const active = isActiveTreeEdge(node.parentId ?? 'project', node.id, selectedId, descendantIds);
|
|
388
|
+
const active = isActiveTreeEdge(node.parentId ?? 'project', node.id, selectedId, parentIds, descendantIds);
|
|
376
389
|
return <path key={node.id} d={path} fill="none" stroke="rgba(103,130,118,.48)" strokeWidth="1.2" className={\`edge-line \${active ? 'is-active' : selectedId ? 'is-muted' : ''}\`} />;
|
|
377
390
|
})}
|
|
378
391
|
</svg>;
|
|
@@ -649,7 +662,7 @@ function AddServiceModal({
|
|
|
649
662
|
placeholder="billing, hosting, analytics"
|
|
650
663
|
/>
|
|
651
664
|
|
|
652
|
-
<label className="field-label" htmlFor="new-service-parent">Parent
|
|
665
|
+
<label className="field-label" htmlFor="new-service-parent">Parent service</label>
|
|
653
666
|
<select
|
|
654
667
|
id="new-service-parent"
|
|
655
668
|
className="details-input"
|
|
@@ -713,16 +726,17 @@ function ServiceGraph3D({
|
|
|
713
726
|
label: node.confidence === 'high' ? relationshipLabel(node.parentId) : \`may \${relationshipLabel(node.parentId)}\`,
|
|
714
727
|
})),
|
|
715
728
|
}), [nodes]);
|
|
729
|
+
const parentIds = useMemo(() => collectParentIds(nodes, selectedId), [nodes, selectedId]);
|
|
716
730
|
const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
|
|
717
731
|
const activeTreeLinkIds = useMemo(() => {
|
|
718
732
|
const ids = new Set<string>();
|
|
719
733
|
graphData.links.forEach((link) => {
|
|
720
734
|
const source = endpointId(link.source);
|
|
721
735
|
const target = endpointId(link.target);
|
|
722
|
-
if (isActiveTreeEdge(source, target, selectedId, descendantIds)) ids.add(link.id);
|
|
736
|
+
if (isActiveTreeEdge(source, target, selectedId, parentIds, descendantIds)) ids.add(link.id);
|
|
723
737
|
});
|
|
724
738
|
return ids;
|
|
725
|
-
}, [descendantIds, graphData.links, selectedId]);
|
|
739
|
+
}, [descendantIds, graphData.links, parentIds, selectedId]);
|
|
726
740
|
|
|
727
741
|
useEffect(() => {
|
|
728
742
|
const element = containerRef.current;
|
|
@@ -852,7 +866,7 @@ function ServiceGraph3D({
|
|
|
852
866
|
<button type="button" onClick={() => zoom(1.5)} title="Zoom out" aria-label="Zoom out">−</button>
|
|
853
867
|
</div>
|
|
854
868
|
<div className="graph-3d__help" aria-hidden="true">Drag to rotate <span /> Scroll to zoom <span /> Click a service</div>
|
|
855
|
-
{selectedId && <div className="graph-3d__focus" aria-hidden="true">{activeTreeLinkIds.size}
|
|
869
|
+
{selectedId && <div className="graph-3d__focus" aria-hidden="true">{activeTreeLinkIds.size} connections highlighted</div>}
|
|
856
870
|
</div>
|
|
857
871
|
);
|
|
858
872
|
}
|
|
@@ -931,6 +945,7 @@ export default function InternalServiceMapPage() {
|
|
|
931
945
|
])));
|
|
932
946
|
const selected = nodes.find((service) => service.id === selectedId) ?? null;
|
|
933
947
|
const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
|
|
948
|
+
const highlightedChildIds = useMemo(() => selectedId === 'project' ? descendantIds : new Set<string>(), [descendantIds, selectedId]);
|
|
934
949
|
const zoomIn = () => setZoom((current) => Math.min(1.45, current * 1.5));
|
|
935
950
|
const zoomOut = () => setZoom((current) => current / 1.5);
|
|
936
951
|
const resetZoom = () => {
|
|
@@ -1014,7 +1029,10 @@ export default function InternalServiceMapPage() {
|
|
|
1014
1029
|
<button type="button" className={\`mode-button \${mode === '3d' ? 'is-active' : ''}\`} onClick={() => setMode('3d')}>3D</button>
|
|
1015
1030
|
</div>
|
|
1016
1031
|
<button type="button" className="center-button" onClick={centerMap} aria-label="Center graph" title="Center graph"><span className="center-icon" aria-hidden="true" /></button>
|
|
1017
|
-
<button type="button" className="add-service-button" onClick={() =>
|
|
1032
|
+
<button type="button" className="add-service-button" onClick={() => {
|
|
1033
|
+
setNewService((current) => ({ ...current, parentId: selectedId && selectedId !== 'project' ? selectedId : 'product' }));
|
|
1034
|
+
setIsAddingService(true);
|
|
1035
|
+
}}>+ Add service</button>
|
|
1018
1036
|
</div>
|
|
1019
1037
|
</header>
|
|
1020
1038
|
|
|
@@ -1035,12 +1053,13 @@ export default function InternalServiceMapPage() {
|
|
|
1035
1053
|
<Connections nodes={nodes} selectedId={selectedId} />
|
|
1036
1054
|
<button type="button" className={\`hub \${selectedId === 'project' ? 'is-selected' : ''}\`} onClick={() => setSelectedId('project')} aria-label={\`Trace \${graph.project?.name ?? 'Project'} child services\`}><div className="hub-inner"><ProjectMark /></div></button>
|
|
1037
1055
|
{nodes.map((service) => {
|
|
1038
|
-
const
|
|
1056
|
+
const isHighlightedChild = highlightedChildIds.has(service.id);
|
|
1057
|
+
const isRelated = descendantIds.has(service.id) || selected?.parentId === service.id || isHighlightedChild;
|
|
1039
1058
|
return (
|
|
1040
1059
|
<button
|
|
1041
1060
|
type="button"
|
|
1042
1061
|
key={service.id}
|
|
1043
|
-
className={\`service-node \${selectedId === service.id ? 'is-selected' : ''} \${isRelated ? 'is-related' : ''}\`}
|
|
1062
|
+
className={\`service-node \${selectedId === service.id ? 'is-selected' : ''} \${isRelated ? 'is-related' : ''} \${isHighlightedChild ? 'is-highlighted-child' : ''}\`}
|
|
1044
1063
|
style={{ left: \`\${service.x}%\`, top: \`\${service.y}%\` }}
|
|
1045
1064
|
onPointerDown={(event) => startNodeDrag(event, service)}
|
|
1046
1065
|
onPointerMove={moveNodeDrag}
|