datajunction-ui 0.0.1-rc.12 → 0.0.1-rc.14
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/.env +1 -0
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +3 -0
- package/src/app/components/QueryInfo.jsx +2 -34
- 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 +8 -2
- 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 +110 -1
- package/src/app/pages/NodePage/NodeInfoTab.jsx +2 -19
- package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +1 -9
- package/src/app/pages/NodePage/NodesWithDimension.jsx +40 -0
- package/src/app/pages/NodePage/index.jsx +71 -30
- package/src/app/pages/Root/index.tsx +7 -1
- package/src/app/pages/SQLBuilderPage/index.jsx +64 -55
- package/src/app/services/DJService.js +36 -13
- package/src/styles/index.css +16 -8
- package/webpack.config.js +1 -0
|
@@ -13,6 +13,113 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
13
13
|
};
|
|
14
14
|
fetchData().catch(console.error);
|
|
15
15
|
}, [djClient, node]);
|
|
16
|
+
|
|
17
|
+
const eventData = event => {
|
|
18
|
+
console.log('event', event);
|
|
19
|
+
if (
|
|
20
|
+
event.activity_type === 'set_attribute' &&
|
|
21
|
+
event.entity_type === 'column_attribute'
|
|
22
|
+
) {
|
|
23
|
+
return event.details.attributes
|
|
24
|
+
.map(attr => (
|
|
25
|
+
<div>
|
|
26
|
+
Set{' '}
|
|
27
|
+
<span className={`badge partition_value`}>{attr.column_name}</span>{' '}
|
|
28
|
+
as{' '}
|
|
29
|
+
<span className={`badge partition_value_highlight`}>
|
|
30
|
+
{attr.attribute_type_name}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
))
|
|
34
|
+
.reduce((prev, curr) => [prev, <br />, curr]);
|
|
35
|
+
}
|
|
36
|
+
if (event.activity_type === 'create' && event.entity_type === 'link') {
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
Linked{' '}
|
|
40
|
+
<span className={`badge partition_value`}>
|
|
41
|
+
{event.details.column}
|
|
42
|
+
</span>{' '}
|
|
43
|
+
to
|
|
44
|
+
<span className={`badge partition_value_highlight`}>
|
|
45
|
+
{event.details.dimension}
|
|
46
|
+
</span>{' '}
|
|
47
|
+
via
|
|
48
|
+
<span className={`badge partition_value`}>
|
|
49
|
+
{event.details.dimension_column}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
event.activity_type === 'create' &&
|
|
56
|
+
event.entity_type === 'materialization'
|
|
57
|
+
) {
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
Initialized materialization{' '}
|
|
61
|
+
<span className={`badge partition_value`}>
|
|
62
|
+
{event.details.materialization}
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
event.activity_type === 'create' &&
|
|
69
|
+
event.entity_type === 'availability'
|
|
70
|
+
) {
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
Materialized at{' '}
|
|
74
|
+
<span className={`badge partition_value_highlight`}>
|
|
75
|
+
{event.post.catalog}.{event.post.schema_}.{event.post.table}
|
|
76
|
+
</span>
|
|
77
|
+
from{' '}
|
|
78
|
+
<span className={`badge partition_value`}>
|
|
79
|
+
{event.post.min_temporal_partition}
|
|
80
|
+
</span>{' '}
|
|
81
|
+
to
|
|
82
|
+
<span className={`badge partition_value`}>
|
|
83
|
+
{event.post.max_temporal_partition}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
event.activity_type === 'status_change' &&
|
|
90
|
+
event.entity_type === 'node'
|
|
91
|
+
) {
|
|
92
|
+
const expr = (
|
|
93
|
+
<div>
|
|
94
|
+
Caused by a change in upstream{' '}
|
|
95
|
+
<a href={`/nodes/${event.details['upstream_node']}`}>
|
|
96
|
+
{event.details['upstream_node']}
|
|
97
|
+
</a>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
Status changed from{' '}
|
|
103
|
+
<span className={`status__${event.pre['status']}`}>
|
|
104
|
+
{event.pre['status']}
|
|
105
|
+
</span>{' '}
|
|
106
|
+
to{' '}
|
|
107
|
+
<span className={`status__${event.post['status']}`}>
|
|
108
|
+
{event.post['status']}
|
|
109
|
+
</span>{' '}
|
|
110
|
+
{event.details['upstream_node'] !== undefined ? expr : ''}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
<div>
|
|
116
|
+
{JSON.stringify(event.details) === '{}'
|
|
117
|
+
? ''
|
|
118
|
+
: JSON.stringify(event.details)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
16
123
|
const tableData = history => {
|
|
17
124
|
return history.map(event => (
|
|
18
125
|
<tr>
|
|
@@ -27,6 +134,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
27
134
|
<td>{event.entity_name}</td>
|
|
28
135
|
<td>{event.user ? event.user : 'unknown'}</td>
|
|
29
136
|
<td>{event.created_at}</td>
|
|
137
|
+
<td>{eventData(event)}</td>
|
|
30
138
|
</tr>
|
|
31
139
|
));
|
|
32
140
|
};
|
|
@@ -45,7 +153,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
45
153
|
));
|
|
46
154
|
};
|
|
47
155
|
return (
|
|
48
|
-
<div className="table-
|
|
156
|
+
<div className="table-vertical">
|
|
49
157
|
<table className="card-inner-table table">
|
|
50
158
|
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
51
159
|
<th className="text-start">Version</th>
|
|
@@ -63,6 +171,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
63
171
|
<th>Name</th>
|
|
64
172
|
<th>User</th>
|
|
65
173
|
<th>Timestamp</th>
|
|
174
|
+
<th>Details</th>
|
|
66
175
|
</thead>
|
|
67
176
|
{tableData(history)}
|
|
68
177
|
</table>
|
|
@@ -2,7 +2,6 @@ import { useState, useContext, useEffect } from 'react';
|
|
|
2
2
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
3
3
|
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
4
4
|
import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql';
|
|
5
|
-
import { format } from 'sql-formatter';
|
|
6
5
|
import NodeStatus from './NodeStatus';
|
|
7
6
|
import ListGroupItem from '../../components/ListGroupItem';
|
|
8
7
|
import ToggleSwitch from '../../components/ToggleSwitch';
|
|
@@ -18,7 +17,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
18
17
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
19
18
|
useEffect(() => {
|
|
20
19
|
const fetchData = async () => {
|
|
21
|
-
const data =
|
|
20
|
+
const data = djClient.compiledSql(node.name);
|
|
22
21
|
if (data.sql) {
|
|
23
22
|
setCompiledSQL(data.sql);
|
|
24
23
|
} else {
|
|
@@ -50,23 +49,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
50
49
|
<></>
|
|
51
50
|
)}
|
|
52
51
|
<SyntaxHighlighter language="sql" style={foundation}>
|
|
53
|
-
{checked
|
|
54
|
-
? format(compiledSQL, {
|
|
55
|
-
language: 'spark',
|
|
56
|
-
tabWidth: 2,
|
|
57
|
-
keywordCase: 'upper',
|
|
58
|
-
denseOperators: true,
|
|
59
|
-
logicalOperatorNewline: 'before',
|
|
60
|
-
expressionWidth: 10,
|
|
61
|
-
})
|
|
62
|
-
: format(node?.query, {
|
|
63
|
-
language: 'spark',
|
|
64
|
-
tabWidth: 2,
|
|
65
|
-
keywordCase: 'upper',
|
|
66
|
-
denseOperators: true,
|
|
67
|
-
logicalOperatorNewline: 'before',
|
|
68
|
-
expressionWidth: 10,
|
|
69
|
-
})}
|
|
52
|
+
{checked ? compiledSQL : node?.query}
|
|
70
53
|
</SyntaxHighlighter>
|
|
71
54
|
</div>
|
|
72
55
|
</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;
|
|
@@ -82,14 +81,7 @@ const NodeSQLTab = djNode => {
|
|
|
82
81
|
>
|
|
83
82
|
<h6 className="mb-0 w-100">Query</h6>
|
|
84
83
|
<SyntaxHighlighter language="sql" style={foundation}>
|
|
85
|
-
{
|
|
86
|
-
language: 'spark',
|
|
87
|
-
tabWidth: 2,
|
|
88
|
-
keywordCase: 'upper',
|
|
89
|
-
denseOperators: true,
|
|
90
|
-
logicalOperatorNewline: 'before',
|
|
91
|
-
expressionWidth: 10,
|
|
92
|
-
})}
|
|
84
|
+
{query}
|
|
93
85
|
</SyntaxHighlighter>
|
|
94
86
|
</div>
|
|
95
87
|
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
export default function NodesWithDimension({ node, djClient }) {
|
|
5
|
+
const [availableNodes, setAvailableNodes] = useState([]);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const fetchData = async () => {
|
|
9
|
+
const data = await djClient.nodesWithDimension(node.name);
|
|
10
|
+
setAvailableNodes(data);
|
|
11
|
+
};
|
|
12
|
+
fetchData().catch(console.error);
|
|
13
|
+
}, [djClient, node]);
|
|
14
|
+
return (
|
|
15
|
+
<div className="table-responsive">
|
|
16
|
+
<table className="card-inner-table table">
|
|
17
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
18
|
+
<th className="text-start">Name</th>
|
|
19
|
+
<th>Type</th>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
{availableNodes.map(node => (
|
|
23
|
+
<tr>
|
|
24
|
+
<td>
|
|
25
|
+
<a href={`/nodes/${node.name}`}>{node.display_name}</a>
|
|
26
|
+
</td>
|
|
27
|
+
<td>
|
|
28
|
+
<span
|
|
29
|
+
className={'node_type__' + node.type + ' badge node_type'}
|
|
30
|
+
>
|
|
31
|
+
{node.type}
|
|
32
|
+
</span>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
))}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -11,6 +11,8 @@ import DJClientContext from '../../providers/djclient';
|
|
|
11
11
|
import NodeSQLTab from './NodeSQLTab';
|
|
12
12
|
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
13
13
|
import ClientCodePopover from './ClientCodePopover';
|
|
14
|
+
import NodesWithDimension from './NodesWithDimension';
|
|
15
|
+
import NodeColumnLineage from './NodeLineageTab';
|
|
14
16
|
|
|
15
17
|
export function NodePage() {
|
|
16
18
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -25,7 +27,7 @@ export function NodePage() {
|
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
const buildTabs = tab => {
|
|
28
|
-
return (
|
|
30
|
+
return tab.display ? (
|
|
29
31
|
<Tab
|
|
30
32
|
key={tab.id}
|
|
31
33
|
id={tab.id}
|
|
@@ -33,7 +35,7 @@ export function NodePage() {
|
|
|
33
35
|
onClick={onClickTab(tab.id)}
|
|
34
36
|
selectedTab={state.selectedTab}
|
|
35
37
|
/>
|
|
36
|
-
);
|
|
38
|
+
) : null;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const { name } = useParams();
|
|
@@ -57,32 +59,51 @@ export function NodePage() {
|
|
|
57
59
|
fetchData().catch(console.error);
|
|
58
60
|
}, [djClient, name]);
|
|
59
61
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
62
|
+
const tabsList = node => {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: 0,
|
|
66
|
+
name: 'Info',
|
|
67
|
+
display: true,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 1,
|
|
71
|
+
name: 'Columns',
|
|
72
|
+
display: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 2,
|
|
76
|
+
name: 'Graph',
|
|
77
|
+
display: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 3,
|
|
81
|
+
name: 'History',
|
|
82
|
+
display: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 4,
|
|
86
|
+
name: 'SQL',
|
|
87
|
+
display: node?.type !== 'dimension' && node?.type !== 'source',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 5,
|
|
91
|
+
name: 'Materializations',
|
|
92
|
+
display: node?.type !== 'source',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 6,
|
|
96
|
+
name: 'Linked Nodes',
|
|
97
|
+
display: node?.type === 'dimension',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 7,
|
|
101
|
+
name: 'Lineage',
|
|
102
|
+
display: node?.type === 'metric',
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
};
|
|
106
|
+
|
|
86
107
|
//
|
|
87
108
|
//
|
|
88
109
|
let tabToDisplay = null;
|
|
@@ -106,6 +127,12 @@ export function NodePage() {
|
|
|
106
127
|
case 5:
|
|
107
128
|
tabToDisplay = <NodeMaterializationTab node={node} djClient={djClient} />;
|
|
108
129
|
break;
|
|
130
|
+
case 6:
|
|
131
|
+
tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
|
|
132
|
+
break;
|
|
133
|
+
case 7:
|
|
134
|
+
tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
|
|
135
|
+
break;
|
|
109
136
|
default:
|
|
110
137
|
tabToDisplay = <NodeInfoTab node={node} />;
|
|
111
138
|
}
|
|
@@ -121,12 +148,26 @@ export function NodePage() {
|
|
|
121
148
|
style={{ display: 'inline-block' }}
|
|
122
149
|
>
|
|
123
150
|
<span className="card-label fw-bold text-gray-800">
|
|
124
|
-
{node?.display_name}
|
|
151
|
+
{node?.display_name}{' '}
|
|
152
|
+
<span className={'node_type__' + node?.type + ' badge node_type'}>
|
|
153
|
+
{node?.type}
|
|
154
|
+
</span>
|
|
125
155
|
</span>
|
|
126
156
|
</h3>
|
|
127
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>
|
|
128
169
|
<div className="align-items-center row">
|
|
129
|
-
{
|
|
170
|
+
{tabsList(node).map(buildTabs)}
|
|
130
171
|
</div>
|
|
131
172
|
{tabToDisplay}
|
|
132
173
|
</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>
|