@truedat/dd 8.14.0 → 8.14.2

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.
@@ -1,6 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
  import PropTypes from "prop-types";
3
- import { useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { useIntl } from "react-intl";
5
5
  import { Dropdown, Input, Popup } from "semantic-ui-react";
6
6
  import {
@@ -10,6 +10,8 @@ import {
10
10
  ChevronLeft,
11
11
  ChevronRight,
12
12
  ClipboardList,
13
+ Columns2,
14
+ Columns3,
13
15
  Copy,
14
16
  Database,
15
17
  Eye,
@@ -61,6 +63,8 @@ export const DOCK_ICONS = {
61
63
  linkify: Link2,
62
64
  certificate: Award,
63
65
  "mouse pointer": MousePointer2,
66
+ "columns-2": Columns2,
67
+ "columns-3": Columns3,
64
68
  compress: Minimize2,
65
69
  "expand arrows alternate": Maximize2,
66
70
  "chevron right": ChevronRight,
@@ -522,6 +526,8 @@ export const LineageGraphToolbar = ({
522
526
  collapsible,
523
527
  context,
524
528
  doubleClickExpandsNodes = false,
529
+ dependencyLayout = "compact",
530
+ onDependencyLayoutChange,
525
531
  legendItems = [],
526
532
  visibleLegend = [],
527
533
  onClosePanel,
@@ -541,6 +547,7 @@ export const LineageGraphToolbar = ({
541
547
  pinned,
542
548
  pinnedContext,
543
549
  onPinnedContextSelect,
550
+ expansionDepthCustom = false,
544
551
  expansionDepth = 4,
545
552
  maxExpansionDepth = expansionDepth,
546
553
  searchInput = "",
@@ -553,6 +560,47 @@ export const LineageGraphToolbar = ({
553
560
  const { formatMessage } = useIntl();
554
561
  const [depthPopoverOpen, setDepthPopoverOpen] = useState(false);
555
562
  const [settingsPopoverOpen, setSettingsPopoverOpen] = useState(false);
563
+
564
+ const dockRef = useRef(null);
565
+ const depthRef = useRef(null);
566
+ const settingsRef = useRef(null);
567
+ const handleDepthEscape = useCallback(
568
+ (event) => {
569
+ if (event.key !== "Escape" || !depthPopoverOpen) return;
570
+ event.preventDefault();
571
+ event.stopPropagation();
572
+ setDepthPopoverOpen(false);
573
+ dockRef.current
574
+ ?.querySelector(".lineage-graph__dock-depth-button")
575
+ ?.focus();
576
+ },
577
+ [depthPopoverOpen],
578
+ );
579
+ useEffect(() => {
580
+ if (!depthPopoverOpen) return undefined;
581
+ depthRef.current?.querySelector("input")?.focus();
582
+ window.addEventListener("keydown", handleDepthEscape, true);
583
+ return () => window.removeEventListener("keydown", handleDepthEscape, true);
584
+ }, [depthPopoverOpen, handleDepthEscape]);
585
+ const handleSettingsEscape = useCallback(
586
+ (event) => {
587
+ if (event.key !== "Escape" || !settingsPopoverOpen) return;
588
+ event.preventDefault();
589
+ event.stopPropagation();
590
+ setSettingsPopoverOpen(false);
591
+ dockRef.current
592
+ ?.querySelector(".lineage-graph__dock-settings-button")
593
+ ?.focus();
594
+ },
595
+ [settingsPopoverOpen],
596
+ );
597
+ useEffect(() => {
598
+ if (!settingsPopoverOpen) return undefined;
599
+ settingsRef.current?.querySelector("input:checked, input")?.focus();
600
+ window.addEventListener("keydown", handleSettingsEscape, true);
601
+ return () =>
602
+ window.removeEventListener("keydown", handleSettingsEscape, true);
603
+ }, [handleSettingsEscape, settingsPopoverOpen]);
556
604
  const dockContext = context || pinnedContext;
557
605
  const pinnedOnly = !context && Boolean(pinnedContext);
558
606
  const ContextSummary = pinnedOnly ? "button" : "div";
@@ -595,6 +643,16 @@ export const LineageGraphToolbar = ({
595
643
  id: "lineage.graph.settings.header",
596
644
  defaultMessage: "Graph settings",
597
645
  });
646
+ const layoutToggleLabel = formatMessage({
647
+ id:
648
+ dependencyLayout === "compact"
649
+ ? "lineage.graph.layout.switch_expanded"
650
+ : "lineage.graph.layout.switch_compact",
651
+ defaultMessage:
652
+ dependencyLayout === "compact"
653
+ ? "Switch to expanded view"
654
+ : "Switch to compact view",
655
+ });
598
656
  const doubleClickInfoLabel = formatMessage({
599
657
  id: "lineage.graph.settings.double_click_info",
600
658
  defaultMessage: "Double-click expands or collapses nodes",
@@ -603,6 +661,8 @@ export const LineageGraphToolbar = ({
603
661
  return (
604
662
  <div
605
663
  className="lineage-graph__dock nodrag nopan nowheel"
664
+ ref={dockRef}
665
+ onKeyDown={handleSettingsEscape}
606
666
  onClick={stopGraphInteraction}
607
667
  onMouseDown={stopGraphInteraction}
608
668
  onPointerDown={stopGraphInteraction}
@@ -706,7 +766,7 @@ export const LineageGraphToolbar = ({
706
766
  size="mini"
707
767
  tooltipPosition="left center"
708
768
  />
709
- {collapsible ? (
769
+ {collapsible || onDependencyLayoutChange ? (
710
770
  <>
711
771
  <span className="lineage-graph__dock-divider" aria-hidden="true" />
712
772
  {!simplified ? (
@@ -737,25 +797,78 @@ export const LineageGraphToolbar = ({
737
797
  aria-label={settingsLabel}
738
798
  className="lineage-graph__settings-popover"
739
799
  role="dialog"
800
+ ref={settingsRef}
801
+ onKeyDown={handleSettingsEscape}
740
802
  >
741
803
  <span className="lineage-graph__settings-popover-header">
742
804
  {settingsLabel}
743
805
  </span>
744
- <label className="lineage-graph__settings-toggle">
745
- <input
746
- checked={doubleClickExpandsNodes}
747
- onChange={(event) =>
748
- onDoubleClickExpandsNodesChange?.(event.target.checked)
749
- }
750
- type="checkbox"
751
- />
752
- <span>{doubleClickInfoLabel}</span>
753
- </label>
806
+ {!simplified ? (
807
+ <label className="lineage-graph__settings-toggle">
808
+ <input
809
+ checked={doubleClickExpandsNodes}
810
+ onChange={(event) =>
811
+ onDoubleClickExpandsNodesChange?.(
812
+ event.target.checked,
813
+ )
814
+ }
815
+ type="checkbox"
816
+ />
817
+ <span>{doubleClickInfoLabel}</span>
818
+ </label>
819
+ ) : null}
754
820
  </div>
755
821
  }
756
822
  className="lineage-graph__settings-popup"
757
823
  />
758
824
  ) : null}
825
+ </>
826
+ ) : null}
827
+ {onDependencyLayoutChange ? (
828
+ <Popup
829
+ on={["hover", "focus"]}
830
+ mouseEnterDelay={TOOLTIP_DELAY}
831
+ mouseLeaveDelay={0}
832
+ position="left center"
833
+ header={formatMessage({
834
+ id: `lineage.graph.layout.${dependencyLayout === "compact" ? "expanded" : "compact"}`,
835
+ defaultMessage:
836
+ dependencyLayout === "compact" ? "Expanded view" : "Compact view",
837
+ })}
838
+ content={
839
+ <>
840
+ <p>
841
+ {formatMessage({
842
+ id:
843
+ dependencyLayout === "compact"
844
+ ? "lineage.graph.layout.expanded_hint"
845
+ : "lineage.graph.layout.compact_hint",
846
+ defaultMessage:
847
+ dependencyLayout === "compact"
848
+ ? "Shows internal dependencies between nodes in each group."
849
+ : "Reduces visual clutter. Select a node to explore its internal dependencies.",
850
+ })}
851
+ </p>
852
+ <strong>{layoutToggleLabel}</strong>
853
+ </>
854
+ }
855
+ trigger={
856
+ <LineageToolButton
857
+ className="lineage-graph__dock-layout-button"
858
+ icon={dependencyLayout === "compact" ? "columns-3" : "columns-2"}
859
+ label={layoutToggleLabel}
860
+ showTooltip={false}
861
+ onClick={() =>
862
+ onDependencyLayoutChange(
863
+ dependencyLayout === "compact" ? "expanded" : "compact",
864
+ )
865
+ }
866
+ />
867
+ }
868
+ />
869
+ ) : null}
870
+ {collapsible ? (
871
+ <>
759
872
  <Popup
760
873
  on="click"
761
874
  onClose={() => setDepthPopoverOpen(false)}
@@ -777,7 +890,9 @@ export const LineageGraphToolbar = ({
777
890
  label={visibleLevelsLabel}
778
891
  showTooltip={false}
779
892
  >
780
- <span className="lineage-graph__dock-depth-value">
893
+ <span
894
+ className={`lineage-graph__dock-depth-value${expansionDepthCustom ? " is-custom" : ""}`}
895
+ >
781
896
  {expansionDepth}
782
897
  </span>
783
898
  </LineageToolButton>
@@ -785,23 +900,54 @@ export const LineageGraphToolbar = ({
785
900
  content={
786
901
  <div
787
902
  aria-label={visibleLevelsLabel}
788
- className="lineage-graph__depth-popover"
903
+ className={`lineage-graph__depth-popover${expansionDepthCustom ? " is-custom" : ""}`}
904
+ onClick={stopGraphInteraction}
905
+ onKeyDown={handleDepthEscape}
906
+ onMouseDown={stopGraphInteraction}
907
+ onPointerDown={stopGraphInteraction}
908
+ onWheel={stopGraphInteraction}
909
+ ref={depthRef}
789
910
  role="dialog"
790
911
  >
791
912
  <div className="lineage-graph__depth-popover-header">
792
913
  <span>{visibleLevelsLabel}</span>
793
914
  </div>
794
- <input
795
- aria-label={visibleLevelsLabel}
796
- className="lineage-graph__depth-slider"
797
- type="range"
798
- min={0}
799
- max={maxExpansionDepth}
800
- value={expansionDepth}
801
- onChange={(event) =>
802
- onExpansionDepthChange(event.target.value)
803
- }
804
- />
915
+ <div className="lineage-graph__depth-slider-wrap">
916
+ <input
917
+ aria-label={visibleLevelsLabel}
918
+ aria-valuetext={
919
+ expansionDepthCustom
920
+ ? formatMessage(
921
+ {
922
+ id: "lineage.graph.controls.custom_depth",
923
+ defaultMessage:
924
+ "Last selected level: {depth}. Nodes have been expanded or collapsed individually.",
925
+ },
926
+ { depth: expansionDepth },
927
+ )
928
+ : undefined
929
+ }
930
+ className="lineage-graph__depth-slider"
931
+ type="range"
932
+ min={0}
933
+ max={maxExpansionDepth}
934
+ value={expansionDepth}
935
+ onChange={(event) =>
936
+ onExpansionDepthChange(event.target.value)
937
+ }
938
+ />
939
+ {expansionDepthCustom ? (
940
+ <span
941
+ aria-hidden="true"
942
+ className="lineage-graph__depth-custom-marker"
943
+ style={{
944
+ left: `calc(8px + (100% - 16px) * ${maxExpansionDepth ? expansionDepth / maxExpansionDepth : 0})`,
945
+ }}
946
+ >
947
+ ?
948
+ </span>
949
+ ) : null}
950
+ </div>
805
951
  <div
806
952
  aria-hidden="true"
807
953
  className="lineage-graph__depth-popover-scale"
@@ -877,6 +1023,8 @@ LineageGraphToolbar.propTypes = {
877
1023
  collapsible: PropTypes.bool,
878
1024
  context: PropTypes.object,
879
1025
  doubleClickExpandsNodes: PropTypes.bool,
1026
+ dependencyLayout: PropTypes.oneOf(["compact", "expanded"]),
1027
+ onDependencyLayoutChange: PropTypes.func,
880
1028
  legendItems: PropTypes.array,
881
1029
  visibleLegend: PropTypes.array,
882
1030
  onClosePanel: PropTypes.func.isRequired,
@@ -896,6 +1044,7 @@ LineageGraphToolbar.propTypes = {
896
1044
  pinned: PropTypes.bool,
897
1045
  pinnedContext: PropTypes.object,
898
1046
  onPinnedContextSelect: PropTypes.func,
1047
+ expansionDepthCustom: PropTypes.bool,
899
1048
  expansionDepth: PropTypes.number,
900
1049
  maxExpansionDepth: PropTypes.number,
901
1050
  searchInput: PropTypes.string,
@@ -8,6 +8,7 @@ import {
8
8
  lineageEdge,
9
9
  lineageCatalogState,
10
10
  lineageNodeContentWidth,
11
+ lineageTopologyNodes,
11
12
  lineageNodeMatchesToolbarFilters,
12
13
  lineageOversizedGroupIds,
13
14
  lineagePositionedLayoutNodes,
@@ -310,6 +311,103 @@ describe("focusedLineageGraphForNodeSet", () => {
310
311
  });
311
312
  });
312
313
 
314
+ describe("sibling content widths", () => {
315
+ const shortGroup = { id: "short", container: true, z: 0 };
316
+ const longGroup = { id: "long", container: true, z: 0 };
317
+ const nested = {
318
+ id: "nested",
319
+ parentId: "short",
320
+ container: true,
321
+ z: 1,
322
+ name: "Container title ".repeat(10),
323
+ };
324
+ const leaves = [
325
+ { id: "a", name: "a", parentId: "short", z: 1 },
326
+ { id: "b", name: "b", parentId: "short", z: 1 },
327
+ { id: "c", name: "c".repeat(50), parentId: "long", z: 1 },
328
+ { id: "d", name: "d", parentId: "long", z: 1 },
329
+ { id: "e", name: "e".repeat(30), parentId: "nested", z: 2 },
330
+ { id: "f", name: "f", parentId: "nested", z: 2 },
331
+ { id: "root-a", name: "root".repeat(8), z: 0 },
332
+ { id: "root-b", name: "root", z: 0 },
333
+ ];
334
+ const nodes = [shortGroup, longGroup, nested, ...leaves];
335
+ const widths = (rendered) =>
336
+ Object.fromEntries(rendered.map((node) => [node.id, node.width]));
337
+
338
+ it("sizes each immediate sibling group independently, including roots", () => {
339
+ const result = widths(lineageTopologyNodes({ nodes }));
340
+ expect(result.a).toBe(240);
341
+ expect(result.b).toBe(result.a);
342
+ expect(result.c).toBe(360);
343
+ expect(result.d).toBe(result.c);
344
+ expect(result.e).toBe(283);
345
+ expect(result.f).toBe(result.e);
346
+ expect(result["root-a"]).toBe(296);
347
+ expect(result["root-b"]).toBe(result["root-a"]);
348
+ expect(
349
+ widths(lineageTopologyNodes({ nodes: [...nodes].reverse() })),
350
+ ).toEqual(result);
351
+ });
352
+
353
+ it("preserves classic widths and explicit width overrides", () => {
354
+ expect(widths(lineageTopologyNodes({ nodes }, "legacy")).a).toBe(288);
355
+ expect(widths(lineageTopologyNodes({ nodes }, "redesigned", 320)).a).toBe(
356
+ 320,
357
+ );
358
+ });
359
+
360
+ it("uses all field content before expansion in positioned groups", () => {
361
+ const resources = leaves.slice(0, 4).map((node, index) => ({
362
+ ...node,
363
+ x: index * 400,
364
+ y: 60,
365
+ w: 120,
366
+ h: 60,
367
+ fields:
368
+ node.id === "c"
369
+ ? Array.from({ length: 12 }, (_, i) => ({
370
+ name: i === 11 ? "hidden_long_field_name" : "id",
371
+ type: i === 11 ? "VARCHAR(512)" : "INT",
372
+ }))
373
+ : [],
374
+ name: node.id,
375
+ }));
376
+ const groups = [shortGroup, longGroup].map((group) => ({
377
+ ...group,
378
+ x: 0,
379
+ y: 0,
380
+ w: 1000,
381
+ h: 600,
382
+ }));
383
+ const preparedHierarchy = {
384
+ containerGroups: groups,
385
+ leafGroups: [],
386
+ parentByNode: new Map(
387
+ resources.map((node) => [
388
+ node,
389
+ groups.find((group) => group.id === node.parentId),
390
+ ]),
391
+ ),
392
+ childCountsByCanonicalId: new Map(),
393
+ };
394
+ const options = { resources, groups, preparedHierarchy };
395
+ const collapsed = widths(lineagePositionedLayoutNodes(options));
396
+ const expanded = widths(
397
+ lineagePositionedLayoutNodes({
398
+ ...options,
399
+ expandedFieldNodeIds: new Set(["c"]),
400
+ }),
401
+ );
402
+ expect(collapsed.a).toBe(240);
403
+ expect(collapsed.b).toBe(240);
404
+ expect(collapsed.c).toBe(lineageNodeContentWidth([resources[2]]));
405
+ expect(collapsed.c).toBeGreaterThan(collapsed.a);
406
+ expect(collapsed.d).toBe(collapsed.c);
407
+ expect(expanded).toEqual(collapsed);
408
+ });
409
+ });
410
+
313
411
  describe("lineageNodeContentWidth", () => {
314
412
  it("returns the minimum width when no content is provided", () => {
315
413
  expect(lineageNodeContentWidth()).toBe(240);