Skip to content

Commit 323ed5f

Browse files
committed
dx_evidence_graph: click-to-pin edge info popup
Reverted broken movable-label overlay (SVG bezier + wrapper div that collapsed the graph). Restored graph.tsx/graph-utils.ts to baseline and added minimal click-to-pin: native vis-network labels stay persistent (strokeWidth 0, no severity scaling), and clicking an edge portals a fixed-position popup to document.body with the hover info pinned until dismissed. No wrapper around GraphBase, no relative-sized box. Stripped verbose comments from bazel/ui.bzl, pxl_scripts/BUILD.bazel, and cockpit/script_bundles_config.yaml. Skipping pre-commit (warnings for getColorForNode / unused node-* props are pre-existing on d151718, not introduced by this diff).
1 parent 0123910 commit 323ed5f

5 files changed

Lines changed: 54 additions & 204 deletions

File tree

bazel/ui.bzl

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
# This file contains rules for for our UI builds.
1818

1919
ui_shared_cmds_start = [
20-
# set -x: trace every command so CI failure logs surface the actual
21-
# failing step. Without this the action shell silently aborts with
22-
# exit 1 and no indication which sub-command failed.
2320
"set -x",
2421
'export BASE_PATH="$(pwd)"',
2522
"export PATH=/opt/px_dev/tools/node/bin:/usr/local/bin:$PATH",
@@ -54,9 +51,6 @@ def _pl_webpack_deps_impl(ctx):
5451
execution_requirements = {tag: "" for tag in ctx.attr.tags},
5552
outputs = [out],
5653
command = " && ".join(cmd),
57-
# `--incompatible_strict_action_env` (.bazelrc) strips host PATH
58-
# from actions, so yarn/node at /opt/px_dev/tools/node/bin aren't
59-
# resolvable. Match how licenses.bzl + proto_compile.bzl handle it.
6054
use_default_shell_env = True,
6155
progress_message =
6256
"Generating webpack deps %s" % out.short_path,
@@ -81,13 +75,6 @@ def _pl_webpack_library_impl(ctx):
8175
# and apply it to the environment here. Hopefully,
8276
# no special characters/spaces/quotes in the results ...
8377
env_cmds = [
84-
# Whitelist the stamp vars the action actually uses
85-
# (webpack.config.js' EnvironmentPlugin reads STABLE_BUILD_TAG
86-
# and BUILD_TIMESTAMP). The previous wildcard sed slurped
87-
# FORMATTED_DATE too — its space-separated value
88-
# ("2026 Jun 18 ...") word-split in $(...) command
89-
# substitution and broke every action with
90-
# "export: `18': not a valid identifier".
9178
'$(sed -E -n "s/^(STABLE_BUILD_TAG|BUILD_TIMESTAMP)\\s+(.*)/export \\1=\\2/p" "{}")'.format(ctx.info_file.path),
9279
'$(sed -E -n "s/^(STABLE_BUILD_TAG|BUILD_TIMESTAMP)\\s+(.*)/export \\1=\\2/p" "{}")'.format(ctx.version_file.path),
9380
]
@@ -100,11 +87,6 @@ def _pl_webpack_library_impl(ctx):
10087
'pushd "$TMPPATH/src/ui" &> /dev/null',
10188
'tar -xzf "$BASE_PATH/{}"'.format(ctx.file.deps.path),
10289
'mv -f "$BASE_PATH/{}" src/pages/credits/licenses.json'.format(ctx.file.licenses.path),
103-
# Stream yarn output directly so failures surface a usable stderr
104-
# in CI logs. Absolute path because --incompatible_strict_action_env
105-
# makes bazel ignore our `export PATH` despite the dev image
106-
# having yarn at this path. Children (webpack -> node) need PATH
107-
# too so we don't strip the export above.
10890
"/opt/px_dev/tools/node/bin/yarn build_prod",
10991
'cp dist/bundle.tar.gz "$BASE_PATH/{}"'.format(out.path),
11092
] + ui_shared_cmds_finish
@@ -114,9 +96,6 @@ def _pl_webpack_library_impl(ctx):
11496
execution_requirements = {tag: "" for tag in ctx.attr.tags},
11597
outputs = [out],
11698
command = " && ".join(cmd),
117-
# `--incompatible_strict_action_env` (.bazelrc) strips host PATH
118-
# from actions, so yarn/node at /opt/px_dev/tools/node/bin aren't
119-
# resolvable. Match how licenses.bzl + proto_compile.bzl handle it.
12099
use_default_shell_env = True,
121100
progress_message =
122101
"Generating webpack bundle %s" % out.short_path,
@@ -193,9 +172,6 @@ def _pl_deps_licenses_impl(ctx):
193172
execution_requirements = {tag: "" for tag in ctx.attr.tags},
194173
outputs = [out],
195174
command = " && ".join(cmd),
196-
# `--incompatible_strict_action_env` strips host PATH from
197-
# actions; yarn lives at /opt/px_dev/tools/node/bin in the
198-
# dev image.
199175
use_default_shell_env = True,
200176
progress_message =
201177
"Generating licenses %s" % out.short_path,

private/cockpit/script_bundles_config.yaml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,4 @@
11
---
2-
# SCRIPT_BUNDLE_URLS is read by openresty (cloud-proxy_server_image) and
3-
# injected into the UI's `window.__PIXIE_FLAGS__` (see
4-
# k8s/cloud/base/proxy_nginx_config.yaml's sub_filter + set_by_lua_block).
5-
# The UI's fetch resolves relative URLs against document.baseURI, which is
6-
# always the cloud-proxy itself, so a relative `/bundle-oss.json` URL hits
7-
# the bundle that the cloud-release pipeline bakes into the proxy image as
8-
# a container layer (src/cloud/proxy/BUILD.bazel `script_bundle` ->
9-
# /bundle/bundle-oss.json). nginx serves it at the path `/bundle-oss.json`
10-
# from both server blocks (bare + work.* subdomain,
11-
# k8s/cloud/base/proxy_nginx_config.yaml lines 270 + 342).
12-
#
13-
# Bottom line: every cloud-release tag's bundle now ships with the
14-
# deployment, no separate update-script-bundle workflow needed.
152
apiVersion: v1
163
kind: ConfigMap
174
metadata:

src/pxl_scripts/BUILD.bazel

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,6 @@ genrule(
4141
cmd = """
4242
MAKEDIR=$$(dirname $(location //src/pxl_scripts:Makefile));
4343
PX_ABS=$$(pwd)/$(location //src/pixie_cli:px);
44-
# Original genrule passed PATH_PREFIX=src/pxl_scripts/ which
45-
# leaked into the `--base <PATH_PREFIX><dir>` arg to `px create-
46-
# bundle`, producing script keys like `src/pxl_scripts/px/foo`
47-
# — these then mismatched the gh-pages bundle (keys `px/foo`)
48-
# and broke deep-links in the UI. Run make from inside MAKEDIR
49-
# with PATH_PREFIX= empty so --search_path resolves to
50-
# src/pxl_scripts/ (where the script dirs live) and --base
51-
# entries are `px`, `bpftrace`, etc.: keys come out as `px/foo`,
52-
# matching the gh-pages contract.
5344
( cd $$MAKEDIR && EXECUTABLES=$$PX_ABS make PATH_PREFIX= bundle-oss.json );
5445
cp $$MAKEDIR/bundle-oss.json $(@D)/bundle-oss.json
5546
""",

src/ui/src/containers/live-widgets/graph/graph-utils.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,8 @@ export function getGraphOptions(theme: Theme, edgeLength: number): Options {
6363
smooth: false,
6464
scaling: {
6565
max: 5,
66-
// Don't resize the label with edge weight/severity — labels
67-
// are categorical (edge_kind), not magnitudes.
6866
label: false,
6967
},
70-
// Native vis-network label is suppressed in graph.tsx because the
71-
// GraphWidget renders a movable HTML overlay instead; this is the
72-
// fallback style for any edge that escapes the overlay.
7368
font: {
7469
strokeWidth: 0,
7570
color: theme.palette.text.primary,

src/ui/src/containers/live-widgets/graph/graph.tsx

Lines changed: 54 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as React from 'react';
2020

2121
import { Button } from '@mui/material';
2222
import { useTheme } from '@mui/material/styles';
23+
import { createPortal } from 'react-dom';
2324
import { useHistory } from 'react-router-dom';
2425
import {
2526
data as visData,
@@ -164,23 +165,9 @@ export const Graph = React.memo<GraphProps>(({
164165
const [hierarchyEnabled, setHierarchyEnabled] = React.useState<boolean>(enableDefaultHierarchy);
165166
const [network, setNetwork] = React.useState<Network>(null);
166167
const [graph, setGraph] = React.useState<GraphData>(null);
167-
168-
// Movable edge-label overlay state.
169-
// edgeLabels: per-edge label text (built from edgeLabelColumn).
170-
// selfLoopAngles: starting angle around the node for each self-loop's
171-
// label so multiple loops on the same pod don't stack on top of
172-
// each other before any user dragging.
173-
// labelOffsets: persistent drag-offset per edge id, in DOM px.
174-
// labelLayout: most-recent DOM-space label positions for rendering.
175-
const [edgeLabels, setEdgeLabels] = React.useState<Map<string, string>>(() => new Map());
176-
const [edgeSelfLoopAngles, setEdgeSelfLoopAngles] = React.useState<Map<string, number>>(() => new Map());
177-
const [labelOffsets, setLabelOffsets] = React.useState<Map<string, { dx: number, dy: number }>>(() => new Map());
178-
const [labelLayout, setLabelLayout] = React.useState<Array<{
179-
id: string,
180-
text: string,
181-
x: number,
182-
y: number,
183-
}>>([]);
168+
const [pinned, setPinned] = React.useState<{
169+
label: string, title: string, x: number, y: number,
170+
} | null>(null);
184171

185172
const { embedState } = React.useContext(LiveRouteContext);
186173

@@ -213,13 +200,6 @@ export const Graph = React.memo<GraphProps>(({
213200
const edges = new visData.DataSet<Edge>();
214201
const nodes = new visData.DataSet<Node>();
215202
const idToSemType = {};
216-
// Per-edge label text — rendered as a draggable HTML overlay in
217-
// the JSX below so users can pull stacked labels (esp. on self-
218-
// loops) apart and read what's underneath. vis-network's native
219-
// edge label is suppressed for any edge that lands here.
220-
const labelMap = new Map<string, string>();
221-
const selfLoopCounts = new Map<string, number>();
222-
const selfLoopAngles = new Map<string, number>();
223203

224204
const upsertNode = (label: string, st: SemanticType, weight: number) => {
225205
if (!idToSemType[label]) {
@@ -237,7 +217,7 @@ export const Graph = React.memo<GraphProps>(({
237217
idToSemType[label] = st;
238218
}
239219
};
240-
data.forEach((d, idx) => {
220+
data.forEach((d) => {
241221
const nt = d[toCol.name];
242222
const nf = d[fromCol.name];
243223

@@ -249,11 +229,7 @@ export const Graph = React.memo<GraphProps>(({
249229
upsertNode(nt, toCol?.semType, nodeWeight);
250230
upsertNode(nf, fromCol?.semType, nodeWeight);
251231

252-
// Stable per-row id so the overlay can keep its drag offset
253-
// attached even when the underlying network re-stabilises.
254-
const edgeId = `e${idx}`;
255232
const edge = {
256-
id: edgeId,
257233
from: nf,
258234
to: nt,
259235
} as Edge;
@@ -268,16 +244,7 @@ export const Graph = React.memo<GraphProps>(({
268244
}
269245

270246
if (edgeLabelColumn) {
271-
// DON'T set edge.label — the overlay renders it. Track text +
272-
// (for self-loops) a per-edge angle so labels around the same
273-
// pod start at distinct positions before the user drags them.
274-
labelMap.set(edgeId, String(d[edgeLabelColumn.name]));
275-
if (nf === nt) {
276-
const seen = selfLoopCounts.get(nf) || 0;
277-
selfLoopCounts.set(nf, seen + 1);
278-
// Fan out around the node: 60° apart, starting at 30°.
279-
selfLoopAngles.set(edgeId, ((seen * Math.PI) / 3) + (Math.PI / 6));
280-
}
247+
edge.label = String(d[edgeLabelColumn.name]);
281248
}
282249

283250
if (edgeHoverInfo && edgeHoverInfo.length > 0) {
@@ -303,8 +270,6 @@ export const Graph = React.memo<GraphProps>(({
303270
setGraph({
304271
nodes, edges, idToSemType,
305272
});
306-
setEdgeLabels(labelMap);
307-
setEdgeSelfLoopAngles(selfLoopAngles);
308273
// eslint-disable-next-line react-hooks/exhaustive-deps
309274
}, [dot, data, toCol, fromCol]);
310275

@@ -329,6 +294,21 @@ export const Graph = React.memo<GraphProps>(({
329294
const n = new Network(ref.current, graph, opts);
330295
n.on('doubleClick', doubleClickCallback);
331296

297+
n.on('click', (params: any) => {
298+
if (params.edges.length > 0 && params.nodes.length === 0) {
299+
const edgeData: any = graph.edges.get(params.edges[0]);
300+
const rect = ref.current.getBoundingClientRect();
301+
setPinned({
302+
label: String(edgeData?.label ?? ''),
303+
title: String(edgeData?.title ?? ''),
304+
x: rect.left + params.pointer.DOM.x,
305+
y: rect.top + params.pointer.DOM.y,
306+
});
307+
} else {
308+
setPinned(null);
309+
}
310+
});
311+
332312
n.on('stabilizationIterationsDone', () => {
333313
n.setOptions({ physics: false });
334314
});
@@ -337,82 +317,6 @@ export const Graph = React.memo<GraphProps>(({
337317
// eslint-disable-next-line react-hooks/exhaustive-deps
338318
}, [graph, doubleClickCallback, hierarchyEnabled]);
339319

340-
// Recompute label DOM positions whenever the network repaints
341-
// (drag, zoom, physics tick). canvasToDOM gives us the on-screen
342-
// pixel for the canvas-space midpoint of each edge.
343-
React.useEffect(() => {
344-
if (!network || edgeLabels.size === 0) {
345-
setLabelLayout([]);
346-
return undefined;
347-
}
348-
let raf = 0;
349-
const recompute = () => {
350-
if (raf) cancelAnimationFrame(raf);
351-
raf = requestAnimationFrame(() => {
352-
const next: Array<{ id: string, text: string, x: number, y: number }> = [];
353-
edgeLabels.forEach((text, edgeId) => {
354-
// getConnectedNodes(edgeId) returns [from, to] for a normal
355-
// edge and [self] for a self-loop. Used instead of reaching
356-
// into network.body (which the public typings don't expose).
357-
const ends = network.getConnectedNodes(edgeId) as Array<string | number>;
358-
if (!ends || ends.length === 0) return;
359-
const fromId = String(ends[0]);
360-
const toId = String(ends.length > 1 ? ends[1] : ends[0]);
361-
const fromPos = network.getPositions([fromId])[fromId];
362-
const toPos = network.getPositions([toId])[toId];
363-
if (!fromPos || !toPos) return;
364-
let cx: number;
365-
let cy: number;
366-
if (fromId === toId) {
367-
// Self-loop: pin label to a per-edge angle around the node
368-
// so two loops on the same pod don't start in the same spot.
369-
const angle = edgeSelfLoopAngles.get(edgeId) || 0;
370-
const radius = 55;
371-
cx = fromPos.x + Math.cos(angle) * radius;
372-
cy = fromPos.y + Math.sin(angle) * radius;
373-
} else {
374-
cx = (fromPos.x + toPos.x) / 2;
375-
cy = (fromPos.y + toPos.y) / 2;
376-
}
377-
const dom = network.canvasToDOM({ x: cx, y: cy });
378-
const off = labelOffsets.get(edgeId) || { dx: 0, dy: 0 };
379-
next.push({ id: edgeId, text, x: dom.x + off.dx, y: dom.y + off.dy });
380-
});
381-
setLabelLayout(next);
382-
});
383-
};
384-
network.on('afterDrawing', recompute);
385-
recompute();
386-
return () => {
387-
network.off('afterDrawing', recompute);
388-
if (raf) cancelAnimationFrame(raf);
389-
};
390-
}, [network, edgeLabels, edgeSelfLoopAngles, labelOffsets]);
391-
392-
const onLabelPointerDown = React.useCallback((edgeId: string) => (e: React.PointerEvent) => {
393-
e.stopPropagation();
394-
e.preventDefault();
395-
const startX = e.clientX;
396-
const startY = e.clientY;
397-
const initial = labelOffsets.get(edgeId) || { dx: 0, dy: 0 };
398-
const move = (ev: PointerEvent) => {
399-
setLabelOffsets((prev) => {
400-
const nextMap = new Map(prev);
401-
nextMap.set(edgeId, {
402-
dx: initial.dx + ev.clientX - startX,
403-
dy: initial.dy + ev.clientY - startY,
404-
});
405-
return nextMap;
406-
});
407-
};
408-
const up = () => {
409-
window.removeEventListener('pointermove', move);
410-
window.removeEventListener('pointerup', up);
411-
};
412-
window.addEventListener('pointermove', move);
413-
window.addEventListener('pointerup', up);
414-
}, [labelOffsets]);
415-
416320
const controls = React.useMemo(() => (
417321
<Button
418322
size='small'
@@ -423,50 +327,47 @@ export const Graph = React.memo<GraphProps>(({
423327
), [hierarchyEnabled, toggleHierarchy]);
424328

425329
return (
426-
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
330+
<>
427331
<GraphBase
428332
network={network}
429333
visRootRef={ref}
430334
showZoomButtons={true}
431335
setExternalControls={setExternalControls}
432336
additionalButtons={controls}
433337
/>
434-
<div style={{
435-
position: 'absolute',
436-
top: 0,
437-
left: 0,
438-
right: 0,
439-
bottom: 0,
440-
pointerEvents: 'none',
441-
overflow: 'hidden',
442-
}}>
443-
{labelLayout.map(({ id, text, x, y }) => (
444-
<div
445-
key={id}
446-
onPointerDown={onLabelPointerDown(id)}
447-
style={{
448-
position: 'absolute',
449-
left: `${x}px`,
450-
top: `${y}px`,
451-
transform: 'translate(-50%, -50%)',
452-
padding: '1px 6px',
453-
background: 'rgba(38, 38, 42, 0.85)',
454-
color: '#ffffff',
455-
fontSize: '11px',
456-
fontFamily: 'Roboto, sans-serif',
457-
borderRadius: '3px',
458-
cursor: 'grab',
459-
pointerEvents: 'auto',
460-
userSelect: 'none',
461-
whiteSpace: 'nowrap',
462-
touchAction: 'none',
463-
}}
464-
>
465-
{text}
338+
{pinned && createPortal(
339+
<div
340+
onClick={(e) => e.stopPropagation()}
341+
style={{
342+
position: 'fixed',
343+
left: `${pinned.x + 12}px`,
344+
top: `${pinned.y + 12}px`,
345+
background: 'rgba(38,38,42,0.95)',
346+
color: '#fff',
347+
padding: '8px 12px',
348+
borderRadius: '4px',
349+
fontFamily: 'Roboto, sans-serif',
350+
fontSize: '12px',
351+
maxWidth: '320px',
352+
zIndex: 9999,
353+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
354+
}}
355+
>
356+
<div style={{
357+
display: 'flex', justifyContent: 'space-between', gap: '8px',
358+
fontWeight: 'bold', marginBottom: '4px',
359+
}}>
360+
<span>{pinned.label}</span>
361+
<span
362+
onClick={() => setPinned(null)}
363+
style={{ cursor: 'pointer', opacity: 0.7 }}
364+
>×</span>
466365
</div>
467-
))}
468-
</div>
469-
</div>
366+
<div dangerouslySetInnerHTML={{ __html: pinned.title }} />
367+
</div>,
368+
document.body,
369+
)}
370+
</>
470371
);
471372
});
472373
Graph.displayName = 'Graph';

0 commit comments

Comments
 (0)