@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,748 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ import { connect } from "react-redux";
5
+ import { Link } from "react-router";
6
+ import { Icon, Popup } from "semantic-ui-react";
7
+ import { FormattedMessage, useIntl } from "react-intl";
8
+ import { linkTo } from "@truedat/core/routes";
9
+ import { excludeNode, createGraph } from "../routines";
10
+
11
+ const compactText = (value) => {
12
+ if (value === undefined || value === null) return "";
13
+ if (typeof value === "object") {
14
+ return compactText(
15
+ value.name || value.label || value.value || value.external_id || value.id,
16
+ );
17
+ }
18
+
19
+ return `${value}`;
20
+ };
21
+
22
+ const normalizeNodeEntityType = (node = {}) => {
23
+ const raw = node.entityType || node.type;
24
+
25
+ if (raw === undefined || raw === null) return undefined;
26
+
27
+ const normalized = compactText(raw)
28
+ .trim()
29
+ .toLowerCase()
30
+ .replace(/^lineage[\s_-]*/, "")
31
+ .replace(/[\s_]+/g, "-");
32
+
33
+ return normalized || undefined;
34
+ };
35
+
36
+ const capitalize = (value = "") =>
37
+ value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
38
+
39
+ const firstText = (...values) =>
40
+ values.map(compactText).find((value) => !_.isEmpty(value));
41
+
42
+ const normalizeId = (value) =>
43
+ value === undefined || value === null ? "" : `${value}`;
44
+
45
+ export const formatMetadataValue = (value) => {
46
+ if (value === undefined || value === null || value === "") return "—";
47
+ if (typeof value === "string") return value;
48
+ if (typeof value === "number" || typeof value === "boolean") {
49
+ return `${value}`;
50
+ }
51
+
52
+ try {
53
+ return JSON.stringify(value, null, 2);
54
+ } catch {
55
+ return `${value}`;
56
+ }
57
+ };
58
+
59
+ const qualityLevel = (quality) => {
60
+ const score = Number.parseFloat(quality);
61
+
62
+ if (!Number.isFinite(score)) return "neutral";
63
+ if (score >= 90) return "good";
64
+ if (score >= 75) return "warning";
65
+ return "poor";
66
+ };
67
+
68
+ const nodePath = (node = {}) => {
69
+ const path = Array.isArray(node.path) ? node.path.map(compactText) : [];
70
+ const selectedName = firstText(node.name, node.label, node.id);
71
+
72
+ if (!_.isEmpty(path)) {
73
+ return path.at(-1) === selectedName ? path : [...path, selectedName];
74
+ }
75
+
76
+ return [
77
+ firstText(node.system, node.system_name),
78
+ firstText(node.schema, node.schema_name, node.database),
79
+ selectedName,
80
+ ]
81
+ .filter(Boolean)
82
+ .filter((part, index, parts) => index === 0 || part !== parts[index - 1]);
83
+ };
84
+
85
+ export const HierarchyPath = ({ parts }) => {
86
+ const measureRef = useRef(null);
87
+ const [truncated, setTruncated] = useState(false);
88
+ const fullPath = parts.join(" › ");
89
+ const summary =
90
+ parts.length > 1 ? `${parts[0]} › … › ${parts.at(-1)}` : fullPath;
91
+
92
+ useLayoutEffect(() => {
93
+ const measure = measureRef.current;
94
+ if (!measure) return undefined;
95
+
96
+ const update = () => {
97
+ const lineHeight = Number.parseFloat(
98
+ getComputedStyle(measure).lineHeight,
99
+ );
100
+ setTruncated(measure.scrollHeight > (lineHeight || 17.4) * 2 + 1);
101
+ };
102
+
103
+ update();
104
+ if (typeof ResizeObserver === "undefined") return undefined;
105
+
106
+ const observer = new ResizeObserver(update);
107
+ observer.observe(measure);
108
+
109
+ return () => observer.disconnect();
110
+ }, [fullPath]);
111
+
112
+ const path = (
113
+ <span
114
+ className={`lineage-inspector__path${
115
+ truncated ? " lineage-inspector__path--truncated" : ""
116
+ }`}
117
+ tabIndex={truncated ? 0 : undefined}
118
+ >
119
+ {truncated ? summary : fullPath}
120
+ </span>
121
+ );
122
+
123
+ return (
124
+ <div className="lineage-inspector__path-wrap">
125
+ <span
126
+ aria-hidden="true"
127
+ className="lineage-inspector__path lineage-inspector__path--measure"
128
+ ref={measureRef}
129
+ >
130
+ {fullPath}
131
+ </span>
132
+ {truncated ? (
133
+ <Popup
134
+ content={fullPath}
135
+ on={["hover", "focus"]}
136
+ position="top center"
137
+ trigger={path}
138
+ />
139
+ ) : (
140
+ path
141
+ )}
142
+ </div>
143
+ );
144
+ };
145
+
146
+ HierarchyPath.propTypes = {
147
+ parts: PropTypes.arrayOf(PropTypes.string).isRequired,
148
+ };
149
+
150
+ const InspectorHeader = ({ context, onMinimize, pinned }) => {
151
+ const { formatMessage } = useIntl();
152
+ const edgeSelected = context.type === "edge";
153
+ const entityLabel = normalizeNodeEntityType(context.node);
154
+ const title = edgeSelected
155
+ ? `${context.edge?.sourceName || context.edge?.source} → ${
156
+ context.edge?.targetName || context.edge?.target
157
+ }`
158
+ : firstText(context.node?.name, context.node?.label, context.node?.id);
159
+
160
+ return (
161
+ <div
162
+ className={`lineage-inspector__header${
163
+ edgeSelected ? " lineage-inspector__header--edge" : ""
164
+ }`}
165
+ >
166
+ <button
167
+ aria-label={formatMessage({
168
+ id: "lineage.inspector.minimize",
169
+ defaultMessage: "Minimize inspector",
170
+ })}
171
+ className={`lineage-inspector__minimize${
172
+ pinned ? " lineage-inspector__minimize--pinned" : ""
173
+ }`}
174
+ onClick={onMinimize}
175
+ type="button"
176
+ >
177
+ <Icon name="chevron right" />
178
+ </button>
179
+ <span className="lineage-inspector__header-icon" aria-hidden="true">
180
+ <Icon
181
+ name={
182
+ edgeSelected
183
+ ? context.iconName || "exchange"
184
+ : context.node?.iconName || "database"
185
+ }
186
+ />
187
+ </span>
188
+ <div className="lineage-inspector__header-copy">
189
+ <span className="lineage-inspector__eyebrow">
190
+ {edgeSelected
191
+ ? formatMessage({
192
+ id: "lineage.inspector.edge",
193
+ defaultMessage: "Connection metadata",
194
+ })
195
+ : entityLabel
196
+ ? formatMessage({
197
+ id: `lineage.graph.entity_type.${entityLabel}`,
198
+ defaultMessage: capitalize(entityLabel),
199
+ })
200
+ : formatMessage({
201
+ id:
202
+ context.node?.lineageKind === "resource"
203
+ ? "lineage.inspector.resource"
204
+ : "lineage.inspector.group",
205
+ defaultMessage:
206
+ context.node?.lineageKind === "resource"
207
+ ? "Data structure"
208
+ : "Group",
209
+ })}
210
+ </span>
211
+ <strong title={title}>{title}</strong>
212
+ </div>
213
+ </div>
214
+ );
215
+ };
216
+
217
+ InspectorHeader.propTypes = {
218
+ context: PropTypes.object.isRequired,
219
+ onMinimize: PropTypes.func.isRequired,
220
+ pinned: PropTypes.bool,
221
+ };
222
+
223
+ const NodeInspector = ({
224
+ createGraph,
225
+ excludeNode,
226
+ levels,
227
+ node: selectedNode,
228
+ onAction,
229
+ onCenterNode,
230
+ onTogglePin,
231
+ pinned,
232
+ }) => {
233
+ const { formatMessage } = useIntl();
234
+ const [originalId, setOriginalId] = useState(null);
235
+ const original =
236
+ selectedNode.originals?.find(({ id }) => id === originalId) ||
237
+ selectedNode.originals?.[0];
238
+ const node = original
239
+ ? { ...selectedNode, ...original, structure_id: original.structure_id }
240
+ : selectedNode;
241
+ const nodeId = normalizeId(node.id);
242
+ const isResource = node.lineageKind === "resource";
243
+ const structureId = node.structure_id;
244
+ const path = nodePath(node);
245
+ const systemTypeCandidate = firstText(
246
+ node.systemTypeLabel,
247
+ node.system_group_name,
248
+ node.system_group,
249
+ );
250
+ const normalizedSystemType = compactText(systemTypeCandidate)
251
+ .trim()
252
+ .toLowerCase()
253
+ .replace(/[\s_]+/g, " ");
254
+ const systemType =
255
+ !normalizedSystemType ||
256
+ normalizedSystemType === "without system group" ||
257
+ normalizedSystemType.startsWith("without system") ||
258
+ normalizedSystemType.startsWith("sin grupo") ||
259
+ normalizedSystemType === "sin sistema" ||
260
+ normalizedSystemType === "unknown" ||
261
+ normalizedSystemType === "n/a"
262
+ ? ""
263
+ : systemTypeCandidate;
264
+ const status = firstText(node.status, node.health, node.state);
265
+ const nodeType = normalizeNodeEntityType(node);
266
+ const quality = firstText(
267
+ node.quality,
268
+ node.quality_score,
269
+ node.metadata?.quality,
270
+ node.metadata?.quality_score,
271
+ node.metadata?.score,
272
+ );
273
+ const fieldsKnown =
274
+ node.fieldsKnown ??
275
+ [
276
+ node.fields,
277
+ node.columns,
278
+ node.metadata?.fields,
279
+ node.metadata?.columns,
280
+ node.data_fields,
281
+ ].some(Array.isArray);
282
+ const fieldCount =
283
+ fieldsKnown && (nodeType || "").toLowerCase() !== "column"
284
+ ? Array.isArray(node.fields)
285
+ ? node.fields.length
286
+ : node.totalFields
287
+ : undefined;
288
+ const handleCreateGraph = (type) => () => {
289
+ onAction();
290
+ createGraph({
291
+ ids: [nodeId],
292
+ levels,
293
+ type,
294
+ isRedirected: true,
295
+ });
296
+ };
297
+ const handleExclude = () => {
298
+ onAction();
299
+ excludeNode({ id: nodeId, isRedirected: true });
300
+ };
301
+
302
+ return (
303
+ <div className="lineage-inspector__body">
304
+ {!_.isEmpty(path) ? (
305
+ <section className="lineage-inspector__section">
306
+ <span className="lineage-inspector__section-title">
307
+ <FormattedMessage
308
+ id="lineage.inspector.path"
309
+ defaultMessage="Hierarchy path"
310
+ />
311
+ </span>
312
+ <HierarchyPath parts={path} />
313
+ </section>
314
+ ) : null}
315
+
316
+ {selectedNode.originals?.length > 1 ? (
317
+ <section className="lineage-inspector__section">
318
+ <label
319
+ htmlFor="lineage-original-resource"
320
+ className="lineage-inspector__section-title"
321
+ >
322
+ <FormattedMessage
323
+ id="lineage.inspector.original_resource"
324
+ defaultMessage="Original resource"
325
+ />
326
+ </label>
327
+ <select
328
+ id="lineage-original-resource"
329
+ className="lineage-inspector__original-resource"
330
+ value={nodeId}
331
+ onChange={(event) => setOriginalId(event.target.value)}
332
+ >
333
+ {selectedNode.originals.map(({ id }) => (
334
+ <option key={id} value={id}>
335
+ {id}
336
+ </option>
337
+ ))}
338
+ </select>
339
+ <p className="lineage-inspector__path">{nodeId}</p>
340
+ </section>
341
+ ) : null}
342
+
343
+ {systemType || status || nodeType || fieldCount !== undefined ? (
344
+ <section className="lineage-inspector__facts">
345
+ {systemType ? (
346
+ <div>
347
+ <span>
348
+ <FormattedMessage
349
+ id="lineage.inspector.system"
350
+ defaultMessage="System"
351
+ />
352
+ </span>
353
+ <strong>{systemType}</strong>
354
+ </div>
355
+ ) : null}
356
+ {status ? (
357
+ <div>
358
+ <span>
359
+ <FormattedMessage
360
+ id="lineage.inspector.status"
361
+ defaultMessage="Status"
362
+ />
363
+ </span>
364
+ <strong>{status}</strong>
365
+ </div>
366
+ ) : null}
367
+ {nodeType ? (
368
+ <div>
369
+ <span>
370
+ <FormattedMessage
371
+ id="lineage.inspector.type"
372
+ defaultMessage="Type"
373
+ />
374
+ </span>
375
+ <strong>
376
+ {formatMessage({
377
+ id: `lineage.graph.entity_type.${nodeType}`,
378
+ defaultMessage: capitalize(nodeType),
379
+ })}
380
+ </strong>
381
+ </div>
382
+ ) : null}
383
+ {fieldCount !== undefined ? (
384
+ <div>
385
+ <span>
386
+ <FormattedMessage
387
+ id="lineage.inspector.fields"
388
+ defaultMessage="Fields"
389
+ />
390
+ </span>
391
+ <strong>{fieldCount}</strong>
392
+ </div>
393
+ ) : null}
394
+ </section>
395
+ ) : null}
396
+
397
+ {quality ? (
398
+ <section
399
+ className={`lineage-inspector__quality lineage-inspector__quality--${qualityLevel(
400
+ quality,
401
+ )}`}
402
+ >
403
+ <span>
404
+ <Icon name="check circle outline" />
405
+ <FormattedMessage
406
+ id="lineage.inspector.quality"
407
+ defaultMessage="Data quality"
408
+ />
409
+ </span>
410
+ <strong>{quality}</strong>
411
+ </section>
412
+ ) : null}
413
+
414
+ <section className="lineage-inspector__section lineage-inspector__section--actions">
415
+ <div className="lineage-inspector__actions">
416
+ <div className="lineage-inspector__action-group lineage-inspector__action-group--local">
417
+ {onCenterNode ? (
418
+ <button
419
+ aria-label={formatMessage({
420
+ id: "lineage.inspector.center_node",
421
+ defaultMessage: "Center in graph",
422
+ })}
423
+ className="lineage-inspector__action"
424
+ onClick={() => onCenterNode(node.graphNodeId || node.id)}
425
+ type="button"
426
+ >
427
+ <Icon name="target" />
428
+ </button>
429
+ ) : null}
430
+ {onTogglePin ? (
431
+ <Popup
432
+ content={formatMessage({
433
+ id: pinned
434
+ ? "lineage.context_menu.unpin_focus"
435
+ : "lineage.context_menu.pin_focus",
436
+ defaultMessage: pinned ? "Unpin focus" : "Pin focus",
437
+ })}
438
+ mouseEnterDelay={120}
439
+ position="top center"
440
+ trigger={
441
+ <button
442
+ aria-label={formatMessage({
443
+ id: pinned
444
+ ? "lineage.context_menu.unpin_focus"
445
+ : "lineage.context_menu.pin_focus",
446
+ defaultMessage: pinned ? "Unpin focus" : "Pin focus",
447
+ })}
448
+ aria-pressed={pinned}
449
+ className={`lineage-inspector__action${
450
+ pinned ? " lineage-inspector__action--active" : ""
451
+ }`}
452
+ onClick={onTogglePin}
453
+ type="button"
454
+ >
455
+ <Icon
456
+ className={pinned ? "lineage-unpin-icon" : undefined}
457
+ name="pin"
458
+ />
459
+ </button>
460
+ }
461
+ />
462
+ ) : null}
463
+ {isResource ? (
464
+ <Popup
465
+ content={formatMessage({
466
+ id: "navigation.lineage.exclude",
467
+ defaultMessage: "Exclude",
468
+ })}
469
+ mouseEnterDelay={120}
470
+ position="top center"
471
+ trigger={
472
+ <button
473
+ aria-label={formatMessage({
474
+ id: "navigation.lineage.exclude",
475
+ defaultMessage: "Exclude",
476
+ })}
477
+ className="lineage-inspector__action lineage-inspector__action--danger"
478
+ onClick={handleExclude}
479
+ type="button"
480
+ >
481
+ <Icon name="eye slash outline" />
482
+ </button>
483
+ }
484
+ />
485
+ ) : null}
486
+ </div>
487
+ {isResource || structureId ? (
488
+ <span
489
+ aria-hidden="true"
490
+ className="lineage-inspector__action-divider"
491
+ />
492
+ ) : null}
493
+ <div className="lineage-inspector__action-group lineage-inspector__action-group--navigation">
494
+ {isResource ? (
495
+ <>
496
+ <Popup
497
+ content={formatMessage({
498
+ id: "navigation.lineage.lineage",
499
+ defaultMessage: "Lineage",
500
+ })}
501
+ mouseEnterDelay={120}
502
+ position="top center"
503
+ trigger={
504
+ <button
505
+ aria-label={formatMessage({
506
+ id: "navigation.lineage.lineage",
507
+ defaultMessage: "Lineage",
508
+ })}
509
+ className="lineage-inspector__action"
510
+ onClick={handleCreateGraph("lineage")}
511
+ type="button"
512
+ >
513
+ <Icon name="random" flipped="horizontally" />
514
+ </button>
515
+ }
516
+ />
517
+ <Popup
518
+ content={formatMessage({
519
+ id: "navigation.lineage.impact",
520
+ defaultMessage: "Impact",
521
+ })}
522
+ mouseEnterDelay={120}
523
+ position="top center"
524
+ trigger={
525
+ <button
526
+ aria-label={formatMessage({
527
+ id: "navigation.lineage.impact",
528
+ defaultMessage: "Impact",
529
+ })}
530
+ className="lineage-inspector__action"
531
+ onClick={handleCreateGraph("impact")}
532
+ type="button"
533
+ >
534
+ <Icon name="random" />
535
+ </button>
536
+ }
537
+ />
538
+ </>
539
+ ) : null}
540
+ {structureId ? (
541
+ <Popup
542
+ content={formatMessage({
543
+ id: "lineage.context_menu.quality_rules",
544
+ defaultMessage: "View Quality Rules",
545
+ })}
546
+ mouseEnterDelay={120}
547
+ position="top center"
548
+ trigger={
549
+ <Link
550
+ aria-label={formatMessage({
551
+ id: "lineage.context_menu.quality_rules",
552
+ defaultMessage: "View Quality Rules",
553
+ })}
554
+ className="lineage-inspector__action"
555
+ onClick={onAction}
556
+ to={linkTo.STRUCTURE_RULES({ id: structureId })}
557
+ target="_blank"
558
+ rel="noopener noreferrer"
559
+ >
560
+ <Icon name="check circle outline" />
561
+ </Link>
562
+ }
563
+ />
564
+ ) : null}
565
+ {structureId ? (
566
+ <Popup
567
+ content={formatMessage({
568
+ id: "navigation.lineage.view",
569
+ defaultMessage: "View in Data Catalog",
570
+ })}
571
+ mouseEnterDelay={120}
572
+ position="top center"
573
+ trigger={
574
+ <Link
575
+ aria-label={formatMessage({
576
+ id: "navigation.lineage.view",
577
+ defaultMessage: "View in Data Catalog",
578
+ })}
579
+ className="lineage-inspector__action lineage-inspector__action--view-detail"
580
+ onClick={onAction}
581
+ to={linkTo.STRUCTURE({ id: structureId })}
582
+ target="_blank"
583
+ rel="noopener noreferrer"
584
+ >
585
+ <Icon name="external alternate" />
586
+ </Link>
587
+ }
588
+ />
589
+ ) : null}
590
+ </div>
591
+ </div>
592
+ </section>
593
+ </div>
594
+ );
595
+ };
596
+
597
+ NodeInspector.propTypes = {
598
+ createGraph: PropTypes.func.isRequired,
599
+ excludeNode: PropTypes.func.isRequired,
600
+ levels: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
601
+ node: PropTypes.object.isRequired,
602
+ onAction: PropTypes.func.isRequired,
603
+ onCenterNode: PropTypes.func,
604
+ onTogglePin: PropTypes.func,
605
+ pinned: PropTypes.bool,
606
+ };
607
+
608
+ const EdgeInspector = ({ edge }) => {
609
+ const metadataPairs = _.toPairs(edge.metadata || {});
610
+ const relationType = String(edge.relationType || "").toUpperCase();
611
+
612
+ return (
613
+ <div className="lineage-inspector__body">
614
+ <section className="lineage-inspector__edge-summary">
615
+ <div>
616
+ <span>
617
+ <FormattedMessage
618
+ id="lineage.inspector.edge.source"
619
+ defaultMessage="Source"
620
+ />
621
+ </span>
622
+ <strong title={edge.originalSource}>
623
+ {edge.sourceName || edge.source}
624
+ </strong>
625
+ </div>
626
+ <Icon name="long arrow alternate right" />
627
+ <div>
628
+ <span>
629
+ <FormattedMessage
630
+ id="lineage.inspector.edge.target"
631
+ defaultMessage="Target"
632
+ />
633
+ </span>
634
+ <strong title={edge.originalTarget}>
635
+ {edge.targetName || edge.target}
636
+ </strong>
637
+ </div>
638
+ </section>
639
+ {["CONTAINS", "DEPENDS"].includes(relationType) ? (
640
+ <section className="lineage-inspector__relation-type">
641
+ <span>
642
+ <FormattedMessage
643
+ id="lineage.inspector.edge.type"
644
+ defaultMessage="Relation type"
645
+ />
646
+ </span>
647
+ <strong>{relationType}</strong>
648
+ </section>
649
+ ) : null}
650
+ {metadataPairs.length ? (
651
+ <section className="lineage-inspector__section lineage-inspector__metadata-section">
652
+ <dl className="lineage-inspector__metadata-list">
653
+ {metadataPairs.map(([key, value]) => (
654
+ <div className="lineage-inspector__metadata-item" key={key}>
655
+ <dt>{key}</dt>
656
+ <dd>
657
+ <pre>{formatMetadataValue(value)}</pre>
658
+ </dd>
659
+ </div>
660
+ ))}
661
+ </dl>
662
+ </section>
663
+ ) : null}
664
+ </div>
665
+ );
666
+ };
667
+
668
+ EdgeInspector.propTypes = {
669
+ edge: PropTypes.object.isRequired,
670
+ };
671
+
672
+ export const LineageInspectorPanel = ({
673
+ context,
674
+ createGraph,
675
+ excludeNode,
676
+ levels,
677
+ onClose,
678
+ onCenterNode,
679
+ onMinimize,
680
+ onTogglePin,
681
+ pinned,
682
+ showHeader = true,
683
+ }) => {
684
+ const { formatMessage } = useIntl();
685
+
686
+ if (!context) return null;
687
+
688
+ return (
689
+ <aside
690
+ aria-label={formatMessage({
691
+ id: "lineage.inspector.label",
692
+ defaultMessage: "Lineage inspector",
693
+ })}
694
+ className={`lineage-inspector${
695
+ showHeader ? "" : " lineage-inspector--body-only"
696
+ }`}
697
+ onClick={(event) => event.stopPropagation()}
698
+ onMouseDown={(event) => event.stopPropagation()}
699
+ >
700
+ {showHeader ? (
701
+ <InspectorHeader
702
+ context={context}
703
+ onMinimize={onMinimize}
704
+ pinned={pinned}
705
+ />
706
+ ) : null}
707
+ {context.type === "node" ? (
708
+ <NodeInspector
709
+ createGraph={createGraph}
710
+ excludeNode={excludeNode}
711
+ levels={levels}
712
+ node={context.node}
713
+ onAction={onClose}
714
+ onCenterNode={onCenterNode}
715
+ onTogglePin={onTogglePin}
716
+ pinned={pinned}
717
+ />
718
+ ) : (
719
+ <EdgeInspector edge={context.edge} />
720
+ )}
721
+ </aside>
722
+ );
723
+ };
724
+
725
+ LineageInspectorPanel.propTypes = {
726
+ context: PropTypes.shape({
727
+ edge: PropTypes.object,
728
+ node: PropTypes.object,
729
+ type: PropTypes.oneOf(["node", "edge"]).isRequired,
730
+ }),
731
+ createGraph: PropTypes.func,
732
+ excludeNode: PropTypes.func,
733
+ levels: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
734
+ onClose: PropTypes.func.isRequired,
735
+ onCenterNode: PropTypes.func,
736
+ onMinimize: PropTypes.func.isRequired,
737
+ onTogglePin: PropTypes.func,
738
+ pinned: PropTypes.bool,
739
+ showHeader: PropTypes.bool,
740
+ };
741
+
742
+ const mapStateToProps = ({ lineageQuery }) => ({
743
+ levels: _.prop("levels")(lineageQuery),
744
+ });
745
+
746
+ export default connect(mapStateToProps, { excludeNode, createGraph })(
747
+ LineageInspectorPanel,
748
+ );