@truedat/dd 8.11.4 → 8.11.7

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.
Files changed (43) hide show
  1. package/package.json +4 -3
  2. package/src/components/Graphs.js +30 -4
  3. package/src/components/LineageContextMenu.js +185 -58
  4. package/src/components/LineageDownloadButton.js +16 -6
  5. package/src/components/LineageEventsPanel.js +80 -27
  6. package/src/components/LineageEventsTable.js +7 -3
  7. package/src/components/LineageGraph.js +5144 -306
  8. package/src/components/LineageGraphToolbar.js +896 -0
  9. package/src/components/LineageInspectorPanel.js +748 -0
  10. package/src/components/LineageResourceNode.js +501 -0
  11. package/src/components/LineageRoutes.js +1 -2
  12. package/src/components/LineageSelectionPanel.js +28 -12
  13. package/src/components/StructureGraph.js +8 -1
  14. package/src/components/StructureGraphView.js +14 -0
  15. package/src/components/StructureImpact.js +2 -22
  16. package/src/components/StructureLineage.js +2 -19
  17. package/src/components/StructuresUploadEventsTable.js +0 -3
  18. package/src/components/__tests__/LineageContextMenu.spec.js +42 -0
  19. package/src/components/__tests__/LineageGraph.spec.js +896 -0
  20. package/src/components/__tests__/LineageGraphInteractions.spec.js +660 -0
  21. package/src/components/__tests__/LineageGraphLayout.spec.js +620 -0
  22. package/src/components/__tests__/LineageGraphToolbar.spec.js +366 -0
  23. package/src/components/__tests__/LineageHierarchy.spec.js +116 -0
  24. package/src/components/__tests__/LineageInspectorPanel.spec.js +357 -0
  25. package/src/components/__tests__/LineageResourceNode.spec.js +338 -0
  26. package/src/components/__tests__/LineageRoutes.spec.js +27 -9
  27. package/src/components/__tests__/__snapshots__/LineageDownloadButton.spec.js.snap +23 -3
  28. package/src/components/__tests__/__snapshots__/LineageForm.spec.js.snap +23 -3
  29. package/src/components/__tests__/lineageEventHelpers.spec.js +60 -0
  30. package/src/components/lineageEventHelpers.js +53 -17
  31. package/src/reducers/__tests__/lineageEvents.spec.js +164 -0
  32. package/src/reducers/__tests__/lineageQuery.spec.js +11 -0
  33. package/src/reducers/lineageEvents.js +68 -2
  34. package/src/reducers/lineageQuery.js +2 -6
  35. package/src/styles/LineageGraphColors.less +105 -0
  36. package/src/styles/LineageGraphCore.less +20 -0
  37. package/src/styles/LineageResourceNode.less +711 -0
  38. package/src/styles/lineageGraph.less +2973 -84
  39. package/src/utils/__tests__/lineageGraphIndex.spec.js +52 -0
  40. package/src/utils/__tests__/lineageLayout.spec.js +234 -0
  41. package/src/utils/__tests__/lineageTopology.spec.js +256 -0
  42. package/src/utils/lineageGraphIndex.js +296 -0
  43. package/src/utils/lineageLayout.js +1047 -0
@@ -0,0 +1,896 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { useState } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { Dropdown, Input, Popup } from "semantic-ui-react";
6
+ import {
7
+ ArrowLeftRight,
8
+ Award,
9
+ CheckCircle2,
10
+ ChevronLeft,
11
+ ChevronRight,
12
+ ClipboardList,
13
+ Copy,
14
+ Database,
15
+ Eye,
16
+ Grid3x3,
17
+ GripVertical,
18
+ Info,
19
+ KeyRound,
20
+ Layers,
21
+ Link2,
22
+ List,
23
+ Maximize2,
24
+ Minimize2,
25
+ MousePointer2,
26
+ Network,
27
+ Pin,
28
+ RefreshCw,
29
+ Search,
30
+ Settings,
31
+ Table2,
32
+ X,
33
+ } from "lucide-react";
34
+ import { ControlButton } from "@truedat/core/components/graph/FlowPorts";
35
+ import LineageDownloadButton from "./LineageDownloadButton";
36
+ import LineageInspectorPanel from "./LineageInspectorPanel";
37
+
38
+ const ALL_SYSTEM_TYPES_VALUE = "";
39
+ const TOOLTIP_DELAY = 120;
40
+ const DOCK_ICON_SIZE = 20;
41
+ const DOCK_ICON_STROKE = 2;
42
+
43
+ const stopGraphInteraction = (event) => event.stopPropagation();
44
+
45
+ export const DOCK_ICONS = {
46
+ database: Database,
47
+ table: Table2,
48
+ sitemap: Network,
49
+ "grip vertical": GripVertical,
50
+ copy: Copy,
51
+ "sync alternate": RefreshCw,
52
+ tasks: ClipboardList,
53
+ th: Grid3x3,
54
+ eye: Eye,
55
+ exchange: ArrowLeftRight,
56
+ search: Search,
57
+ "list ul": List,
58
+ "info circle": Info,
59
+ "check circle outline": CheckCircle2,
60
+ key: KeyRound,
61
+ linkify: Link2,
62
+ certificate: Award,
63
+ "mouse pointer": MousePointer2,
64
+ compress: Minimize2,
65
+ "expand arrows alternate": Maximize2,
66
+ "chevron right": ChevronRight,
67
+ "chevron left": ChevronLeft,
68
+ pin: Pin,
69
+ close: X,
70
+ };
71
+
72
+ const DockIcon = ({ name }) => {
73
+ const IconComponent = DOCK_ICONS[name] || Database;
74
+
75
+ return (
76
+ <span className="lineage-graph__dock-button-icon">
77
+ <IconComponent
78
+ aria-hidden="true"
79
+ size={DOCK_ICON_SIZE}
80
+ strokeWidth={DOCK_ICON_STROKE}
81
+ />
82
+ </span>
83
+ );
84
+ };
85
+
86
+ DockIcon.propTypes = {
87
+ name: PropTypes.string,
88
+ };
89
+
90
+ const LineageToolButton = ({
91
+ active = false,
92
+ children,
93
+ className,
94
+ disabled = false,
95
+ icon,
96
+ label,
97
+ showTooltip = true,
98
+ ...buttonProps
99
+ }) => {
100
+ const button = (
101
+ <ControlButton
102
+ {...buttonProps}
103
+ aria-label={label}
104
+ className={[
105
+ "lineage-graph__dock-button",
106
+ active ? "lineage-graph__dock-button--active" : "",
107
+ className,
108
+ ]
109
+ .filter(Boolean)
110
+ .join(" ")}
111
+ disabled={disabled}
112
+ title={showTooltip ? label : undefined}
113
+ >
114
+ {icon ? (
115
+ typeof icon === "string" ? (
116
+ <DockIcon name={icon} />
117
+ ) : (
118
+ <span className="lineage-graph__dock-button-icon">{icon}</span>
119
+ )
120
+ ) : null}
121
+ {children}
122
+ </ControlButton>
123
+ );
124
+
125
+ if (!showTooltip) return button;
126
+
127
+ return (
128
+ <Popup
129
+ content={label}
130
+ mouseEnterDelay={TOOLTIP_DELAY}
131
+ mouseLeaveDelay={0}
132
+ position="left center"
133
+ trigger={
134
+ <span className="lineage-graph__dock-tooltip-trigger">{button}</span>
135
+ }
136
+ />
137
+ );
138
+ };
139
+
140
+ LineageToolButton.propTypes = {
141
+ active: PropTypes.bool,
142
+ children: PropTypes.node,
143
+ className: PropTypes.string,
144
+ disabled: PropTypes.bool,
145
+ icon: PropTypes.string,
146
+ label: PropTypes.string.isRequired,
147
+ showTooltip: PropTypes.bool,
148
+ };
149
+
150
+ const PanelHeader = ({ icon, onClose, title }) => {
151
+ const { formatMessage } = useIntl();
152
+
153
+ return (
154
+ <div className="lineage-graph__tool-panel-header">
155
+ <span>
156
+ <DockIcon name={icon} />
157
+ {title}
158
+ </span>
159
+ <button
160
+ aria-label={formatMessage({
161
+ id: "lineage.graph.toolbar.panel.close",
162
+ defaultMessage: "Close panel",
163
+ })}
164
+ className="lineage-graph__tool-panel-close"
165
+ onClick={onClose}
166
+ type="button"
167
+ >
168
+ <DockIcon name="close" />
169
+ </button>
170
+ </div>
171
+ );
172
+ };
173
+
174
+ PanelHeader.propTypes = {
175
+ icon: PropTypes.string.isRequired,
176
+ onClose: PropTypes.func.isRequired,
177
+ title: PropTypes.string.isRequired,
178
+ };
179
+
180
+ const SearchPanel = ({
181
+ onClose,
182
+ onSearchInputChange,
183
+ onSearchResultSelect,
184
+ onSystemTypeFilterChange,
185
+ searchInput,
186
+ searchResults,
187
+ searchResultsTotal,
188
+ selectedGraphNodeId,
189
+ systemTypeFilter,
190
+ systemTypeOptions,
191
+ }) => {
192
+ const { formatMessage } = useIntl();
193
+ const hasFilter = Boolean(searchInput.trim() || systemTypeFilter);
194
+ const dropdownOptions = [
195
+ {
196
+ key: ALL_SYSTEM_TYPES_VALUE,
197
+ value: ALL_SYSTEM_TYPES_VALUE,
198
+ text: formatMessage({
199
+ id: "lineage.graph.toolbar.system_filter.all",
200
+ defaultMessage: "All systems",
201
+ }),
202
+ },
203
+ ...systemTypeOptions.map((option) => ({
204
+ key: option.key,
205
+ value: option.key,
206
+ text: option.label,
207
+ })),
208
+ ];
209
+
210
+ return (
211
+ <section
212
+ aria-label={formatMessage({
213
+ id: "lineage.graph.toolbar.search.label",
214
+ defaultMessage: "Search graph nodes",
215
+ })}
216
+ className="lineage-graph__tool-panel lineage-graph__tool-panel--search"
217
+ >
218
+ <PanelHeader
219
+ icon="search"
220
+ onClose={onClose}
221
+ title={formatMessage({
222
+ id: "lineage.graph.toolbar.search.header",
223
+ defaultMessage: "Search nodes",
224
+ })}
225
+ />
226
+ <div className="lineage-graph__search-fields">
227
+ <Input
228
+ aria-label={formatMessage({
229
+ id: "lineage.graph.toolbar.search.label",
230
+ defaultMessage: "Search graph nodes",
231
+ })}
232
+ className="lineage-graph__search-input"
233
+ icon="search"
234
+ iconPosition="left"
235
+ onChange={(event) => onSearchInputChange(event.target.value)}
236
+ placeholder={formatMessage({
237
+ id: "lineage.graph.toolbar.search.placeholder",
238
+ defaultMessage: "Search by name or identifier…",
239
+ })}
240
+ value={searchInput}
241
+ />
242
+ <Dropdown
243
+ aria-label={formatMessage({
244
+ id: "lineage.graph.toolbar.system_filter.label",
245
+ defaultMessage: "Filter by system type",
246
+ })}
247
+ className="lineage-graph__search-system-filter"
248
+ fluid
249
+ onChange={(_event, { value }) => onSystemTypeFilterChange(value)}
250
+ options={dropdownOptions}
251
+ selection
252
+ value={systemTypeFilter || ALL_SYSTEM_TYPES_VALUE}
253
+ />
254
+ </div>
255
+ {!hasFilter ? (
256
+ <div className="lineage-graph__search-empty">
257
+ <DockIcon name="mouse pointer" />
258
+ {formatMessage({
259
+ id: "lineage.graph.toolbar.search.hint",
260
+ defaultMessage: "Type a node name or choose a system.",
261
+ })}
262
+ </div>
263
+ ) : null}
264
+ {hasFilter && _.isEmpty(searchResults) ? (
265
+ <div className="lineage-graph__search-empty">
266
+ <DockIcon name="search" />
267
+ {formatMessage({
268
+ id: "lineage.graph.toolbar.search.empty",
269
+ defaultMessage: "No matching nodes",
270
+ })}
271
+ </div>
272
+ ) : null}
273
+ {!_.isEmpty(searchResults) ? (
274
+ <>
275
+ <div className="lineage-graph__search-count">
276
+ {formatMessage(
277
+ {
278
+ id: "lineage.graph.toolbar.search.results",
279
+ defaultMessage: "{count} results",
280
+ },
281
+ { count: searchResultsTotal },
282
+ )}
283
+ </div>
284
+ <div
285
+ aria-label={formatMessage({
286
+ id: "lineage.graph.toolbar.search.results.label",
287
+ defaultMessage: "Matching graph nodes",
288
+ })}
289
+ className="lineage-graph__search-results"
290
+ role="listbox"
291
+ >
292
+ {searchResults.map((result) => (
293
+ <button
294
+ aria-selected={selectedGraphNodeId === result.graphNodeId}
295
+ className="lineage-graph__search-result"
296
+ key={result.graphNodeId}
297
+ onClick={() => onSearchResultSelect(result.graphNodeId)}
298
+ role="option"
299
+ type="button"
300
+ >
301
+ <span className="lineage-graph__search-result-icon">
302
+ <DockIcon name={result.iconName} />
303
+ </span>
304
+ <span className="lineage-graph__search-result-copy">
305
+ <span title={result.label}>{result.label}</span>
306
+ <small>{result.contextLabel}</small>
307
+ </span>
308
+ </button>
309
+ ))}
310
+ </div>
311
+ </>
312
+ ) : null}
313
+ </section>
314
+ );
315
+ };
316
+
317
+ SearchPanel.propTypes = {
318
+ onClose: PropTypes.func.isRequired,
319
+ onSearchInputChange: PropTypes.func.isRequired,
320
+ onSearchResultSelect: PropTypes.func.isRequired,
321
+ onSystemTypeFilterChange: PropTypes.func.isRequired,
322
+ searchInput: PropTypes.string,
323
+ searchResults: PropTypes.array,
324
+ searchResultsTotal: PropTypes.number,
325
+ selectedGraphNodeId: PropTypes.string,
326
+ systemTypeFilter: PropTypes.string,
327
+ systemTypeOptions: PropTypes.array,
328
+ };
329
+
330
+ export const visibleLegendKeys = (
331
+ nodes = [],
332
+ edges = [],
333
+ graphType = "impact",
334
+ ) => {
335
+ const visible = nodes.filter((node) => !node.hidden);
336
+ const classes = (node) => node.className || "";
337
+ const fields = visible.flatMap(({ data = {} }) =>
338
+ data.fieldLike
339
+ ? [{ ...data.lineageNode, ...data.lineageNode?.metadata }]
340
+ : (data.fields || []).slice(0, data.fieldHandleLimit),
341
+ );
342
+ const features = {
343
+ system: visible.some(
344
+ (node) =>
345
+ classes(node).includes("td-lineage-catalog--present") &&
346
+ (classes(node).includes("--entity-system") ||
347
+ classes(node).includes("td-lineage-entity-color--system")),
348
+ ),
349
+ types: visible.some(
350
+ (node) =>
351
+ !node.data?.fieldLike &&
352
+ node.data?.entityType &&
353
+ node.data.entityType !== "system",
354
+ ),
355
+ absent: visible.some((node) =>
356
+ classes(node).includes("td-lineage-catalog--absent"),
357
+ ),
358
+ deleted: visible.some((node) =>
359
+ classes(node).includes("td-lineage-catalog--deleted"),
360
+ ),
361
+ selection: visible.some(
362
+ (node) => node.data?.isActive || node.data?.focusSelected,
363
+ ),
364
+ [graphType === "impact" ? "impact" : "lineage"]: edges.some(
365
+ (edge) => !edge.hidden && !edge.data?.containment,
366
+ ),
367
+ PK: fields.some(
368
+ (field) => field?.isPK || field?.is_pk || field?.primary_key,
369
+ ),
370
+ FK: fields.some(
371
+ (field) => field?.isFK || field?.is_fk || field?.foreign_key,
372
+ ),
373
+ CDE: fields.some(
374
+ (field) =>
375
+ field?.isCDE ||
376
+ field?.is_cde ||
377
+ field?.critical_data_element ||
378
+ field?.metadata?.isCDE ||
379
+ field?.metadata?.is_cde,
380
+ ),
381
+ };
382
+ return Object.keys(features).filter((key) => features[key]);
383
+ };
384
+
385
+ const BASIC_LEGEND = [
386
+ ["system", "SYSTEM in TrueDat"],
387
+ ["types", "Other node colors identify their type"],
388
+ ["absent", "Not in TrueDat · gray dashed border"],
389
+ ["deleted", "Deleted structure · red dashed border"],
390
+ ["selection", "Orange outline · selected node"],
391
+ ["impact", "Impact dependencies"],
392
+ ["lineage", "Traceability dependencies"],
393
+ ];
394
+
395
+ const LegendPanel = ({ onClose, visibleKeys }) => {
396
+ const { formatMessage } = useIntl();
397
+ return (
398
+ <section
399
+ aria-label={formatMessage({
400
+ id: "lineage.graph.legend.header",
401
+ defaultMessage: "Lineage legend",
402
+ })}
403
+ className="lineage-graph__tool-panel lineage-graph__tool-panel--legend"
404
+ >
405
+ <PanelHeader
406
+ icon="list ul"
407
+ onClose={onClose}
408
+ title={formatMessage({
409
+ id: "lineage.graph.legend.header",
410
+ defaultMessage: "Lineage legend",
411
+ })}
412
+ />
413
+ <div className="lineage-graph__legend-section">
414
+ {BASIC_LEGEND.filter(([key]) => visibleKeys.includes(key)).map(
415
+ ([key, defaultMessage]) => (
416
+ <div className="lineage-graph__legend-item" key={key}>
417
+ <span
418
+ aria-hidden="true"
419
+ className={`lineage-graph__basic-legend-swatch lineage-graph__basic-legend-swatch--${key}`}
420
+ />
421
+ <span>
422
+ {formatMessage({
423
+ id: `lineage.graph.legend.basic.${key}`,
424
+ defaultMessage,
425
+ })}
426
+ </span>
427
+ </div>
428
+ ),
429
+ )}
430
+ </div>
431
+ <div className="lineage-graph__legend-section">
432
+ {[
433
+ ["PK", "Primary key"],
434
+ ["FK", "Foreign key"],
435
+ ["CDE", "Critical data element"],
436
+ ]
437
+ .filter(([badge]) => visibleKeys.includes(badge))
438
+ .map(([badge, defaultMessage]) => (
439
+ <div className="lineage-graph__legend-item" key={badge}>
440
+ <span className="lineage-graph__basic-legend-badge">{badge}</span>
441
+ <span>
442
+ {formatMessage({
443
+ id: `lineage.graph.legend.basic.${badge}`,
444
+ defaultMessage,
445
+ })}
446
+ </span>
447
+ </div>
448
+ ))}
449
+ </div>
450
+ </section>
451
+ );
452
+ };
453
+ LegendPanel.propTypes = {
454
+ onClose: PropTypes.func.isRequired,
455
+ visibleKeys: PropTypes.array.isRequired,
456
+ };
457
+
458
+ const contextLabel = (context) => {
459
+ if (context?.type === "node") return context.node?.name || context.node?.id;
460
+ if (context?.type === "edge") {
461
+ return `${context.edge?.sourceName || context.edge?.source} → ${
462
+ context.edge?.targetName || context.edge?.target
463
+ }`;
464
+ }
465
+
466
+ return "";
467
+ };
468
+
469
+ const contextIcon = (context) =>
470
+ context?.type === "node"
471
+ ? context.node?.iconName || "database"
472
+ : context?.iconName || "exchange";
473
+
474
+ const normalizeEntityType = (value) => {
475
+ if (value === undefined || value === null) return undefined;
476
+
477
+ const normalized = `${value}`
478
+ .trim()
479
+ .toLowerCase()
480
+ .replace(/^lineage[\s_-]*/, "")
481
+ .replace(/[\s_]+/g, "-");
482
+
483
+ return normalized || undefined;
484
+ };
485
+
486
+ const capitalize = (value = "") =>
487
+ value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
488
+
489
+ const contextTypeLabel = (context, formatMessage) => {
490
+ if (context?.type === "edge") {
491
+ return formatMessage({
492
+ id: "lineage.inspector.edge",
493
+ defaultMessage: "Connection metadata",
494
+ });
495
+ }
496
+
497
+ const entityType =
498
+ normalizeEntityType(context?.node?.entityType) ||
499
+ normalizeEntityType(context?.node?.type);
500
+
501
+ if (entityType) {
502
+ return formatMessage({
503
+ id: `lineage.graph.entity_type.${entityType}`,
504
+ defaultMessage: capitalize(entityType),
505
+ });
506
+ }
507
+
508
+ const resourceSelected = context?.node?.lineageKind === "resource";
509
+
510
+ return formatMessage({
511
+ id: resourceSelected
512
+ ? "lineage.inspector.resource"
513
+ : "lineage.inspector.group",
514
+ defaultMessage: resourceSelected ? "Data structure" : "Group",
515
+ });
516
+ };
517
+
518
+ export const LineageGraphToolbar = ({
519
+ activePanel,
520
+ canCollapseAll = true,
521
+ canExpandAll = true,
522
+ collapsible,
523
+ context,
524
+ doubleClickExpandsNodes = false,
525
+ legendItems = [],
526
+ visibleLegend = [],
527
+ onClosePanel,
528
+ onClearContext,
529
+ onCollapseAll,
530
+ onDoubleClickExpandsNodesChange,
531
+ onExpandAll,
532
+ onExpansionDepthChange,
533
+ onMinimizeInspector,
534
+ onPanelChange,
535
+ onSearchInputChange,
536
+ onSearchResultSelect,
537
+ onSystemTypeFilterChange,
538
+ onTogglePin,
539
+ onUnpin,
540
+ onCenterNode,
541
+ pinned,
542
+ expansionDepth = 4,
543
+ maxExpansionDepth = expansionDepth,
544
+ searchInput = "",
545
+ searchResults = [],
546
+ searchResultsTotal = 0,
547
+ selectedGraphNodeId,
548
+ simplified = false,
549
+ systemTypeFilter = "",
550
+ }) => {
551
+ const { formatMessage } = useIntl();
552
+ const [depthPopoverOpen, setDepthPopoverOpen] = useState(false);
553
+ const [settingsPopoverOpen, setSettingsPopoverOpen] = useState(false);
554
+ const hasContext = Boolean(context);
555
+ const inspectorOpen =
556
+ !simplified && hasContext && activePanel === "inspector";
557
+ const selectedLabel = contextLabel(context);
558
+ const selectedTypeLabel = hasContext
559
+ ? contextTypeLabel(context, formatMessage)
560
+ : "";
561
+ const togglePanel = (panel) =>
562
+ onPanelChange(activePanel === panel ? null : panel);
563
+ const clearContextLabel = formatMessage(
564
+ {
565
+ id: "lineage.graph.toolbar.context.clear",
566
+ defaultMessage: "Clear selection for {name}",
567
+ },
568
+ { name: selectedLabel },
569
+ );
570
+ const visibleLevelsLabel = formatMessage({
571
+ id: "lineage.graph.controls.visible_levels",
572
+ defaultMessage: "Visible levels",
573
+ });
574
+ const settingsLabel = formatMessage({
575
+ id: "lineage.graph.settings.header",
576
+ defaultMessage: "Graph settings",
577
+ });
578
+ const doubleClickInfoLabel = formatMessage({
579
+ id: "lineage.graph.settings.double_click_info",
580
+ defaultMessage: "Double-click expands or collapses nodes",
581
+ });
582
+
583
+ return (
584
+ <div
585
+ className="lineage-graph__dock nodrag nopan nowheel"
586
+ onClick={stopGraphInteraction}
587
+ onMouseDown={stopGraphInteraction}
588
+ onPointerDown={stopGraphInteraction}
589
+ onWheel={stopGraphInteraction}
590
+ >
591
+ <div
592
+ className={`lineage-graph__dock-context${
593
+ hasContext ? " lineage-graph__dock-context--selected" : ""
594
+ }${
595
+ inspectorOpen ? " lineage-graph__dock-context--inspector-open" : ""
596
+ }${pinned ? " lineage-graph__dock-context--pinned" : ""}`}
597
+ >
598
+ {inspectorOpen ? (
599
+ <LineageInspectorPanel
600
+ context={context}
601
+ onClose={onClosePanel}
602
+ onMinimize={onMinimizeInspector}
603
+ onTogglePin={onTogglePin}
604
+ onCenterNode={onCenterNode}
605
+ pinned={pinned}
606
+ showHeader={false}
607
+ />
608
+ ) : null}
609
+ {hasContext ? (
610
+ <div
611
+ className={`lineage-graph__dock-context-bar${
612
+ inspectorOpen ? " lineage-graph__dock-context-bar--expanded" : ""
613
+ }${
614
+ context?.type === "edge"
615
+ ? " lineage-graph__dock-context-bar--edge"
616
+ : ""
617
+ }`}
618
+ >
619
+ {pinned ? (
620
+ <button
621
+ aria-label={formatMessage({
622
+ id: "lineage.context_menu.unpin_focus",
623
+ defaultMessage: "Unpin focus",
624
+ })}
625
+ aria-pressed="true"
626
+ className="lineage-graph__dock-context-unpin"
627
+ onClick={onUnpin}
628
+ type="button"
629
+ >
630
+ <DockIcon name="pin" />
631
+ </button>
632
+ ) : null}
633
+ <div
634
+ aria-live="polite"
635
+ className="lineage-graph__dock-context-reopen lineage-graph__dock-context-button"
636
+ >
637
+ <span
638
+ aria-live="polite"
639
+ className="lineage-graph__dock-context-summary"
640
+ >
641
+ <span
642
+ aria-hidden="true"
643
+ className="lineage-graph__dock-context-summary-icon"
644
+ >
645
+ <DockIcon name={contextIcon(context)} />
646
+ </span>
647
+ <span className="lineage-graph__dock-context-copy">
648
+ <span className="lineage-graph__dock-context-label">
649
+ {selectedLabel}
650
+ </span>
651
+ <span className="lineage-graph__dock-context-type">
652
+ {selectedTypeLabel}
653
+ </span>
654
+ </span>
655
+ </span>
656
+ </div>
657
+ <button
658
+ aria-label={clearContextLabel}
659
+ className="lineage-graph__dock-context-clear"
660
+ onClick={onClearContext}
661
+ type="button"
662
+ >
663
+ <DockIcon name="close" />
664
+ </button>
665
+ </div>
666
+ ) : (
667
+ <LineageToolButton
668
+ active={activePanel === "legend"}
669
+ aria-expanded={activePanel === "legend"}
670
+ className="lineage-graph__dock-context-button"
671
+ icon="list ul"
672
+ label={formatMessage({
673
+ id: "lineage.graph.legend.header",
674
+ defaultMessage: "Lineage legend",
675
+ })}
676
+ onClick={() => togglePanel("legend")}
677
+ />
678
+ )}
679
+ </div>
680
+ {!simplified && (
681
+ <LineageToolButton
682
+ active={activePanel === "search"}
683
+ aria-expanded={activePanel === "search"}
684
+ disabled={pinned}
685
+ icon="search"
686
+ label={formatMessage({
687
+ id: "lineage.graph.toolbar.search.header",
688
+ defaultMessage: "Search nodes",
689
+ })}
690
+ onClick={() => togglePanel("search")}
691
+ />
692
+ )}
693
+ <LineageDownloadButton
694
+ basic
695
+ className="lineage-graph__dock-button lineage-graph__dock-download"
696
+ compact
697
+ size="mini"
698
+ tooltipPosition="left center"
699
+ />
700
+ {collapsible && !simplified ? (
701
+ <>
702
+ <span className="lineage-graph__dock-divider" aria-hidden="true" />
703
+ <Popup
704
+ on="click"
705
+ onClose={() => setSettingsPopoverOpen(false)}
706
+ onOpen={() => setSettingsPopoverOpen(true)}
707
+ open={settingsPopoverOpen}
708
+ position="left center"
709
+ trigger={
710
+ <LineageToolButton
711
+ active={settingsPopoverOpen}
712
+ aria-expanded={settingsPopoverOpen}
713
+ aria-haspopup="dialog"
714
+ className="lineage-graph__dock-settings-button"
715
+ icon={
716
+ <Settings
717
+ size={DOCK_ICON_SIZE}
718
+ strokeWidth={DOCK_ICON_STROKE}
719
+ />
720
+ }
721
+ label={settingsLabel}
722
+ showTooltip={false}
723
+ />
724
+ }
725
+ content={
726
+ <div
727
+ aria-label={settingsLabel}
728
+ className="lineage-graph__settings-popover"
729
+ role="dialog"
730
+ >
731
+ <span className="lineage-graph__settings-popover-header">
732
+ {settingsLabel}
733
+ </span>
734
+ <label className="lineage-graph__settings-toggle">
735
+ <input
736
+ checked={doubleClickExpandsNodes}
737
+ onChange={(event) =>
738
+ onDoubleClickExpandsNodesChange?.(event.target.checked)
739
+ }
740
+ type="checkbox"
741
+ />
742
+ <span>{doubleClickInfoLabel}</span>
743
+ </label>
744
+ </div>
745
+ }
746
+ className="lineage-graph__settings-popup"
747
+ />
748
+ <Popup
749
+ on="click"
750
+ onClose={() => setDepthPopoverOpen(false)}
751
+ onOpen={() => setDepthPopoverOpen(true)}
752
+ open={depthPopoverOpen}
753
+ position="left center"
754
+ trigger={
755
+ <LineageToolButton
756
+ active={depthPopoverOpen}
757
+ aria-expanded={depthPopoverOpen}
758
+ aria-haspopup="dialog"
759
+ className="lineage-graph__dock-depth-button"
760
+ icon={
761
+ <Layers
762
+ size={DOCK_ICON_SIZE}
763
+ strokeWidth={DOCK_ICON_STROKE}
764
+ />
765
+ }
766
+ label={visibleLevelsLabel}
767
+ showTooltip={false}
768
+ >
769
+ <span className="lineage-graph__dock-depth-value">
770
+ {expansionDepth}
771
+ </span>
772
+ </LineageToolButton>
773
+ }
774
+ content={
775
+ <div
776
+ aria-label={visibleLevelsLabel}
777
+ className="lineage-graph__depth-popover"
778
+ role="dialog"
779
+ >
780
+ <div className="lineage-graph__depth-popover-header">
781
+ <span>{visibleLevelsLabel}</span>
782
+ </div>
783
+ <input
784
+ aria-label={visibleLevelsLabel}
785
+ className="lineage-graph__depth-slider"
786
+ type="range"
787
+ min={0}
788
+ max={maxExpansionDepth}
789
+ value={expansionDepth}
790
+ onChange={(event) =>
791
+ onExpansionDepthChange(event.target.value)
792
+ }
793
+ />
794
+ <div
795
+ aria-hidden="true"
796
+ className="lineage-graph__depth-popover-scale"
797
+ >
798
+ {Array.from({ length: maxExpansionDepth + 1 }, (_, depth) => (
799
+ <span
800
+ key={depth}
801
+ className={`${
802
+ expansionDepth >= depth ? "is-active" : ""
803
+ }${expansionDepth === depth ? " is-current" : ""}`}
804
+ >
805
+ {depth}
806
+ </span>
807
+ ))}
808
+ </div>
809
+ </div>
810
+ }
811
+ className="lineage-graph__depth-popup"
812
+ />
813
+ </>
814
+ ) : null}
815
+ {(collapsible || simplified) && (canCollapseAll || canExpandAll) ? (
816
+ <>
817
+ <span className="lineage-graph__dock-divider" aria-hidden="true" />
818
+ {canCollapseAll ? (
819
+ <LineageToolButton
820
+ icon="compress"
821
+ label={formatMessage({
822
+ id: "lineage.graph.controls.collapse_all",
823
+ defaultMessage: "Collapse all",
824
+ })}
825
+ onClick={onCollapseAll}
826
+ />
827
+ ) : null}
828
+ {canExpandAll ? (
829
+ <LineageToolButton
830
+ icon="expand arrows alternate"
831
+ label={formatMessage({
832
+ id: "lineage.graph.controls.expand_all",
833
+ defaultMessage: "Expand all",
834
+ })}
835
+ onClick={onExpandAll}
836
+ />
837
+ ) : null}
838
+ </>
839
+ ) : null}
840
+ <span className="lineage-graph__dock-divider" aria-hidden="true" />
841
+ {!simplified && activePanel === "search" ? (
842
+ <SearchPanel
843
+ onClose={onClosePanel}
844
+ onSearchInputChange={onSearchInputChange}
845
+ onSearchResultSelect={onSearchResultSelect}
846
+ onSystemTypeFilterChange={onSystemTypeFilterChange}
847
+ searchInput={searchInput}
848
+ searchResults={searchResults}
849
+ searchResultsTotal={searchResultsTotal}
850
+ selectedGraphNodeId={selectedGraphNodeId}
851
+ systemTypeFilter={systemTypeFilter}
852
+ systemTypeOptions={legendItems}
853
+ />
854
+ ) : null}
855
+ {!hasContext && activePanel === "legend" ? (
856
+ <LegendPanel onClose={onClosePanel} visibleKeys={visibleLegend} />
857
+ ) : null}
858
+ </div>
859
+ );
860
+ };
861
+
862
+ LineageGraphToolbar.propTypes = {
863
+ activePanel: PropTypes.oneOf(["search", "legend", "inspector"]),
864
+ canCollapseAll: PropTypes.bool,
865
+ canExpandAll: PropTypes.bool,
866
+ collapsible: PropTypes.bool,
867
+ context: PropTypes.object,
868
+ doubleClickExpandsNodes: PropTypes.bool,
869
+ legendItems: PropTypes.array,
870
+ visibleLegend: PropTypes.array,
871
+ onClosePanel: PropTypes.func.isRequired,
872
+ onClearContext: PropTypes.func.isRequired,
873
+ onCollapseAll: PropTypes.func,
874
+ onDoubleClickExpandsNodesChange: PropTypes.func,
875
+ onExpandAll: PropTypes.func,
876
+ onExpansionDepthChange: PropTypes.func.isRequired,
877
+ onMinimizeInspector: PropTypes.func.isRequired,
878
+ onPanelChange: PropTypes.func.isRequired,
879
+ onSearchInputChange: PropTypes.func.isRequired,
880
+ onSearchResultSelect: PropTypes.func.isRequired,
881
+ onSystemTypeFilterChange: PropTypes.func.isRequired,
882
+ onTogglePin: PropTypes.func,
883
+ onUnpin: PropTypes.func,
884
+ onCenterNode: PropTypes.func,
885
+ pinned: PropTypes.bool,
886
+ expansionDepth: PropTypes.number,
887
+ maxExpansionDepth: PropTypes.number,
888
+ searchInput: PropTypes.string,
889
+ searchResults: PropTypes.array,
890
+ searchResultsTotal: PropTypes.number,
891
+ selectedGraphNodeId: PropTypes.string,
892
+ simplified: PropTypes.bool,
893
+ systemTypeFilter: PropTypes.string,
894
+ };
895
+
896
+ export default LineageGraphToolbar;