datajunction-ui 0.0.1-rc.13 → 0.0.1-rc.15
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 +1 -1
- package/public/favicon.ico +0 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +1 -0
- package/src/app/components/djgraph/DJNode.jsx +5 -3
- package/src/app/components/djgraph/DJNodeDimensions.jsx +1 -2
- package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
- package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +6 -1
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +3 -0
- package/src/app/pages/NamespacePage/index.jsx +7 -1
- package/src/app/pages/NodePage/NodeColumnTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeGraphTab.jsx +92 -174
- package/src/app/pages/NodePage/NodeHistory.jsx +9 -1
- package/src/app/pages/NodePage/NodeInfoTab.jsx +1 -24
- package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -1
- package/src/app/pages/NodePage/index.jsx +20 -0
- package/src/app/pages/Root/index.tsx +7 -1
- package/src/app/pages/SQLBuilderPage/index.jsx +0 -1
- package/src/app/services/DJService.js +7 -0
- package/webpack.config.js +1 -0
package/package.json
CHANGED
package/public/favicon.ico
CHANGED
|
Binary file
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memo, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { Handle, Position } from 'reactflow';
|
|
3
|
-
import { DJNodeDimensions } from './DJNodeDimensions';
|
|
4
3
|
import Collapse from './Collapse';
|
|
5
4
|
|
|
6
5
|
function capitalize(string) {
|
|
@@ -43,6 +42,8 @@ export function DJNode({ id, data }) {
|
|
|
43
42
|
<>
|
|
44
43
|
<div
|
|
45
44
|
className={'dj-node__full node_type__' + data.type + highlightNodeClass}
|
|
45
|
+
key={data.name}
|
|
46
|
+
style={{ width: '450px' }}
|
|
46
47
|
>
|
|
47
48
|
<div style={handleWrapperStyle}>
|
|
48
49
|
<Handle
|
|
@@ -61,7 +62,8 @@ export function DJNode({ id, data }) {
|
|
|
61
62
|
</div>
|
|
62
63
|
</div>
|
|
63
64
|
<div className="dj-node__body">
|
|
64
|
-
<b>{capitalize(data.type)}</b
|
|
65
|
+
<b>{capitalize(data.type)}</b>
|
|
66
|
+
<br />{' '}
|
|
65
67
|
<a href={`/nodes/${data.name}`}>
|
|
66
68
|
{data.type === 'source' ? data.table : data.display_name}
|
|
67
69
|
</a>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
import DJClientContext from '../../providers/djclient';
|
|
3
|
-
import Collapse from './Collapse';
|
|
4
3
|
|
|
5
4
|
export function DJNodeDimensions(data) {
|
|
6
5
|
const [dimensions, setDimensions] = useState([]);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
addEdge,
|
|
4
|
+
MiniMap,
|
|
5
|
+
Controls,
|
|
6
|
+
Background,
|
|
7
|
+
useNodesState,
|
|
8
|
+
useEdgesState,
|
|
9
|
+
} from 'reactflow';
|
|
10
|
+
|
|
11
|
+
import '../../../styles/dag.css';
|
|
12
|
+
import 'reactflow/dist/style.css';
|
|
13
|
+
import DJNode from '../../components/djgraph/DJNode';
|
|
14
|
+
import dagre from 'dagre';
|
|
15
|
+
|
|
16
|
+
const getLayoutedElements = (
|
|
17
|
+
nodes,
|
|
18
|
+
edges,
|
|
19
|
+
direction = 'LR',
|
|
20
|
+
nodeWidth = 600,
|
|
21
|
+
) => {
|
|
22
|
+
const dagreGraph = new dagre.graphlib.Graph();
|
|
23
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
24
|
+
|
|
25
|
+
const isHorizontal = direction === 'TB';
|
|
26
|
+
dagreGraph.setGraph({
|
|
27
|
+
rankdir: direction,
|
|
28
|
+
nodesep: 40,
|
|
29
|
+
ranksep: 10,
|
|
30
|
+
ranker: 'longest-path',
|
|
31
|
+
});
|
|
32
|
+
const nodeHeightTracker = {};
|
|
33
|
+
|
|
34
|
+
nodes.forEach(node => {
|
|
35
|
+
nodeHeightTracker[node.id] =
|
|
36
|
+
Math.min(node.data.column_names.length, 10) * 40 + 250;
|
|
37
|
+
dagreGraph.setNode(node.id, {
|
|
38
|
+
width: nodeWidth,
|
|
39
|
+
height: nodeHeightTracker[node.id],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
edges.forEach(edge => {
|
|
44
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
dagre.layout(dagreGraph);
|
|
48
|
+
|
|
49
|
+
nodes.forEach(node => {
|
|
50
|
+
const nodeWithPosition = dagreGraph.node(node.id);
|
|
51
|
+
node.targetPosition = isHorizontal ? 'left' : 'top';
|
|
52
|
+
node.sourcePosition = isHorizontal ? 'right' : 'bottom';
|
|
53
|
+
node.position = {
|
|
54
|
+
x: nodeWithPosition.x - nodeWidth / 2,
|
|
55
|
+
y: nodeWithPosition.y - nodeHeightTracker[node.id] / 2,
|
|
56
|
+
};
|
|
57
|
+
node.width = nodeWidth;
|
|
58
|
+
node.height = nodeHeightTracker[node.id];
|
|
59
|
+
return node;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { nodes: nodes, edges: edges };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const LayoutFlow = (djNode, saveGraph) => {
|
|
66
|
+
const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []);
|
|
67
|
+
|
|
68
|
+
// These are used internally by ReactFlow (to update the nodes on the ReactFlow pane)
|
|
69
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
70
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
71
|
+
|
|
72
|
+
const minimapStyle = {
|
|
73
|
+
height: 100,
|
|
74
|
+
width: 150,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
saveGraph(getLayoutedElements, setNodes, setEdges).catch(console.error);
|
|
79
|
+
}, [djNode]);
|
|
80
|
+
|
|
81
|
+
const onConnect = useCallback(
|
|
82
|
+
params => setEdges(eds => addEdge(params, eds)),
|
|
83
|
+
[setEdges],
|
|
84
|
+
);
|
|
85
|
+
return (
|
|
86
|
+
<div style={{ height: '800px' }}>
|
|
87
|
+
<ReactFlow
|
|
88
|
+
nodes={nodes}
|
|
89
|
+
edges={edges}
|
|
90
|
+
nodeTypes={nodeTypes}
|
|
91
|
+
onNodesChange={onNodesChange}
|
|
92
|
+
onEdgesChange={onEdgesChange}
|
|
93
|
+
onConnect={onConnect}
|
|
94
|
+
snapToGrid={true}
|
|
95
|
+
fitView
|
|
96
|
+
>
|
|
97
|
+
<MiniMap style={minimapStyle} zoomable pannable />
|
|
98
|
+
<Controls />
|
|
99
|
+
<Background color="#aaa" gap={16} />
|
|
100
|
+
</ReactFlow>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
export default LayoutFlow;
|
|
@@ -4,6 +4,11 @@ exports[`<DJNode /> should render and match the snapshot 1`] = `
|
|
|
4
4
|
<React.Fragment>
|
|
5
5
|
<div
|
|
6
6
|
className="dj-node__full node_type__source"
|
|
7
|
+
style={
|
|
8
|
+
Object {
|
|
9
|
+
"width": "450px",
|
|
10
|
+
}
|
|
11
|
+
}
|
|
7
12
|
>
|
|
8
13
|
<div
|
|
9
14
|
style={
|
|
@@ -52,7 +57,7 @@ exports[`<DJNode /> should render and match the snapshot 1`] = `
|
|
|
52
57
|
<b>
|
|
53
58
|
Source
|
|
54
59
|
</b>
|
|
55
|
-
|
|
60
|
+
<br />
|
|
56
61
|
|
|
57
62
|
<a
|
|
58
63
|
href="/nodes/shared.dimensions.accounts"
|
|
@@ -75,7 +75,7 @@ export function NamespacePage() {
|
|
|
75
75
|
<tr>
|
|
76
76
|
<td>
|
|
77
77
|
<a href={'/nodes/' + node.name} className="link-table">
|
|
78
|
-
{node.
|
|
78
|
+
{node.name}
|
|
79
79
|
</a>
|
|
80
80
|
<span
|
|
81
81
|
className="rounded-pill badge bg-secondary-soft"
|
|
@@ -84,6 +84,11 @@ export function NamespacePage() {
|
|
|
84
84
|
{node.version}
|
|
85
85
|
</span>
|
|
86
86
|
</td>
|
|
87
|
+
<td>
|
|
88
|
+
<a href={'/nodes/' + node.name} className="link-table">
|
|
89
|
+
{node.display_name}
|
|
90
|
+
</a>
|
|
91
|
+
</td>
|
|
87
92
|
<td>
|
|
88
93
|
<span className={'node_type__' + node.type + ' badge node_type'}>
|
|
89
94
|
{node.type}
|
|
@@ -138,6 +143,7 @@ export function NamespacePage() {
|
|
|
138
143
|
<thead>
|
|
139
144
|
<tr>
|
|
140
145
|
<th>Name</th>
|
|
146
|
+
<th>Display Name</th>
|
|
141
147
|
<th>Type</th>
|
|
142
148
|
<th>Status</th>
|
|
143
149
|
<th>Mode</th>
|
|
@@ -1,195 +1,113 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import
|
|
3
|
-
addEdge,
|
|
4
|
-
MiniMap,
|
|
5
|
-
Controls,
|
|
6
|
-
Background,
|
|
7
|
-
useNodesState,
|
|
8
|
-
useEdgesState,
|
|
9
|
-
MarkerType,
|
|
10
|
-
} from 'reactflow';
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { MarkerType } from 'reactflow';
|
|
11
3
|
|
|
12
4
|
import '../../../styles/dag.css';
|
|
13
5
|
import 'reactflow/dist/style.css';
|
|
14
6
|
import DJNode from '../../components/djgraph/DJNode';
|
|
15
|
-
import dagre from 'dagre';
|
|
16
7
|
import DJClientContext from '../../providers/djclient';
|
|
8
|
+
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
17
9
|
|
|
18
10
|
const NodeLineage = djNode => {
|
|
19
11
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
20
|
-
const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []);
|
|
21
12
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
const createNode = node => {
|
|
14
|
+
const primary_key = node.columns
|
|
15
|
+
.filter(col =>
|
|
16
|
+
col.attributes.some(attr => attr.attribute_type.name === 'primary_key'),
|
|
17
|
+
)
|
|
18
|
+
.map(col => col.name);
|
|
19
|
+
const column_names = node.columns.map(col => {
|
|
20
|
+
return { name: col.name, type: col.type };
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
id: String(node.name),
|
|
24
|
+
type: 'DJNode',
|
|
25
|
+
data: {
|
|
26
|
+
label:
|
|
27
|
+
node.table !== null
|
|
28
|
+
? String(node.schema_ + '.' + node.table)
|
|
29
|
+
: 'default.' + node.name,
|
|
30
|
+
table: node.table,
|
|
31
|
+
name: String(node.name),
|
|
32
|
+
display_name: String(node.display_name),
|
|
33
|
+
type: node.type,
|
|
34
|
+
primary_key: primary_key,
|
|
35
|
+
column_names: column_names,
|
|
36
|
+
is_current: node.name === djNode.djNode.name,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
28
39
|
};
|
|
29
40
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
edges.forEach(edge => {
|
|
54
|
-
dagreGraph.setEdge(edge.source, edge.target);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
dagre.layout(dagreGraph);
|
|
58
|
-
|
|
59
|
-
nodes.forEach(node => {
|
|
60
|
-
const nodeWithPosition = dagreGraph.node(node.id);
|
|
61
|
-
node.targetPosition = isHorizontal ? 'left' : 'top';
|
|
62
|
-
node.sourcePosition = isHorizontal ? 'right' : 'bottom';
|
|
63
|
-
node.position = {
|
|
64
|
-
x: nodeWithPosition.x - nodeWidth / 2,
|
|
65
|
-
y: nodeWithPosition.y - nodeHeightTracker[node.id] / 2,
|
|
41
|
+
const dimensionEdges = node => {
|
|
42
|
+
return node.columns
|
|
43
|
+
.filter(col => col.dimension)
|
|
44
|
+
.map(col => {
|
|
45
|
+
return {
|
|
46
|
+
id: col.dimension.name + '->' + node.name + '.' + col.name,
|
|
47
|
+
source: col.dimension.name,
|
|
48
|
+
sourceHandle: col.dimension.name,
|
|
49
|
+
target: node.name,
|
|
50
|
+
targetHandle: node.name + '.' + col.name,
|
|
51
|
+
draggable: true,
|
|
52
|
+
markerStart: {
|
|
53
|
+
type: MarkerType.Arrow,
|
|
54
|
+
width: 20,
|
|
55
|
+
height: 20,
|
|
56
|
+
color: '#b0b9c2',
|
|
57
|
+
},
|
|
58
|
+
style: {
|
|
59
|
+
strokeWidth: 3,
|
|
60
|
+
stroke: '#b0b9c2',
|
|
61
|
+
},
|
|
66
62
|
};
|
|
67
|
-
node.width = nodeWidth;
|
|
68
|
-
node.height = nodeHeightTracker[node.id];
|
|
69
|
-
return node;
|
|
70
63
|
});
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const dagFetch = async () => {
|
|
75
|
-
let related_nodes = await djClient.node_dag(djNode.djNode.name);
|
|
76
|
-
// djNode.djNode.is_current = true;
|
|
77
|
-
var djNodes = [djNode.djNode];
|
|
78
|
-
for (const iterable of [related_nodes]) {
|
|
79
|
-
for (const item of iterable) {
|
|
80
|
-
if (item.type !== 'cube') {
|
|
81
|
-
djNodes.push(item);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
let edges = [];
|
|
86
|
-
djNodes.forEach(obj => {
|
|
87
|
-
obj.parents.forEach(parent => {
|
|
88
|
-
if (parent.name) {
|
|
89
|
-
edges.push({
|
|
90
|
-
id: obj.name + '-' + parent.name,
|
|
91
|
-
source: parent.name,
|
|
92
|
-
sourceHandle: parent.name,
|
|
93
|
-
target: obj.name,
|
|
94
|
-
targetHandle: obj.name,
|
|
95
|
-
animated: true,
|
|
96
|
-
markerEnd: {
|
|
97
|
-
type: MarkerType.Arrow,
|
|
98
|
-
},
|
|
99
|
-
style: {
|
|
100
|
-
strokeWidth: 3,
|
|
101
|
-
stroke: '#b0b9c2',
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
});
|
|
64
|
+
};
|
|
106
65
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
source: col.dimension.name,
|
|
112
|
-
sourceHandle: col.dimension.name,
|
|
113
|
-
target: obj.name,
|
|
114
|
-
targetHandle: obj.name + '.' + col.name,
|
|
115
|
-
draggable: true,
|
|
116
|
-
markerStart: {
|
|
117
|
-
type: MarkerType.Arrow,
|
|
118
|
-
width: 20,
|
|
119
|
-
height: 20,
|
|
120
|
-
color: '#b0b9c2',
|
|
121
|
-
},
|
|
122
|
-
style: {
|
|
123
|
-
strokeWidth: 3,
|
|
124
|
-
stroke: '#b0b9c2',
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
edges.push(edge);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
const nodes = djNodes.map(node => {
|
|
132
|
-
const primary_key = node.columns
|
|
133
|
-
.filter(col =>
|
|
134
|
-
col.attributes.some(
|
|
135
|
-
attr => attr.attribute_type.name === 'primary_key',
|
|
136
|
-
),
|
|
137
|
-
)
|
|
138
|
-
.map(col => col.name);
|
|
139
|
-
const column_names = node.columns.map(col => {
|
|
140
|
-
return { name: col.name, type: col.type };
|
|
141
|
-
});
|
|
66
|
+
const parentEdges = node => {
|
|
67
|
+
return node.parents
|
|
68
|
+
.filter(parent => parent.name)
|
|
69
|
+
.map(parent => {
|
|
142
70
|
return {
|
|
143
|
-
id:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
column_names: column_names,
|
|
156
|
-
is_current: node.name === djNode.djNode.name,
|
|
71
|
+
id: node.name + '-' + parent.name,
|
|
72
|
+
source: parent.name,
|
|
73
|
+
sourceHandle: parent.name,
|
|
74
|
+
target: node.name,
|
|
75
|
+
targetHandle: node.name,
|
|
76
|
+
animated: true,
|
|
77
|
+
markerEnd: {
|
|
78
|
+
type: MarkerType.Arrow,
|
|
79
|
+
},
|
|
80
|
+
style: {
|
|
81
|
+
strokeWidth: 3,
|
|
82
|
+
stroke: '#b0b9c2',
|
|
157
83
|
},
|
|
158
84
|
};
|
|
159
85
|
});
|
|
160
|
-
|
|
161
|
-
setEdges(edges);
|
|
162
|
-
|
|
163
|
-
// use dagre to determine the position of the parents (the DJ nodes)
|
|
164
|
-
// the positions of the columns are relative to each DJ node
|
|
165
|
-
setElementsLayout(nodes, edges);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
dagFetch();
|
|
169
|
-
}, [dagreGraph, djClient, djNode.djNode, setEdges, setNodes]);
|
|
170
|
-
|
|
171
|
-
const onConnect = useCallback(
|
|
172
|
-
params => setEdges(eds => addEdge(params, eds)),
|
|
173
|
-
[setEdges],
|
|
174
|
-
);
|
|
86
|
+
};
|
|
175
87
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
88
|
+
const dagFetch = async (getLayoutedElements, setNodes, setEdges) => {
|
|
89
|
+
let related_nodes = await djClient.node_dag(djNode.djNode.name);
|
|
90
|
+
var djNodes = [djNode.djNode];
|
|
91
|
+
for (const iterable of [related_nodes]) {
|
|
92
|
+
for (const item of iterable) {
|
|
93
|
+
if (item.type !== 'cube') {
|
|
94
|
+
djNodes.push(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let edges = [];
|
|
99
|
+
djNodes.forEach(node => {
|
|
100
|
+
edges = edges.concat(parentEdges(node));
|
|
101
|
+
edges = edges.concat(dimensionEdges(node));
|
|
102
|
+
});
|
|
103
|
+
const nodes = djNodes.map(node => createNode(node));
|
|
104
|
+
|
|
105
|
+
// use dagre to determine the position of the parents (the DJ nodes)
|
|
106
|
+
// the positions of the columns are relative to each DJ node
|
|
107
|
+
getLayoutedElements(nodes, edges);
|
|
108
|
+
setNodes(nodes);
|
|
109
|
+
setEdges(edges);
|
|
110
|
+
};
|
|
111
|
+
return LayoutFlow(djNode, dagFetch);
|
|
194
112
|
};
|
|
195
113
|
export default NodeLineage;
|
|
@@ -111,7 +111,13 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
111
111
|
</div>
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
|
-
return
|
|
114
|
+
return (
|
|
115
|
+
<div>
|
|
116
|
+
{JSON.stringify(event.details) === '{}'
|
|
117
|
+
? ''
|
|
118
|
+
: JSON.stringify(event.details)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
115
121
|
};
|
|
116
122
|
|
|
117
123
|
const tableData = history => {
|
|
@@ -125,6 +131,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
125
131
|
</span>
|
|
126
132
|
</td>
|
|
127
133
|
<td>{event.entity_type}</td>
|
|
134
|
+
<td>{event.entity_name}</td>
|
|
128
135
|
<td>{event.user ? event.user : 'unknown'}</td>
|
|
129
136
|
<td>{event.created_at}</td>
|
|
130
137
|
<td>{eventData(event)}</td>
|
|
@@ -161,6 +168,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
161
168
|
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
162
169
|
<th className="text-start">Activity</th>
|
|
163
170
|
<th>Type</th>
|
|
171
|
+
<th>Name</th>
|
|
164
172
|
<th>User</th>
|
|
165
173
|
<th>Timestamp</th>
|
|
166
174
|
<th>Details</th>
|
|
@@ -11,21 +11,8 @@ SyntaxHighlighter.registerLanguage('sql', sql);
|
|
|
11
11
|
foundation.hljs['padding'] = '2rem';
|
|
12
12
|
|
|
13
13
|
export default function NodeInfoTab({ node }) {
|
|
14
|
-
const [compiledSQL, setCompiledSQL] = useState('');
|
|
15
14
|
const [checked, setChecked] = useState(false);
|
|
16
15
|
const nodeTags = node?.tags.map(tag => <div>{tag}</div>);
|
|
17
|
-
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
const fetchData = async () => {
|
|
20
|
-
const data = await djClient.compiledSql(node.name);
|
|
21
|
-
if (data.sql) {
|
|
22
|
-
setCompiledSQL(data.sql);
|
|
23
|
-
} else {
|
|
24
|
-
setCompiledSQL('/* Ran into an issue while generating compiled SQL */');
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
fetchData().catch(console.error);
|
|
28
|
-
}, [node, djClient]);
|
|
29
16
|
function toggle(value) {
|
|
30
17
|
return !value;
|
|
31
18
|
}
|
|
@@ -38,18 +25,8 @@ export default function NodeInfoTab({ node }) {
|
|
|
38
25
|
}}
|
|
39
26
|
>
|
|
40
27
|
<h6 className="mb-0 w-100">Query</h6>
|
|
41
|
-
{['metric', 'dimension', 'transform'].indexOf(node?.type) > -1 ? (
|
|
42
|
-
<ToggleSwitch
|
|
43
|
-
id="toggleSwitch"
|
|
44
|
-
checked={checked}
|
|
45
|
-
onChange={() => setChecked(toggle)}
|
|
46
|
-
toggleName="Show Compiled SQL"
|
|
47
|
-
/>
|
|
48
|
-
) : (
|
|
49
|
-
<></>
|
|
50
|
-
)}
|
|
51
28
|
<SyntaxHighlighter language="sql" style={foundation}>
|
|
52
|
-
{
|
|
29
|
+
{node?.query}
|
|
53
30
|
</SyntaxHighlighter>
|
|
54
31
|
</div>
|
|
55
32
|
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { MarkerType } from 'reactflow';
|
|
3
|
+
|
|
4
|
+
import '../../../styles/dag.css';
|
|
5
|
+
import 'reactflow/dist/style.css';
|
|
6
|
+
import DJClientContext from '../../providers/djclient';
|
|
7
|
+
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
8
|
+
|
|
9
|
+
const createDJNode = node => {
|
|
10
|
+
return {
|
|
11
|
+
id: node.node_name,
|
|
12
|
+
type: 'DJNode',
|
|
13
|
+
data: {
|
|
14
|
+
label: node.node_name,
|
|
15
|
+
name: node.node_name,
|
|
16
|
+
type: node.node_type,
|
|
17
|
+
table: node.node_type === 'source' ? node.node_name : '',
|
|
18
|
+
display_name:
|
|
19
|
+
node.node_type === 'source' ? node.node_name : node.display_name,
|
|
20
|
+
column_names: [{ name: node.column_name, type: '' }],
|
|
21
|
+
primary_key: [],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const NodeColumnLineage = djNode => {
|
|
27
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
28
|
+
const dagFetch = async (getLayoutedElements, setNodes, setEdges) => {
|
|
29
|
+
let relatedNodes = await djClient.node_lineage(djNode.djNode.name);
|
|
30
|
+
let nodesMapping = {};
|
|
31
|
+
let edgesMapping = {};
|
|
32
|
+
let processing = relatedNodes;
|
|
33
|
+
while (processing.length > 0) {
|
|
34
|
+
let current = processing.pop();
|
|
35
|
+
let node = createDJNode(current);
|
|
36
|
+
if (node.id in nodesMapping) {
|
|
37
|
+
nodesMapping[node.id].data.column_names = Array.from(
|
|
38
|
+
new Set([
|
|
39
|
+
...nodesMapping[node.id].data.column_names.map(x => x.name),
|
|
40
|
+
...node.data.column_names.map(x => x.name),
|
|
41
|
+
]),
|
|
42
|
+
).map(x => {
|
|
43
|
+
return { name: x, type: '' };
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
nodesMapping[node.id] = node;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
current.lineage.forEach(lineageColumn => {
|
|
50
|
+
const sourceHandle =
|
|
51
|
+
lineageColumn.node_name + '.' + lineageColumn.column_name;
|
|
52
|
+
const targetHandle = current.node_name + '.' + current.column_name;
|
|
53
|
+
edgesMapping[sourceHandle + '->' + targetHandle] = {
|
|
54
|
+
id: sourceHandle + '->' + targetHandle,
|
|
55
|
+
source: lineageColumn.node_name,
|
|
56
|
+
sourceHandle: sourceHandle,
|
|
57
|
+
target: current.node_name,
|
|
58
|
+
targetHandle: targetHandle,
|
|
59
|
+
animated: true,
|
|
60
|
+
markerEnd: {
|
|
61
|
+
type: MarkerType.Arrow,
|
|
62
|
+
},
|
|
63
|
+
style: {
|
|
64
|
+
strokeWidth: 3,
|
|
65
|
+
stroke: '#b0b9c2',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
processing.push(lineageColumn);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// use dagre to determine the position of the parents (the DJ nodes)
|
|
73
|
+
// the positions of the columns are relative to each DJ node
|
|
74
|
+
const elements = getLayoutedElements(
|
|
75
|
+
Object.keys(nodesMapping).map(key => nodesMapping[key]),
|
|
76
|
+
Object.keys(edgesMapping).map(key => edgesMapping[key]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
setNodes(elements.nodes);
|
|
80
|
+
setEdges(elements.edges);
|
|
81
|
+
};
|
|
82
|
+
return LayoutFlow(djNode, dagFetch);
|
|
83
|
+
};
|
|
84
|
+
export default NodeColumnLineage;
|
|
@@ -3,7 +3,6 @@ import Select from 'react-select';
|
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
5
5
|
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
6
|
-
import { format } from 'sql-formatter';
|
|
7
6
|
|
|
8
7
|
const NodeSQLTab = djNode => {
|
|
9
8
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -12,6 +12,7 @@ import NodeSQLTab from './NodeSQLTab';
|
|
|
12
12
|
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
13
13
|
import ClientCodePopover from './ClientCodePopover';
|
|
14
14
|
import NodesWithDimension from './NodesWithDimension';
|
|
15
|
+
import NodeColumnLineage from './NodeLineageTab';
|
|
15
16
|
|
|
16
17
|
export function NodePage() {
|
|
17
18
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -95,6 +96,11 @@ export function NodePage() {
|
|
|
95
96
|
name: 'Linked Nodes',
|
|
96
97
|
display: node?.type === 'dimension',
|
|
97
98
|
},
|
|
99
|
+
{
|
|
100
|
+
id: 7,
|
|
101
|
+
name: 'Lineage',
|
|
102
|
+
display: node?.type === 'metric',
|
|
103
|
+
},
|
|
98
104
|
];
|
|
99
105
|
};
|
|
100
106
|
|
|
@@ -124,6 +130,9 @@ export function NodePage() {
|
|
|
124
130
|
case 6:
|
|
125
131
|
tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
|
|
126
132
|
break;
|
|
133
|
+
case 7:
|
|
134
|
+
tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
|
|
135
|
+
break;
|
|
127
136
|
default:
|
|
128
137
|
tabToDisplay = <NodeInfoTab node={node} />;
|
|
129
138
|
}
|
|
@@ -146,6 +155,17 @@ export function NodePage() {
|
|
|
146
155
|
</span>
|
|
147
156
|
</h3>
|
|
148
157
|
<ClientCodePopover code={node?.createNodeClientCode} />
|
|
158
|
+
<div>
|
|
159
|
+
<a href={'/nodes/' + node?.name} className="link-table">
|
|
160
|
+
{node?.name}
|
|
161
|
+
</a>
|
|
162
|
+
<span
|
|
163
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
164
|
+
style={{ marginLeft: '0.5rem' }}
|
|
165
|
+
>
|
|
166
|
+
{node?.version}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
149
169
|
<div className="align-items-center row">
|
|
150
170
|
{tabsList(node).map(buildTabs)}
|
|
151
171
|
</div>
|
|
@@ -34,7 +34,13 @@ export function Root() {
|
|
|
34
34
|
</span>
|
|
35
35
|
<span className="menu-link">
|
|
36
36
|
<span className="menu-title">
|
|
37
|
-
<a
|
|
37
|
+
<a
|
|
38
|
+
href="https://www.datajunction.io"
|
|
39
|
+
target="_blank"
|
|
40
|
+
rel="noreferrer"
|
|
41
|
+
>
|
|
42
|
+
Docs
|
|
43
|
+
</a>
|
|
38
44
|
</span>
|
|
39
45
|
</span>
|
|
40
46
|
</div>
|
|
@@ -4,7 +4,6 @@ import { DataJunctionAPI } from '../../services/DJService';
|
|
|
4
4
|
import DJClientContext from '../../providers/djclient';
|
|
5
5
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
6
6
|
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
7
|
-
import { format } from 'sql-formatter';
|
|
8
7
|
import Select from 'react-select';
|
|
9
8
|
import QueryInfo from '../../components/QueryInfo';
|
|
10
9
|
|
|
@@ -36,6 +36,13 @@ export const DataJunctionAPI = {
|
|
|
36
36
|
return data;
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
+
node_lineage: async function (name) {
|
|
40
|
+
const data = await (
|
|
41
|
+
await fetch(DJ_URL + '/nodes/' + name + '/lineage/')
|
|
42
|
+
).json();
|
|
43
|
+
return data;
|
|
44
|
+
},
|
|
45
|
+
|
|
39
46
|
metric: async function (name) {
|
|
40
47
|
const data = await (await fetch(DJ_URL + '/metrics/' + name + '/')).json();
|
|
41
48
|
return data;
|
package/webpack.config.js
CHANGED
|
@@ -99,6 +99,7 @@ module.exports = {
|
|
|
99
99
|
plugins: [
|
|
100
100
|
new HtmlWebpackPlugin({
|
|
101
101
|
template: path.resolve(__dirname, 'public', 'index.html'),
|
|
102
|
+
favicon: path.resolve(__dirname, 'public', 'favicon.ico'),
|
|
102
103
|
}),
|
|
103
104
|
new webpack.DefinePlugin({
|
|
104
105
|
'process.env': JSON.stringify(process.env),
|