@truedat/dd 8.12.10 → 8.12.12

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "8.12.10",
3
+ "version": "8.12.12",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -54,7 +54,7 @@
54
54
  "@testing-library/jest-dom": "^6.6.3",
55
55
  "@testing-library/react": "^16.3.0",
56
56
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.12.8",
57
+ "@truedat/test": "8.12.12",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
@@ -90,5 +90,5 @@
90
90
  "svg-pan-zoom": "^3.6.2",
91
91
  "swr": "^2.3.3"
92
92
  },
93
- "gitHead": "ac5f172e0ff4c087ea040ae29d1f2ea822bcadb6"
93
+ "gitHead": "b6e3413dae81b2e28e98a21cf371174caa30ab4b"
94
94
  }
@@ -4,7 +4,6 @@ import PropTypes from "prop-types";
4
4
  import { connect } from "react-redux";
5
5
  import { FormattedMessage, useIntl } from "react-intl";
6
6
  import Graph from "@truedat/core/components/Graph";
7
- import LineageGroupNode from "@truedat/core/components/graph/LineageGroupNode";
8
7
  import { EdgeHoverContext } from "@truedat/core/components/graph/ColoredEdge";
9
8
  import { cancelPoll } from "../routines";
10
9
  import { layoutVisibleLineageGraph } from "../utils/lineageLayout";
@@ -63,7 +62,7 @@ import {
63
62
  spreadLegacyFieldIndexes,
64
63
  legacyPathEdges,
65
64
  } from "../utils/lineageGraph";
66
- import LineageResourceNode from "./LineageResourceNode";
65
+ import LineageGraphNode from "./LineageGraphNode";
67
66
  import LineageModuleButton from "./LineageModuleButton";
68
67
  import LineageGraphToolbar, { visibleLegendKeys } from "./LineageGraphToolbar";
69
68
 
@@ -114,8 +113,8 @@ export {
114
113
  } from "../utils/lineageGraph";
115
114
 
116
115
  const LINEAGE_NODE_TYPES = {
117
- lineageGroup: LineageGroupNode,
118
- lineageResource: LineageResourceNode,
116
+ lineageGroup: LineageGraphNode,
117
+ lineageResource: LineageGraphNode,
119
118
  };
120
119
 
121
120
  export const LineageGraph = ({
@@ -286,16 +285,11 @@ export const LineageGraph = ({
286
285
  setFitViewRequestKey((requestKey) => (requestKey || 0) + 1);
287
286
  }, []);
288
287
 
289
- const handleEdgeSelect = useCallback(
290
- (edgeContext) => {
291
- setPinnedGraphNodeId(null);
292
-
293
- setSelectedContext({ ...edgeContext, iconName: "exchange" });
294
- setFocusNodeId(null);
295
- setActivePanel("inspector");
296
- },
297
- [pinnedGraphNodeId],
298
- );
288
+ const handleEdgeSelect = useCallback((edgeContext) => {
289
+ setSelectedContext({ ...edgeContext, iconName: "exchange" });
290
+ setFocusNodeId(null);
291
+ setActivePanel("inspector");
292
+ }, []);
299
293
 
300
294
  const resolvedNodeWidth = useMemo(
301
295
  () =>
@@ -878,38 +872,41 @@ export const LineageGraph = ({
878
872
  );
879
873
  }, [layoutedGraphNodes, requestedGraphNodeIds, searchableGraphNodes]);
880
874
  const selectedGraphNodeId =
881
- pinnedGraphNodeId ||
882
875
  (selectedContext?.type === "node" && selectedContext.node?.graphNodeId) ||
883
876
  null;
877
+ const visiblePinnedGraphNodeId =
878
+ preparedTopology?.originalToVisibleId.get(pinnedGraphNodeId) ||
879
+ pinnedGraphNodeId;
880
+ const focusGraphNodeId = visiblePinnedGraphNodeId || selectedGraphNodeId;
884
881
  const selectedGraphEdgeId =
885
882
  selectedContext?.type === "edge" ? selectedContext.edge?.id : null;
886
883
  const focusActive =
887
- Boolean(selectedGraphNodeId || selectedGraphEdgeId) || toolbarFilterActive;
884
+ Boolean(focusGraphNodeId || selectedGraphEdgeId) || toolbarFilterActive;
888
885
  const focusedSelectedNodeId = selectedGraphNodeId;
889
886
  const { focusedEdgeIds, focusedNodeIds } = useMemo(() => {
890
- if (selectedGraphEdgeId) {
887
+ if (!pinnedGraphNodeId && selectedGraphEdgeId) {
891
888
  const edge = graphEdges.find(({ id }) => id === selectedGraphEdgeId);
892
889
  return {
893
890
  focusedEdgeIds: new Set(edge ? [edge.id] : []),
894
891
  focusedNodeIds: new Set(edge ? [edge.source, edge.target] : []),
895
892
  };
896
893
  }
897
- if (!selectedGraphNodeId) {
894
+ if (!focusGraphNodeId) {
898
895
  return toolbarFilterActive
899
896
  ? focusedLineageGraphForNodeSet(toolbarMatchedNodeIds, graphEdges)
900
897
  : { focusedEdgeIds: new Set(), focusedNodeIds: new Set() };
901
898
  }
902
899
 
903
- const selectedNode = lineageGraphIndex.nodeById.get(selectedGraphNodeId);
900
+ const selectedNode = lineageGraphIndex.nodeById.get(focusGraphNodeId);
904
901
  const seedIds = new Set(
905
902
  preparedTopology?.originalsByVisibleId
906
- .get(selectedGraphNodeId)
907
- ?.map(({ id }) => id) || [selectedGraphNodeId],
903
+ .get(focusGraphNodeId)
904
+ ?.map(({ id }) => id) || [focusGraphNodeId],
908
905
  );
909
906
 
910
907
  if (selectedNode?.type === "lineageGroup") {
911
908
  lineageGraphIndex
912
- .descendantIds(selectedGraphNodeId)
909
+ .descendantIds(focusGraphNodeId)
913
910
  .forEach((id) => seedIds.add(id));
914
911
  }
915
912
 
@@ -936,15 +933,22 @@ export const LineageGraph = ({
936
933
  focusedNodeIds: projectedNodeIds,
937
934
  };
938
935
  }, [
936
+ pinnedGraphNodeId,
939
937
  graphEdges,
940
938
  layoutedGraphNodes,
941
939
  lineageGraphIndex,
942
940
  preparedTopology,
943
- selectedGraphNodeId,
941
+ focusGraphNodeId,
944
942
  selectedGraphEdgeId,
945
943
  toolbarFilterActive,
946
944
  toolbarMatchedNodeIds,
947
945
  ]);
946
+ const unpinFocus = useCallback(() => {
947
+ setPinnedGraphNodeId(null);
948
+ setSelectedContext(null);
949
+ setActivePanel(null);
950
+ setFocusNodeId(null);
951
+ }, []);
948
952
  const renderedGraphNodes = useMemo(
949
953
  () =>
950
954
  focusLineageNodes(
@@ -952,12 +956,14 @@ export const LineageGraph = ({
952
956
  focusedNodeIds,
953
957
  focusActive,
954
958
  focusedSelectedNodeId,
959
+ visiblePinnedGraphNodeId,
955
960
  ).map((node) => ({
956
961
  ...node,
957
962
  className: [
958
963
  node.className,
959
964
  queryOriginStates.has(node.id) ? "lineage-node--origin" : "",
960
965
  node.id === focusedSelectedNodeId ? "lineage-node--selected" : "",
966
+ node.id === visiblePinnedGraphNodeId ? "lineage-node--pinned" : "",
961
967
  ]
962
968
  .filter(Boolean)
963
969
  .join(" "),
@@ -965,6 +971,8 @@ export const LineageGraph = ({
965
971
  data: {
966
972
  ...node.data,
967
973
  queryOrigin: queryOriginStates.get(node.id),
974
+ onUnpin:
975
+ node.id === visiblePinnedGraphNodeId ? unpinFocus : undefined,
968
976
  connectedHandleIds: Array.from(
969
977
  connectedHandleIdsByNode.get(node.id) || [],
970
978
  ),
@@ -975,6 +983,8 @@ export const LineageGraph = ({
975
983
  focusActive,
976
984
  focusedNodeIds,
977
985
  focusedSelectedNodeId,
986
+ visiblePinnedGraphNodeId,
987
+ unpinFocus,
978
988
  layoutedGraphNodes,
979
989
  queryOriginStates,
980
990
  ],
@@ -985,8 +995,8 @@ export const LineageGraph = ({
985
995
  graphEdges,
986
996
  focusedEdgeIds,
987
997
  focusActive,
988
- Boolean(selectedGraphNodeId),
989
- selectedGraphNodeId,
998
+ Boolean(focusGraphNodeId),
999
+ focusGraphNodeId,
990
1000
  selectedGraphEdgeId,
991
1001
  ).map((edge) => ({
992
1002
  ...edge,
@@ -1009,7 +1019,7 @@ export const LineageGraph = ({
1009
1019
  focusActive,
1010
1020
  focusedEdgeIds,
1011
1021
  graphEdges,
1012
- selectedGraphNodeId,
1022
+ focusGraphNodeId,
1013
1023
  handleEdgeSelect,
1014
1024
  visibleNodeById,
1015
1025
  selectedGraphEdgeId,
@@ -1073,6 +1083,12 @@ export const LineageGraph = ({
1073
1083
  () => new Map(searchableGraphNodes.map((node) => [node.id, node])),
1074
1084
  [searchableGraphNodes],
1075
1085
  );
1086
+ useEffect(() => {
1087
+ setPinnedGraphNodeId(null);
1088
+ setSelectedContext(null);
1089
+ setActivePanel(null);
1090
+ setFocusNodeId(null);
1091
+ }, [graphId, graphType]);
1076
1092
  useEffect(() => {
1077
1093
  if (
1078
1094
  !initialViewState ||
@@ -1143,19 +1159,37 @@ export const LineageGraph = ({
1143
1159
  searchableNodeById,
1144
1160
  renderedGraphEdges,
1145
1161
  ]);
1162
+ const pinnedNode = searchableNodeById.get(visiblePinnedGraphNodeId);
1163
+ const pinnedContextNode = pinnedNode
1164
+ ? selectedNodeFromGraphNode(pinnedNode, searchableNodeById)
1165
+ : null;
1166
+ const pinnedContext = pinnedContextNode
1167
+ ? { type: "node", node: pinnedContextNode }
1168
+ : null;
1146
1169
  useEffect(() => {
1147
- if (!selectedGraphNodeId || searchableNodeById.has(selectedGraphNodeId))
1148
- return;
1149
-
1150
- setActivePanel(null);
1151
- setPinnedGraphNodeId(null);
1152
- setSelectedContext(null);
1153
- setFocusNodeId(null);
1154
- }, [searchableNodeById, selectedGraphNodeId]);
1170
+ if (loading) return;
1171
+ if (
1172
+ pinnedGraphNodeId &&
1173
+ !searchableNodeById.has(visiblePinnedGraphNodeId) &&
1174
+ !lineageGraphIndex.nodeById.has(pinnedGraphNodeId)
1175
+ ) {
1176
+ setPinnedGraphNodeId(null);
1177
+ }
1178
+ if (selectedGraphNodeId && !searchableNodeById.has(selectedGraphNodeId)) {
1179
+ setActivePanel(null);
1180
+ setSelectedContext(null);
1181
+ setFocusNodeId(null);
1182
+ }
1183
+ }, [
1184
+ loading,
1185
+ pinnedGraphNodeId,
1186
+ visiblePinnedGraphNodeId,
1187
+ lineageGraphIndex,
1188
+ searchableNodeById,
1189
+ selectedGraphNodeId,
1190
+ ]);
1155
1191
  const selectGraphNode = useCallback(
1156
1192
  (node, { center = false } = {}) => {
1157
- if (pinnedGraphNodeId) return;
1158
-
1159
1193
  const selectedNode = selectedNodeFromGraphNode(node, searchableNodeById);
1160
1194
 
1161
1195
  if (!selectedNode) return;
@@ -1176,7 +1210,7 @@ export const LineageGraph = ({
1176
1210
  setFocusNodeId(center ? node.id : null);
1177
1211
  if (center) setFocusNodeRequestKey((requestKey) => requestKey + 1);
1178
1212
  },
1179
- [pinnedGraphNodeId, positionedLayout, searchableNodeById],
1213
+ [positionedLayout, searchableNodeById],
1180
1214
  );
1181
1215
  const handleNodeClick = useCallback(
1182
1216
  (event, node) => {
@@ -1220,15 +1254,21 @@ export const LineageGraph = ({
1220
1254
 
1221
1255
  if (!graphNodeId) return;
1222
1256
 
1223
- setPinnedGraphNodeId((currentGraphNodeId) =>
1224
- currentGraphNodeId === graphNodeId ? null : graphNodeId,
1225
- );
1226
- }, [selectedContext]);
1227
- const unpinFocus = useCallback(() => setPinnedGraphNodeId(null), []);
1228
- const handleCenterNode = useCallback((nodeId) => {
1229
- setFocusNodeId(nodeId);
1230
- setFocusNodeRequestKey((requestKey) => requestKey + 1);
1231
- }, []);
1257
+ if (visiblePinnedGraphNodeId === graphNodeId) {
1258
+ unpinFocus();
1259
+ return;
1260
+ }
1261
+ setPinnedGraphNodeId(graphNodeId);
1262
+ }, [selectedContext, unpinFocus, visiblePinnedGraphNodeId]);
1263
+ const handleCenterNode = useCallback(
1264
+ (nodeId) => {
1265
+ const node = searchableNodeById.get(nodeId);
1266
+
1267
+ if (!node) return;
1268
+ selectGraphNode(node, { center: true });
1269
+ },
1270
+ [searchableNodeById, selectGraphNode],
1271
+ );
1232
1272
  const handleNodeDoubleClick = useCallback(
1233
1273
  (event, node) => {
1234
1274
  event?.preventDefault();
@@ -1245,19 +1285,11 @@ export const LineageGraph = ({
1245
1285
  return;
1246
1286
  }
1247
1287
 
1248
- if (pinnedGraphNodeId) {
1249
- if (node?.id === pinnedGraphNodeId) {
1250
- setActivePanel("inspector");
1251
- }
1252
- return;
1253
- }
1254
-
1255
1288
  selectGraphNode(node);
1256
1289
  },
1257
1290
  [
1258
1291
  collapsibleGroupIds,
1259
1292
  doubleClickExpandsNodes,
1260
- pinnedGraphNodeId,
1261
1293
  selectGraphNode,
1262
1294
  toggleGroupCollapse,
1263
1295
  ],
@@ -1270,7 +1302,9 @@ export const LineageGraph = ({
1270
1302
  ? activeSurfaceRef.current
1271
1303
  : stageRef.current
1272
1304
  )
1273
- ?.querySelector(".lineage-graph__dock-context-clear")
1305
+ ?.querySelector(
1306
+ "button.lineage-graph__dock-context-reopen, .lineage-graph__dock-context-clear",
1307
+ )
1274
1308
  ?.focus();
1275
1309
  });
1276
1310
  }, []);
@@ -1280,28 +1314,21 @@ export const LineageGraph = ({
1280
1314
  }, [focusContextButton]);
1281
1315
  const clearSelectedContext = useCallback(() => {
1282
1316
  setActivePanel(null);
1283
- setPinnedGraphNodeId(null);
1284
1317
  setSelectedContext(null);
1285
1318
  setFocusNodeId(null);
1286
1319
  }, []);
1287
- const handlePanelChange = useCallback(
1288
- (panel) => {
1289
- if (pinnedGraphNodeId && panel !== "inspector" && panel !== null) return;
1290
- setActivePanel(panel);
1291
- },
1292
- [pinnedGraphNodeId],
1293
- );
1320
+ const handlePanelChange = useCallback((panel) => setActivePanel(panel), []);
1321
+ const selectPinnedContext = () => {
1322
+ if (!pinnedContext) return;
1323
+ setSelectedContext(pinnedContext);
1324
+ setActivePanel("inspector");
1325
+ };
1294
1326
  const handlePaneSelectionClear = useCallback(
1295
1327
  (event) => {
1296
- if (pinnedGraphNodeId) return;
1297
-
1298
- setActivePanel(null);
1299
- setPinnedGraphNodeId(null);
1300
- setSelectedContext(null);
1301
- setFocusNodeId(null);
1328
+ clearSelectedContext();
1302
1329
  onPaneClick?.(event);
1303
1330
  },
1304
- [onPaneClick, pinnedGraphNodeId],
1331
+ [clearSelectedContext, onPaneClick],
1305
1332
  );
1306
1333
 
1307
1334
  useEffect(() => {
@@ -1317,21 +1344,17 @@ export const LineageGraph = ({
1317
1344
  ) {
1318
1345
  return;
1319
1346
  }
1320
- if (activePanel === "inspector" && pinnedGraphNodeId) return;
1321
- setActivePanel(null);
1347
+ if (pinnedGraphNodeId) clearSelectedContext();
1348
+ else setActivePanel(null);
1322
1349
  };
1323
1350
  const closeOnEscape = (event) => {
1324
1351
  if (event.key !== "Escape") return;
1325
1352
 
1326
- if (pinnedGraphNodeId && !activePanel) return;
1327
1353
  event.preventDefault();
1328
1354
  event.stopPropagation();
1329
- if (pinnedGraphNodeId) {
1330
- setActivePanel(null);
1331
- return;
1332
- }
1333
1355
  if (selectedContext) {
1334
1356
  clearSelectedContext();
1357
+ focusContextButton();
1335
1358
  return;
1336
1359
  }
1337
1360
  setActivePanel(null);
@@ -1348,9 +1371,9 @@ export const LineageGraph = ({
1348
1371
  }, [
1349
1372
  activePanel,
1350
1373
  selectedContext,
1374
+ pinnedGraphNodeId,
1351
1375
  clearSelectedContext,
1352
1376
  focusContextButton,
1353
- pinnedGraphNodeId,
1354
1377
  ]);
1355
1378
 
1356
1379
  const shouldRenderGraph = !loading && hasGraph;
@@ -1376,6 +1399,8 @@ export const LineageGraph = ({
1376
1399
  canExpandAll={canExpandAll}
1377
1400
  collapsible={positionedLayout && Boolean(collapsibleGroupIds.length)}
1378
1401
  context={selectedContext}
1402
+ pinnedContext={pinnedContext}
1403
+ onPinnedContextSelect={selectPinnedContext}
1379
1404
  doubleClickExpandsNodes={doubleClickExpandsNodes}
1380
1405
  visibleLegend={visibleLegend}
1381
1406
  legendItems={visibleLegendItems}
@@ -1397,7 +1422,7 @@ export const LineageGraph = ({
1397
1422
  onCenterNode={handleCenterNode}
1398
1423
  pinned={
1399
1424
  selectedContext?.type === "node" &&
1400
- pinnedGraphNodeId === selectedContext.node?.graphNodeId
1425
+ visiblePinnedGraphNodeId === selectedContext.node?.graphNodeId
1401
1426
  }
1402
1427
  expansionDepth={expansionDepth}
1403
1428
  maxExpansionDepth={effectiveMaxDepth}
@@ -1524,6 +1549,8 @@ export const LineageGraph = ({
1524
1549
  />
1525
1550
  </EdgeHoverContext.Provider>
1526
1551
  </div>
1552
+ ) : loading ? (
1553
+ <div className="lineage-graph__empty" />
1527
1554
  ) : (
1528
1555
  <LineageGraphEmptyState />
1529
1556
  )}
@@ -0,0 +1,53 @@
1
+ import { memo } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import { Pin } from "lucide-react";
5
+ import LineageGroupNode from "@truedat/core/components/graph/LineageGroupNode";
6
+ import LineageResourceNode from "./LineageResourceNode";
7
+
8
+ const stopPropagation = (event) => event.stopPropagation();
9
+
10
+ const LineageGraphNode = ({ type, data, ...props }) => {
11
+ const { formatMessage } = useIntl();
12
+ const Node = type === "lineageGroup" ? LineageGroupNode : LineageResourceNode;
13
+ const unpinLabel = formatMessage({
14
+ id: "lineage.context_menu.unpin_focus",
15
+ defaultMessage: "Unpin focus",
16
+ });
17
+ const unpin = (event) => {
18
+ event.preventDefault();
19
+ event.stopPropagation();
20
+ event.currentTarget.closest(".react-flow__node")?.focus();
21
+ data.onUnpin();
22
+ };
23
+
24
+ return (
25
+ <>
26
+ <Node {...props} data={data} />
27
+ {data.onUnpin ? (
28
+ <button
29
+ aria-label={unpinLabel}
30
+ aria-pressed="true"
31
+ className="lineage-graph__node-unpin nodrag nopan"
32
+ onClick={unpin}
33
+ onDoubleClick={stopPropagation}
34
+ onPointerDown={stopPropagation}
35
+ onMouseDown={stopPropagation}
36
+ onKeyDown={stopPropagation}
37
+ onKeyUp={stopPropagation}
38
+ title={unpinLabel}
39
+ type="button"
40
+ >
41
+ <Pin size={14} aria-hidden="true" />
42
+ </button>
43
+ ) : null}
44
+ </>
45
+ );
46
+ };
47
+
48
+ LineageGraphNode.propTypes = {
49
+ type: PropTypes.string,
50
+ data: PropTypes.object.isRequired,
51
+ };
52
+
53
+ export default memo(LineageGraphNode);
@@ -539,6 +539,8 @@ export const LineageGraphToolbar = ({
539
539
  onUnpin,
540
540
  onCenterNode,
541
541
  pinned,
542
+ pinnedContext,
543
+ onPinnedContextSelect,
542
544
  expansionDepth = 4,
543
545
  maxExpansionDepth = expansionDepth,
544
546
  searchInput = "",
@@ -551,14 +553,22 @@ export const LineageGraphToolbar = ({
551
553
  const { formatMessage } = useIntl();
552
554
  const [depthPopoverOpen, setDepthPopoverOpen] = useState(false);
553
555
  const [settingsPopoverOpen, setSettingsPopoverOpen] = useState(false);
554
- const hasContext = Boolean(context);
555
- const inspectorOpen = hasContext && activePanel === "inspector";
556
- const selectedLabel = contextLabel(context);
556
+ const dockContext = context || pinnedContext;
557
+ const pinnedOnly = !context && Boolean(pinnedContext);
558
+ const ContextSummary = pinnedOnly ? "button" : "div";
559
+ const dockPinned = pinned || pinnedOnly;
560
+ const hasContext = Boolean(dockContext);
561
+ const inspectorOpen = Boolean(context) && activePanel === "inspector";
562
+ const selectedLabel = contextLabel(dockContext);
557
563
  const selectedTypeLabel = hasContext
558
- ? contextTypeLabel(context, formatMessage)
564
+ ? contextTypeLabel(dockContext, formatMessage)
559
565
  : "";
560
566
  const togglePanel = (panel) =>
561
567
  onPanelChange(activePanel === panel ? null : panel);
568
+ const unpinLabel = formatMessage({
569
+ id: "lineage.context_menu.unpin_focus",
570
+ defaultMessage: "Unpin focus",
571
+ });
562
572
  const clearContextLabel = formatMessage(
563
573
  {
564
574
  id: "lineage.graph.toolbar.context.clear",
@@ -566,6 +576,17 @@ export const LineageGraphToolbar = ({
566
576
  },
567
577
  { name: selectedLabel },
568
578
  );
579
+ const unpinButton = (
580
+ <button
581
+ aria-label={unpinLabel}
582
+ aria-pressed="true"
583
+ className="lineage-graph__dock-context-unpin"
584
+ onClick={onUnpin}
585
+ type="button"
586
+ >
587
+ <DockIcon name="pin" />
588
+ </button>
589
+ );
569
590
  const visibleLevelsLabel = formatMessage({
570
591
  id: "lineage.graph.controls.visible_levels",
571
592
  defaultMessage: "Visible levels",
@@ -592,7 +613,7 @@ export const LineageGraphToolbar = ({
592
613
  hasContext ? " lineage-graph__dock-context--selected" : ""
593
614
  }${
594
615
  inspectorOpen ? " lineage-graph__dock-context--inspector-open" : ""
595
- }${pinned ? " lineage-graph__dock-context--pinned" : ""}`}
616
+ }${dockPinned ? " lineage-graph__dock-context--pinned" : ""}`}
596
617
  >
597
618
  {inspectorOpen ? (
598
619
  <LineageInspectorPanel
@@ -610,28 +631,16 @@ export const LineageGraphToolbar = ({
610
631
  className={`lineage-graph__dock-context-bar${
611
632
  inspectorOpen ? " lineage-graph__dock-context-bar--expanded" : ""
612
633
  }${
613
- context?.type === "edge"
634
+ dockContext?.type === "edge"
614
635
  ? " lineage-graph__dock-context-bar--edge"
615
636
  : ""
616
637
  }`}
617
638
  >
618
- {pinned ? (
619
- <button
620
- aria-label={formatMessage({
621
- id: "lineage.context_menu.unpin_focus",
622
- defaultMessage: "Unpin focus",
623
- })}
624
- aria-pressed="true"
625
- className="lineage-graph__dock-context-unpin"
626
- onClick={onUnpin}
627
- type="button"
628
- >
629
- <DockIcon name="pin" />
630
- </button>
631
- ) : null}
632
- <div
633
- aria-live="polite"
639
+ {pinnedOnly ? unpinButton : null}
640
+ <ContextSummary
634
641
  className="lineage-graph__dock-context-reopen lineage-graph__dock-context-button"
642
+ onClick={pinnedOnly ? onPinnedContextSelect : undefined}
643
+ type={pinnedOnly ? "button" : undefined}
635
644
  >
636
645
  <span
637
646
  aria-live="polite"
@@ -641,7 +650,7 @@ export const LineageGraphToolbar = ({
641
650
  aria-hidden="true"
642
651
  className="lineage-graph__dock-context-summary-icon"
643
652
  >
644
- <DockIcon name={contextIcon(context)} />
653
+ <DockIcon name={contextIcon(dockContext)} />
645
654
  </span>
646
655
  <span className="lineage-graph__dock-context-copy">
647
656
  <span className="lineage-graph__dock-context-label">
@@ -652,15 +661,17 @@ export const LineageGraphToolbar = ({
652
661
  </span>
653
662
  </span>
654
663
  </span>
655
- </div>
656
- <button
657
- aria-label={clearContextLabel}
658
- className="lineage-graph__dock-context-clear"
659
- onClick={onClearContext}
660
- type="button"
661
- >
662
- <DockIcon name="close" />
663
- </button>
664
+ </ContextSummary>
665
+ {context ? (
666
+ <button
667
+ aria-label={clearContextLabel}
668
+ className="lineage-graph__dock-context-clear"
669
+ onClick={onClearContext}
670
+ type="button"
671
+ >
672
+ <DockIcon name="close" />
673
+ </button>
674
+ ) : null}
664
675
  </div>
665
676
  ) : (
666
677
  <LineageToolButton
@@ -680,7 +691,6 @@ export const LineageGraphToolbar = ({
680
691
  <LineageToolButton
681
692
  active={activePanel === "search"}
682
693
  aria-expanded={activePanel === "search"}
683
- disabled={pinned}
684
694
  icon="search"
685
695
  label={formatMessage({
686
696
  id: "lineage.graph.toolbar.search.header",
@@ -884,6 +894,8 @@ LineageGraphToolbar.propTypes = {
884
894
  onUnpin: PropTypes.func,
885
895
  onCenterNode: PropTypes.func,
886
896
  pinned: PropTypes.bool,
897
+ pinnedContext: PropTypes.object,
898
+ onPinnedContextSelect: PropTypes.func,
887
899
  expansionDepth: PropTypes.number,
888
900
  maxExpansionDepth: PropTypes.number,
889
901
  searchInput: PropTypes.string,
@@ -2,6 +2,7 @@ import {
2
2
  collapseLineageNodes,
3
3
  expandLineageDescendantsUntilOversized,
4
4
  focusLineageEdges,
5
+ focusLineageNodes,
5
6
  focusedLineageGraphForNodeSet,
6
7
  lineageDirectChildCounts,
7
8
  lineageEdge,
@@ -894,3 +895,18 @@ describe("lineageCatalogState", () => {
894
895
  ).toBe("deleted");
895
896
  });
896
897
  });
898
+
899
+ describe("focusLineageNodes with a pinned node", () => {
900
+ it("keeps an unrelated selection visible without losing the pinned focus", () => {
901
+ const nodes = ["A", "B", "C"].map((id) => ({
902
+ id,
903
+ type: "lineageResource",
904
+ data: {},
905
+ }));
906
+ const result = focusLineageNodes(nodes, new Set(), true, "B", "A");
907
+ expect(result[0].data.focusDimmed).toBe(false);
908
+ expect(result[1].data.focusSelected).toBe(true);
909
+ expect(result[1].data.focusDimmed).toBe(false);
910
+ expect(result[2].data.focusDimmed).toBe(true);
911
+ });
912
+ });
@@ -175,7 +175,7 @@ it("restores groups, fields, depth and a pinned selection without reapplying def
175
175
  },
176
176
  });
177
177
  expect(screen.getAllByRole("button", { name: "Unpin focus" })).toHaveLength(
178
- 2,
178
+ 1,
179
179
  );
180
180
  fireEvent.click(
181
181
  screen.getByRole("button", { name: "View in Lineage module" }),