@truedat/core 8.14.0 → 8.14.1

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/core",
3
- "version": "8.14.0",
3
+ "version": "8.14.1",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -57,7 +57,7 @@
57
57
  "@testing-library/jest-dom": "^6.6.3",
58
58
  "@testing-library/react": "^16.3.0",
59
59
  "@testing-library/user-event": "^14.6.1",
60
- "@truedat/test": "8.14.0",
60
+ "@truedat/test": "8.14.1",
61
61
  "identity-obj-proxy": "^3.0.0",
62
62
  "jest": "^29.7.0",
63
63
  "redux-saga-test-plan": "^4.0.6"
@@ -99,5 +99,5 @@
99
99
  "swr": "^2.3.3",
100
100
  "turndown": "^7.2.2"
101
101
  },
102
- "gitHead": "9396fc850fc50e8e44a7198bac95c7a965396159"
102
+ "gitHead": "391e4e51eebb33b37b9676e541cda5f6f262e5e7"
103
103
  }
@@ -5,6 +5,7 @@ import ColoredEdge, {
5
5
  edgeAnimationDuration,
6
6
  getClassicArrowPoints,
7
7
  getFlexibleEdgePath,
8
+ isCompactInternalEdgeVisible,
8
9
  } from "../graph/ColoredEdge";
9
10
 
10
11
  jest.mock("@xyflow/react", () => ({
@@ -291,3 +292,218 @@ describe("edgeAnimationDuration", () => {
291
292
  },
292
293
  );
293
294
  });
295
+
296
+ describe("guided internal curves", () => {
297
+ const props = {
298
+ id: "internal",
299
+ source: "a",
300
+ target: "b",
301
+ sourceX: 360,
302
+ sourceY: 80,
303
+ targetX: 512,
304
+ targetY: 160,
305
+ sourcePosition: "right",
306
+ targetPosition: "left",
307
+ markerEnd: { type: "arrow" },
308
+ };
309
+ const data = {
310
+ internalOwnerId: "g",
311
+ edgePath: "guided-bezier",
312
+ dependencyLayout: "compact",
313
+ lockSourcePort: true,
314
+ lockTargetPort: true,
315
+ metadata: { relation: "depends_on" },
316
+ routeGuide: {
317
+ kind: "corridor",
318
+ laneX: 420,
319
+ sourceStubX: 384,
320
+ targetStubX: 488,
321
+ labelPoint: { x: 420, y: 120 },
322
+ },
323
+ };
324
+ it("hides all interaction when unrelated and reveals the same curve for the active node", () => {
325
+ const onClick = jest.fn();
326
+ const view = render(
327
+ <EdgeHoverContext.Provider value={{ compactReferenceNodeId: "other" }}>
328
+ <svg>
329
+ <ColoredEdge {...props} data={{ ...data, onClick }} />
330
+ </svg>
331
+ </EdgeHoverContext.Provider>,
332
+ );
333
+ const path = view.container.querySelector("path#internal");
334
+ const d = path.getAttribute("d");
335
+ expect(d).toContain(" C ");
336
+ expect(path.closest("g")).toHaveStyle({ display: "none" });
337
+ expect(view.container.querySelector(".td-graph-edge-hit-area")).toBeNull();
338
+ expect(
339
+ view.container.querySelector(".td-graph-edge-metadata-badge"),
340
+ ).toBeNull();
341
+ view.rerender(
342
+ <EdgeHoverContext.Provider value={{ compactReferenceNodeId: "a" }}>
343
+ <svg>
344
+ <ColoredEdge {...props} data={{ ...data, onClick }} />
345
+ </svg>
346
+ </EdgeHoverContext.Provider>,
347
+ );
348
+ expect(
349
+ view.container.querySelector("path#internal").getAttribute("d"),
350
+ ).toBe(d);
351
+ const hit = view.container.querySelector(".td-graph-edge-hit-area");
352
+ expect(hit.getAttribute("d")).toBe(d);
353
+ fireEvent.click(hit);
354
+ expect(onClick).toHaveBeenCalledWith(data.metadata);
355
+ });
356
+ it("reveals indirect focused dependencies and hides them when selection clears", () => {
357
+ const view = render(
358
+ <EdgeHoverContext.Provider value={{ compactReferenceNodeId: "upstream" }}>
359
+ <svg>
360
+ <ColoredEdge {...props} data={{ ...data, focusFocused: true }} />
361
+ </svg>
362
+ </EdgeHoverContext.Provider>,
363
+ );
364
+
365
+ expect(
366
+ view.container.querySelector("path#internal").closest("g"),
367
+ ).not.toHaveStyle({ display: "none" });
368
+ expect(
369
+ view.container.querySelector(".td-graph-edge-hit-area"),
370
+ ).not.toBeNull();
371
+
372
+ view.rerender(
373
+ <EdgeHoverContext.Provider value={{}}>
374
+ <svg>
375
+ <ColoredEdge {...props} data={data} />
376
+ </svg>
377
+ </EdgeHoverContext.Provider>,
378
+ );
379
+
380
+ expect(
381
+ view.container.querySelector("path#internal").closest("g"),
382
+ ).toHaveStyle({ display: "none" });
383
+ expect(view.container.querySelector(".td-graph-edge-hit-area")).toBeNull();
384
+ });
385
+ it.each([
386
+ ["hover", { hoveredNodeId: "a" }],
387
+ ["keyboard focus", { keyboardFocusedNodeId: "a" }],
388
+ ])(
389
+ "reveals the %s node dependencies while another node remains selected",
390
+ (_interaction, activeContext) => {
391
+ const view = render(
392
+ <EdgeHoverContext.Provider
393
+ value={{ compactReferenceNodeId: "selected-node", ...activeContext }}
394
+ >
395
+ <svg>
396
+ <ColoredEdge {...props} data={data} />
397
+ </svg>
398
+ </EdgeHoverContext.Provider>,
399
+ );
400
+
401
+ expect(
402
+ view.container.querySelector("path#internal").closest("g"),
403
+ ).not.toHaveStyle({ display: "none" });
404
+ expect(
405
+ view.container.querySelector(".td-graph-edge-hit-area"),
406
+ ).not.toBeNull();
407
+ },
408
+ );
409
+ it("uses supplied controls and keeps the path stable on hover", () => {
410
+ const view = render(
411
+ <svg>
412
+ <ColoredEdge
413
+ {...props}
414
+ data={{ ...data, dependencyLayout: "expanded" }}
415
+ />
416
+ </svg>,
417
+ );
418
+ const d = view.container.querySelector("path#internal").getAttribute("d");
419
+ fireEvent.mouseEnter(
420
+ view.container.querySelector(".td-graph-edge-hit-area"),
421
+ );
422
+ expect(
423
+ view.container.querySelector("path#internal").getAttribute("d"),
424
+ ).toBe(d);
425
+ view.rerender(
426
+ <svg>
427
+ <ColoredEdge
428
+ {...props}
429
+ data={{
430
+ ...data,
431
+ dependencyLayout: "expanded",
432
+ routeGuide: { ...data.routeGuide, laneX: 460 },
433
+ }}
434
+ />
435
+ </svg>,
436
+ );
437
+ expect(
438
+ view.container.querySelector("path#internal").getAttribute("d"),
439
+ ).not.toBe(d);
440
+ });
441
+ });
442
+
443
+ describe("isCompactInternalEdgeVisible", () => {
444
+ const edge = {
445
+ id: "internal",
446
+ source: "source",
447
+ target: "target",
448
+ data: { internalOwnerId: "group" },
449
+ };
450
+ const visibility = (context = {}) =>
451
+ isCompactInternalEdgeVisible({
452
+ edge,
453
+ dependencyLayout: "compact",
454
+ ...context,
455
+ });
456
+
457
+ it.each([
458
+ ["selected edge", { selectedEdgeId: "internal" }],
459
+ ["keyboard focus", { keyboardFocusedNodeId: "source" }],
460
+ ["node hover", { hoveredNodeId: "target" }],
461
+ ])("reveals an internal dependency for %s", (_label, context) => {
462
+ expect(visibility(context)).toBe(true);
463
+ });
464
+
465
+ it("reveals focused dependencies for the pinned or selected reference", () => {
466
+ expect(
467
+ visibility({
468
+ edge: { ...edge, data: { ...edge.data, focusFocused: true } },
469
+ referenceNodeId: "upstream-pin",
470
+ }),
471
+ ).toBe(true);
472
+ });
473
+ });
474
+
475
+ it("reuses guided geometry during hover and recomputes it when coordinates change", () => {
476
+ const laneX = jest.fn(() => 200);
477
+ const guide = {
478
+ kind: "corridor",
479
+ sourceStubX: 24,
480
+ targetStubX: 376,
481
+ get laneX() {
482
+ return laneX();
483
+ },
484
+ };
485
+ const data = { edgePath: "guided-bezier", routeGuide: guide };
486
+ const edge = (hoveredNodeId, targetX = 400) => (
487
+ <EdgeHoverContext.Provider value={{ hoveredNodeId }}>
488
+ <svg>
489
+ <ColoredEdge
490
+ id="memo-edge"
491
+ source="a"
492
+ target="b"
493
+ sourceX={0}
494
+ sourceY={0}
495
+ targetX={targetX}
496
+ targetY={100}
497
+ data={data}
498
+ />
499
+ </svg>
500
+ </EdgeHoverContext.Provider>
501
+ );
502
+ const view = render(edge(null));
503
+ const calls = laneX.mock.calls.length;
504
+ expect(calls).toBeGreaterThan(0);
505
+ view.rerender(edge("a"));
506
+ expect(laneX).toHaveBeenCalledTimes(calls);
507
+ view.rerender(edge("b", 500));
508
+ expect(laneX.mock.calls.length).toBeGreaterThan(calls);
509
+ });
@@ -118,6 +118,65 @@ describe("<LineageGroupNode />", () => {
118
118
  expect(rendered.getByText("1 nodo")).toBeInTheDocument();
119
119
  });
120
120
 
121
+ it("renders compact dependency guidance and role columns while expanded", () => {
122
+ const rendered = render(
123
+ <LineageGroupNode
124
+ id="dependency-group"
125
+ data={{
126
+ collapsed: false,
127
+ internalHint: "Select a node to reveal dependencies",
128
+ internalSummary: "3 internal dependencies",
129
+ internalColumns: [
130
+ {
131
+ key: "source",
132
+ label: "Sources",
133
+ countLabel: "2 nodes",
134
+ title: "Source role",
135
+ x: 24,
136
+ y: 64,
137
+ width: 120,
138
+ },
139
+ ],
140
+ label: "Dependency group",
141
+ variant: "absolute",
142
+ }}
143
+ />,
144
+ );
145
+
146
+ expect(
147
+ rendered
148
+ .getByText(/3 internal dependencies/)
149
+ .closest(".td-lineage-group-node__internal-summary"),
150
+ ).toHaveAttribute(
151
+ "title",
152
+ "3 internal dependencies · Select a node to reveal dependencies",
153
+ );
154
+ expect(rendered.getByText("Sources · 2 nodes")).toHaveAttribute(
155
+ "title",
156
+ "Source role",
157
+ );
158
+ });
159
+
160
+ it("hides compact dependency guidance and role columns while collapsed", () => {
161
+ const rendered = render(
162
+ <LineageGroupNode
163
+ id="collapsed-dependency-group"
164
+ data={{
165
+ collapsed: true,
166
+ internalSummary: "3 internal dependencies",
167
+ internalColumns: [
168
+ { key: "source", label: "Sources", countLabel: "2 nodes" },
169
+ ],
170
+ label: "Dependency group",
171
+ variant: "absolute",
172
+ }}
173
+ />,
174
+ );
175
+
176
+ expect(rendered.queryByText(/3 internal dependencies/)).toBeNull();
177
+ expect(rendered.queryByText("Sources · 2 nodes")).toBeNull();
178
+ });
179
+
121
180
  it("retains an explicit classic modifier", () => {
122
181
  const rendered = render(
123
182
  <LineageGroupNode
@@ -3,6 +3,7 @@ import {
3
3
  memo,
4
4
  useContext,
5
5
  useEffect,
6
+ useMemo,
6
7
  useRef,
7
8
  useState,
8
9
  } from "react";
@@ -376,14 +377,89 @@ export const getFlexibleEdgePath = ({
376
377
  };
377
378
  };
378
379
 
380
+ export const getGuidedBezierEdgePath = ({
381
+ sourceX,
382
+ sourceY,
383
+ targetX,
384
+ targetY,
385
+ sourcePosition = "right",
386
+ targetPosition = "left",
387
+ routeGuide,
388
+ }) => {
389
+ if (!routeGuide)
390
+ return getFlexibleEdgePath({
391
+ sourceX,
392
+ sourceY,
393
+ targetX,
394
+ targetY,
395
+ sourcePosition,
396
+ targetPosition,
397
+ });
398
+ const sourceVector = portVector(sourcePosition),
399
+ targetVector = portVector(targetPosition);
400
+ const sourceStubX = routeGuide.sourceStubX ?? sourceX + sourceVector.x * 24;
401
+ const targetStubX = routeGuide.targetStubX ?? targetX + targetVector.x * 24;
402
+ const corridor = routeGuide.kind === "corridor";
403
+ const points = corridor
404
+ ? []
405
+ : dedupePoints([
406
+ point(sourceX, sourceY),
407
+ point(sourceStubX, sourceY),
408
+ point(routeGuide.sourceLaneX, sourceY),
409
+ point(routeGuide.sourceLaneX, routeGuide.laneY),
410
+ point(routeGuide.targetLaneX, routeGuide.laneY),
411
+ point(routeGuide.targetLaneX, targetY),
412
+ point(targetStubX, targetY),
413
+ point(targetX, targetY),
414
+ ]);
415
+ const path = corridor
416
+ ? `M ${sourceX},${sourceY} L ${sourceStubX},${sourceY} C ${routeGuide.laneX},${sourceY} ${routeGuide.laneX},${targetY} ${targetStubX},${targetY} L ${targetX},${targetY}`
417
+ : roundedOrthogonalPath(points, routeGuide.cornerRadius ?? 24);
418
+ return {
419
+ path,
420
+ labelX: corridor
421
+ ? getBezierPoint(
422
+ 0.5,
423
+ sourceStubX,
424
+ routeGuide.laneX,
425
+ routeGuide.laneX,
426
+ targetStubX,
427
+ )
428
+ : routeGuide.labelPoint.x,
429
+ labelY: corridor ? (sourceY + targetY) / 2 : routeGuide.labelPoint.y,
430
+ sourceTangent: sourceVector,
431
+ targetTangent: normalizeVector(-targetVector.x, -targetVector.y),
432
+ };
433
+ };
434
+
435
+ export const isCompactInternalEdgeVisible = ({
436
+ edge,
437
+ dependencyLayout,
438
+ referenceNodeId,
439
+ selectedEdgeId,
440
+ keyboardFocusedNodeId,
441
+ hoveredNodeId,
442
+ }) => {
443
+ if (dependencyLayout !== "compact" || !edge.data?.internalOwnerId)
444
+ return true;
445
+ if (edge.id === selectedEdgeId) return true;
446
+ if (referenceNodeId && edge.data?.focusFocused) return true;
447
+ return [referenceNodeId, keyboardFocusedNodeId, hoveredNodeId].some(
448
+ (activeNodeId) =>
449
+ activeNodeId &&
450
+ (edge.source === activeNodeId || edge.target === activeNodeId),
451
+ );
452
+ };
453
+
379
454
  export const getEdgeSlotOffsets = (data = {}) => ({
380
455
  sourceSlotOffset: data.lockSourcePort
381
456
  ? 0
382
457
  : data.sourceFieldIndex === undefined
383
458
  ? getTargetSlotOffset(data.sourceSlotIndex, data.sourceSlotCount)
384
459
  : 0,
385
- targetSlotOffset:
386
- data.targetFieldIndex === undefined
460
+ targetSlotOffset: data.lockTargetPort
461
+ ? 0
462
+ : data.targetFieldIndex === undefined
387
463
  ? getTargetSlotOffset(
388
464
  data.targetSlotIndex,
389
465
  data.targetSlotCount,
@@ -394,7 +470,11 @@ export const getEdgeSlotOffsets = (data = {}) => ({
394
470
  : 0,
395
471
  });
396
472
 
397
- export const getLineageEdgeVisualState = (data = {}, hovered = false) => {
473
+ export const getLineageEdgeVisualState = (
474
+ data = {},
475
+ hovered = false,
476
+ visible = true,
477
+ ) => {
398
478
  const aggregatedCount = Number(data.aggregatedCount);
399
479
  const aggregated = Number.isFinite(aggregatedCount) && aggregatedCount > 1;
400
480
  const dimmed = Boolean(data.focusDimmed) && !hovered;
@@ -403,9 +483,10 @@ export const getLineageEdgeVisualState = (data = {}, hovered = false) => {
403
483
  aggregated,
404
484
  highlighted:
405
485
  !data.containment && !dimmed && (hovered || Boolean(data.focusFocused)),
406
- showDecorations: !dimmed,
407
- showHitArea: true,
486
+ showDecorations: visible && !dimmed,
487
+ showHitArea: visible,
408
488
  showLabel:
489
+ visible &&
409
490
  !dimmed &&
410
491
  (aggregated
411
492
  ? Boolean(data.selectedEndpoint)
@@ -430,6 +511,14 @@ const ColoredEdge = ({
430
511
  const hoverCtx = useContext(EdgeHoverContext);
431
512
  const hoveredNodeId = hoverCtx?.hoveredNodeId;
432
513
  const hoveredEdgeEndpoints = hoverCtx?.hoveredEdgeEndpoints;
514
+ const compactVisible = isCompactInternalEdgeVisible({
515
+ edge: { id, source, target, data },
516
+ dependencyLayout: data.dependencyLayout,
517
+ referenceNodeId: hoverCtx?.compactReferenceNodeId,
518
+ selectedEdgeId: hoverCtx?.selectedEdgeId,
519
+ keyboardFocusedNodeId: hoverCtx?.keyboardFocusedNodeId,
520
+ hoveredNodeId,
521
+ });
433
522
  const [hovered, setHovered] = useState(false);
434
523
  const [motionTiming, setMotionTiming] = useState(null);
435
524
  const gRef = useRef(null);
@@ -440,12 +529,14 @@ const ColoredEdge = ({
440
529
  const interactive = Boolean(data.onClick);
441
530
  const { sourceSlotOffset, targetSlotOffset } = getEdgeSlotOffsets(data);
442
531
  const useOrthogonalPath = data.edgePath === "orthogonal";
532
+ const useGuidedBezierPath = data.edgePath === "guided-bezier";
533
+ const useGuidedPath = useOrthogonalPath || useGuidedBezierPath;
443
534
  const useFlexibleBezierPath = data.edgePath === "bezier";
444
- const useClassicEdgePath = !useOrthogonalPath && !useFlexibleBezierPath;
535
+ const useClassicEdgePath = !useGuidedPath && !useFlexibleBezierPath;
445
536
  const adjustedSourceY = sourceY + (data.bundleSource ? 0 : sourceSlotOffset);
446
537
  const adjustedTargetY = targetY + (data.bundleTarget ? 0 : targetSlotOffset);
447
538
  const { aggregated, highlighted, showDecorations, showHitArea, showLabel } =
448
- getLineageEdgeVisualState(data, hovered);
539
+ getLineageEdgeVisualState(data, hovered, compactVisible);
449
540
  const dependenciesOnLeft =
450
541
  data.selectedEndpoint === "source" ? targetX < sourceX : sourceX < targetX;
451
542
  const setHoveredEdgeEndpoints = hoverCtx?.setHoveredEdgeEndpoints;
@@ -458,13 +549,13 @@ const ColoredEdge = ({
458
549
  };
459
550
  }, [id, showHitArea, setHoveredEdgeEndpoints]);
460
551
  const forceHorizontalPorts = Boolean(
461
- useOrthogonalPath && data.forceHorizontalPorts,
552
+ useGuidedPath && data.forceHorizontalPorts,
462
553
  );
463
554
  const stroke = style.stroke || "var(--td-graph-edge-stroke, #b0b8c8)";
464
555
  const markerPosition = data.markerPosition;
465
556
  const showTargetMarker =
466
557
  showDecorations &&
467
- (useOrthogonalPath
558
+ (useGuidedPath
468
559
  ? markerPosition === "target" ||
469
560
  markerPosition === "both" ||
470
561
  (markerPosition === undefined && data.showTargetArrow !== false)
@@ -483,7 +574,19 @@ const ColoredEdge = ({
483
574
  const sourcePortY = adjustedSourceY;
484
575
  const targetPortX = targetX;
485
576
  const targetPortY = adjustedTargetY;
486
- const { edgePath, labelX, labelY, targetTangent, sourceTangent } = (() => {
577
+ const geometry = useMemo(() => {
578
+ if (useGuidedBezierPath) {
579
+ const curve = getGuidedBezierEdgePath({
580
+ sourceX: sourcePortX,
581
+ sourceY: sourcePortY,
582
+ targetX: targetPortX,
583
+ targetY: targetPortY,
584
+ sourcePosition: effectiveSourcePosition,
585
+ targetPosition: effectiveTargetPosition,
586
+ routeGuide: data.routeGuide,
587
+ });
588
+ return { ...curve, edgePath: curve.path };
589
+ }
487
590
  if (useOrthogonalPath) {
488
591
  const orthogonalEdge = getOrthogonalEdgePath({
489
592
  sourceX: sourcePortX,
@@ -606,9 +709,37 @@ const ColoredEdge = ({
606
709
 
607
710
  return { edgePath, labelX, labelY, targetTangent, sourceTangent };
608
711
  }
609
- })();
712
+ }, [
713
+ useGuidedBezierPath,
714
+ useOrthogonalPath,
715
+ useFlexibleBezierPath,
716
+ sourcePortX,
717
+ sourcePortY,
718
+ targetPortX,
719
+ targetPortY,
720
+ effectiveSourcePosition,
721
+ effectiveTargetPosition,
722
+ sourceX,
723
+ targetX,
724
+ adjustedSourceY,
725
+ adjustedTargetY,
726
+ sourcePosition,
727
+ targetPosition,
728
+ data.routeGuide,
729
+ data.edgeBorderRadius,
730
+ data.edgeOffset,
731
+ data.routeIndex,
732
+ data.routeCount,
733
+ data.sourceNodeY,
734
+ data.targetNodeY,
735
+ data.edgeCurvature,
736
+ data.bundleSource,
737
+ data.bundleTarget,
738
+ data.bundleOffset,
739
+ ]);
740
+ const { edgePath, labelX, labelY, targetTangent, sourceTangent } = geometry;
610
741
  useEffect(() => {
611
- if (!data.animated || !gRef.current) return;
742
+ if (!compactVisible || !data.animated || !gRef.current) return;
612
743
 
613
744
  const pathElement = gRef.current.querySelector("path.td-graph-edge");
614
745
 
@@ -623,7 +754,7 @@ const ColoredEdge = ({
623
754
  ? current
624
755
  : { duration, path: edgePath },
625
756
  );
626
- }, [data.animated, edgePath]);
757
+ }, [compactVisible, data.animated, edgePath]);
627
758
  const animationDuration =
628
759
  motionTiming?.path === edgePath ? motionTiming.duration : null;
629
760
  const nodeHovered = hoveredNodeId
@@ -643,7 +774,7 @@ const ColoredEdge = ({
643
774
  ? "var(--td-graph-edge-active, var(--td-graph-accent, #ed5c17))"
644
775
  : stroke;
645
776
  const handleClick = (event) => {
646
- if (!interactive) return;
777
+ if (!interactive || !compactVisible) return;
647
778
  event.stopPropagation();
648
779
  data.onClick(data.metadata);
649
780
  };
@@ -664,7 +795,13 @@ const ColoredEdge = ({
664
795
  .join(" ");
665
796
 
666
797
  return (
667
- <g ref={gRef}>
798
+ <g
799
+ ref={gRef}
800
+ style={
801
+ compactVisible ? undefined : { display: "none", pointerEvents: "none" }
802
+ }
803
+ aria-hidden={!compactVisible || undefined}
804
+ >
668
805
  <BaseEdge
669
806
  className={edgeClassName}
670
807
  id={id}
@@ -674,7 +811,7 @@ const ColoredEdge = ({
674
811
  stroke: effectiveStroke,
675
812
  }}
676
813
  />
677
- {data.animated && animationDuration ? (
814
+ {compactVisible && data.animated && animationDuration ? (
678
815
  <circle
679
816
  aria-hidden="true"
680
817
  className="td-graph-edge-flow-dot"
@@ -49,6 +49,9 @@ const LineageGroupNode = ({ id, data = {} }) => {
49
49
  oversized,
50
50
  oversizedWarningOpen,
51
51
  resourceContainer,
52
+ internalColumns = [],
53
+ internalSummary,
54
+ internalHint,
52
55
  systemTypeClassName,
53
56
  systemTypeLabel,
54
57
  systemTypeMissing,
@@ -195,6 +198,30 @@ const LineageGroupNode = ({ id, data = {} }) => {
195
198
  ) : null}
196
199
  </div>
197
200
  ) : null}
201
+ {!collapsed && internalSummary ? (
202
+ <div
203
+ className="td-lineage-group-node__internal-summary"
204
+ title={[internalSummary, internalHint].filter(Boolean).join(" · ")}
205
+ >
206
+ <Icon aria-hidden="true" name="info circle" />
207
+ <span>
208
+ {internalSummary}
209
+ {internalHint ? ` · ${internalHint}` : null}
210
+ </span>
211
+ </div>
212
+ ) : null}
213
+ {!collapsed
214
+ ? internalColumns.map((column) => (
215
+ <span
216
+ key={`${column.key}-${column.y}`}
217
+ className="td-lineage-group-node__column-label"
218
+ title={column.title}
219
+ style={{ left: column.x, top: column.y, width: column.width }}
220
+ >
221
+ {column.label} · {column.countLabel ?? column.count}
222
+ </span>
223
+ ))
224
+ : null}
198
225
  {showContentSummary ? (
199
226
  <div className="td-lineage-group-node__footer">
200
227
  <span className="td-lineage-group-node__content-summary">
@@ -296,6 +323,20 @@ LineageGroupNode.propTypes = {
296
323
  oversized: PropTypes.bool,
297
324
  oversizedWarningOpen: PropTypes.bool,
298
325
  resourceContainer: PropTypes.bool,
326
+ internalColumns: PropTypes.arrayOf(
327
+ PropTypes.shape({
328
+ key: PropTypes.string,
329
+ label: PropTypes.string,
330
+ title: PropTypes.string,
331
+ x: PropTypes.number,
332
+ y: PropTypes.number,
333
+ width: PropTypes.number,
334
+ count: PropTypes.number,
335
+ countLabel: PropTypes.string,
336
+ }),
337
+ ),
338
+ internalSummary: PropTypes.string,
339
+ internalHint: PropTypes.string,
299
340
  systemTypeClassName: PropTypes.string,
300
341
  systemTypeLabel: PropTypes.string,
301
342
  systemTypeMissing: PropTypes.bool,
@@ -556,3 +556,42 @@
556
556
  .td-lineage-group-node.td-lineage-group-node {
557
557
  .lineage-node-states(td-lineage-group-node);
558
558
  }
559
+
560
+ .td-lineage-group-node__column-label {
561
+ position: absolute;
562
+ height: 24px;
563
+ color: #64748b;
564
+ font-size: 12px;
565
+ font-weight: 700;
566
+ line-height: 24px;
567
+ pointer-events: none;
568
+ }
569
+ .td-lineage-group-node__internal-summary {
570
+ position: absolute;
571
+ top: 42px;
572
+ right: 0;
573
+ left: 0;
574
+ display: flex;
575
+ height: 16px;
576
+ align-items: center;
577
+ gap: 4px;
578
+ padding: 0 6px;
579
+ background: #eff6ff;
580
+ color: #1e40af;
581
+ font-size: 10px;
582
+ font-weight: 400;
583
+ line-height: 16px;
584
+
585
+ > .icon {
586
+ flex-shrink: 0;
587
+ height: 16px;
588
+ margin: 0;
589
+ line-height: 16px;
590
+ }
591
+
592
+ > span {
593
+ overflow: hidden;
594
+ text-overflow: ellipsis;
595
+ white-space: nowrap;
596
+ }
597
+ }