datajunction-ui 0.0.1-rc.10 → 0.0.1-rc.11
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 +4 -1
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +18 -16
- package/src/app/components/NamespaceHeader.jsx +4 -13
- package/src/app/components/QueryInfo.jsx +109 -0
- package/src/app/components/ToggleSwitch.jsx +17 -0
- package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
- package/src/app/components/djgraph/Collapse.jsx +46 -0
- package/src/app/components/djgraph/DJNode.jsx +56 -80
- package/src/app/components/djgraph/DJNodeColumns.jsx +68 -0
- package/src/app/components/djgraph/DJNodeDimensions.jsx +69 -0
- package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +82 -43
- package/src/app/icons/CollapsedIcon.jsx +15 -0
- package/src/app/icons/ExpandedIcon.jsx +15 -0
- package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
- package/src/app/icons/InvalidIcon.jsx +14 -0
- package/src/app/icons/PythonIcon.jsx +52 -0
- package/src/app/icons/TableIcon.jsx +14 -0
- package/src/app/icons/ValidIcon.jsx +14 -0
- package/src/app/index.tsx +3 -2
- package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +24 -5
- package/src/app/pages/NamespacePage/index.jsx +78 -10
- package/src/app/pages/NodePage/ClientCodePopover.jsx +30 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +36 -20
- package/src/app/pages/NodePage/NodeGraphTab.jsx +45 -17
- package/src/app/pages/NodePage/NodeHistory.jsx +71 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +132 -49
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +151 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +100 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
- package/src/app/pages/NodePage/index.jsx +43 -8
- package/src/app/pages/Root/index.tsx +5 -0
- package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +344 -0
- package/src/app/services/DJService.js +125 -1
- package/src/styles/dag.css +111 -5
- package/src/styles/index.css +338 -22
- package/webpack.config.js +4 -5
- package/.babelrc +0 -4
- package/.env.local +0 -4
- package/.env.production +0 -1
- package/.vscode/extensions.json +0 -7
- package/.vscode/launch.json +0 -15
- package/.vscode/settings.json +0 -25
- package/Dockerfile +0 -7
- package/src/app/components/DashboardItem.jsx +0 -29
- package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
- package/src/app/pages/ListNamespacesPage/index.jsx +0 -60
|
@@ -36,13 +36,18 @@ const NodeLineage = djNode => {
|
|
|
36
36
|
edges,
|
|
37
37
|
direction = 'LR',
|
|
38
38
|
nodeWidth = 800,
|
|
39
|
-
nodeHeight = 150,
|
|
40
39
|
) => {
|
|
41
40
|
const isHorizontal = direction === 'TB';
|
|
42
41
|
dagreGraph.setGraph({ rankdir: direction });
|
|
42
|
+
const nodeHeightTracker = {};
|
|
43
43
|
|
|
44
44
|
nodes.forEach(node => {
|
|
45
|
-
|
|
45
|
+
nodeHeightTracker[node.id] =
|
|
46
|
+
Math.min(node.data.column_names.length, 10) * 37 + 250;
|
|
47
|
+
dagreGraph.setNode(node.id, {
|
|
48
|
+
width: nodeWidth,
|
|
49
|
+
height: nodeHeightTracker[node.id],
|
|
50
|
+
});
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
edges.forEach(edge => {
|
|
@@ -57,21 +62,24 @@ const NodeLineage = djNode => {
|
|
|
57
62
|
node.sourcePosition = isHorizontal ? 'right' : 'bottom';
|
|
58
63
|
node.position = {
|
|
59
64
|
x: nodeWithPosition.x - nodeWidth / 2,
|
|
60
|
-
y: nodeWithPosition.y -
|
|
65
|
+
y: nodeWithPosition.y - nodeHeightTracker[node.id] / 2,
|
|
61
66
|
};
|
|
67
|
+
node.width = nodeWidth;
|
|
68
|
+
node.height = nodeHeightTracker[node.id];
|
|
62
69
|
return node;
|
|
63
70
|
});
|
|
64
|
-
|
|
65
71
|
return { nodes, edges };
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
const dagFetch = async () => {
|
|
69
|
-
let
|
|
70
|
-
|
|
75
|
+
let related_nodes = await djClient.node_dag(djNode.djNode.name);
|
|
76
|
+
// djNode.djNode.is_current = true;
|
|
71
77
|
var djNodes = [djNode.djNode];
|
|
72
|
-
for (const iterable of [
|
|
78
|
+
for (const iterable of [related_nodes]) {
|
|
73
79
|
for (const item of iterable) {
|
|
74
|
-
|
|
80
|
+
if (item.type !== 'cube') {
|
|
81
|
+
djNodes.push(item);
|
|
82
|
+
}
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
let edges = [];
|
|
@@ -80,24 +88,43 @@ const NodeLineage = djNode => {
|
|
|
80
88
|
if (parent.name) {
|
|
81
89
|
edges.push({
|
|
82
90
|
id: obj.name + '-' + parent.name,
|
|
83
|
-
target: obj.name,
|
|
84
91
|
source: parent.name,
|
|
92
|
+
sourceHandle: parent.name,
|
|
93
|
+
target: obj.name,
|
|
94
|
+
targetHandle: obj.name,
|
|
85
95
|
animated: true,
|
|
86
96
|
markerEnd: {
|
|
87
97
|
type: MarkerType.Arrow,
|
|
88
98
|
},
|
|
99
|
+
style: {
|
|
100
|
+
strokeWidth: 3,
|
|
101
|
+
stroke: '#b0b9c2',
|
|
102
|
+
},
|
|
89
103
|
});
|
|
90
104
|
}
|
|
91
105
|
});
|
|
92
106
|
|
|
93
107
|
obj.columns.forEach(col => {
|
|
94
108
|
if (col.dimension) {
|
|
95
|
-
|
|
96
|
-
id: obj.name + '
|
|
97
|
-
target: obj.name,
|
|
109
|
+
const edge = {
|
|
110
|
+
id: col.dimension.name + '->' + obj.name + '.' + col.name,
|
|
98
111
|
source: col.dimension.name,
|
|
112
|
+
sourceHandle: col.dimension.name,
|
|
113
|
+
target: obj.name,
|
|
114
|
+
targetHandle: obj.name + '.' + col.name,
|
|
99
115
|
draggable: true,
|
|
100
|
-
|
|
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);
|
|
101
128
|
}
|
|
102
129
|
});
|
|
103
130
|
});
|
|
@@ -126,14 +153,15 @@ const NodeLineage = djNode => {
|
|
|
126
153
|
type: node.type,
|
|
127
154
|
primary_key: primary_key,
|
|
128
155
|
column_names: column_names,
|
|
129
|
-
|
|
156
|
+
is_current: node.name === djNode.djNode.name,
|
|
130
157
|
},
|
|
131
|
-
// parentNode: [node.name.split(".").slice(-2, -1)],
|
|
132
|
-
// extent: 'parent',
|
|
133
158
|
};
|
|
134
159
|
});
|
|
135
160
|
setNodes(nodes);
|
|
136
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
|
|
137
165
|
setElementsLayout(nodes, edges);
|
|
138
166
|
};
|
|
139
167
|
|
|
@@ -146,7 +174,7 @@ const NodeLineage = djNode => {
|
|
|
146
174
|
);
|
|
147
175
|
|
|
148
176
|
return (
|
|
149
|
-
<div style={{ height: '
|
|
177
|
+
<div style={{ height: '800px' }}>
|
|
150
178
|
<ReactFlow
|
|
151
179
|
nodes={nodes}
|
|
152
180
|
edges={edges}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function NodeHistory({ node, djClient }) {
|
|
4
|
+
const [history, setHistory] = useState([]);
|
|
5
|
+
const [revisions, setRevisions] = useState([]);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const fetchData = async () => {
|
|
9
|
+
const data = await djClient.history('node', node.name);
|
|
10
|
+
setHistory(data);
|
|
11
|
+
const revisions = await djClient.revisions(node.name);
|
|
12
|
+
setRevisions(revisions);
|
|
13
|
+
};
|
|
14
|
+
fetchData().catch(console.error);
|
|
15
|
+
}, [djClient, node]);
|
|
16
|
+
const tableData = history => {
|
|
17
|
+
return history.map(event => (
|
|
18
|
+
<tr>
|
|
19
|
+
<td className="text-start">
|
|
20
|
+
<span
|
|
21
|
+
className={`history_type__${event.activity_type} badge node_type`}
|
|
22
|
+
>
|
|
23
|
+
{event.activity_type}
|
|
24
|
+
</span>
|
|
25
|
+
</td>
|
|
26
|
+
<td>{event.entity_type}</td>
|
|
27
|
+
<td>{event.entity_name}</td>
|
|
28
|
+
<td>{event.user ? event.user : 'unknown'}</td>
|
|
29
|
+
<td>{event.created_at}</td>
|
|
30
|
+
</tr>
|
|
31
|
+
));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const revisionsTable = revisions => {
|
|
35
|
+
return revisions.map(revision => (
|
|
36
|
+
<tr>
|
|
37
|
+
<td className="text-start">
|
|
38
|
+
<span className={`badge node_type__source`}>{revision.version}</span>
|
|
39
|
+
</td>
|
|
40
|
+
<td>{revision.display_name}</td>
|
|
41
|
+
<td>{revision.description}</td>
|
|
42
|
+
<td>{revision.query}</td>
|
|
43
|
+
<td>{revision.tags}</td>
|
|
44
|
+
</tr>
|
|
45
|
+
));
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
<div className="table-responsive">
|
|
49
|
+
<table className="card-inner-table table">
|
|
50
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
51
|
+
<th className="text-start">Version</th>
|
|
52
|
+
<th>Display Name</th>
|
|
53
|
+
<th>Description</th>
|
|
54
|
+
<th>Query</th>
|
|
55
|
+
<th>Tags</th>
|
|
56
|
+
</thead>
|
|
57
|
+
{revisionsTable(revisions)}
|
|
58
|
+
</table>
|
|
59
|
+
<table className="card-inner-table table">
|
|
60
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
61
|
+
<th className="text-start">Activity</th>
|
|
62
|
+
<th>Type</th>
|
|
63
|
+
<th>Name</th>
|
|
64
|
+
<th>User</th>
|
|
65
|
+
<th>Timestamp</th>
|
|
66
|
+
</thead>
|
|
67
|
+
{tableData(history)}
|
|
68
|
+
</table>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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
5
|
import { format } from 'sql-formatter';
|
|
6
|
-
|
|
7
6
|
import NodeStatus from './NodeStatus';
|
|
8
7
|
import ListGroupItem from '../../components/ListGroupItem';
|
|
8
|
+
import ToggleSwitch from '../../components/ToggleSwitch';
|
|
9
|
+
import DJClientContext from '../../providers/djclient';
|
|
9
10
|
|
|
10
11
|
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
11
12
|
foundation.hljs['padding'] = '2rem';
|
|
12
13
|
|
|
13
|
-
export default
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
export default function NodeInfoTab({ node }) {
|
|
15
|
+
const [compiledSQL, setCompiledSQL] = useState('');
|
|
16
|
+
const [checked, setChecked] = useState(false);
|
|
17
|
+
const nodeTags = node?.tags.map(tag => <div>{tag}</div>);
|
|
18
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const fetchData = async () => {
|
|
21
|
+
const data = await djClient.compiledSql(node.name);
|
|
22
|
+
if (data.sql) {
|
|
23
|
+
setCompiledSQL(data.sql);
|
|
24
|
+
} else {
|
|
25
|
+
setCompiledSQL('/* Ran into an issue while generating compiled SQL */');
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
fetchData().catch(console.error);
|
|
29
|
+
}, [node, djClient]);
|
|
30
|
+
function toggle(value) {
|
|
31
|
+
return !value;
|
|
32
|
+
}
|
|
33
|
+
const queryDiv = node?.query ? (
|
|
16
34
|
<div className="list-group-item d-flex">
|
|
17
35
|
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
18
36
|
<div
|
|
@@ -21,15 +39,34 @@ export default class NodeInfoTab extends Component {
|
|
|
21
39
|
}}
|
|
22
40
|
>
|
|
23
41
|
<h6 className="mb-0 w-100">Query</h6>
|
|
42
|
+
{['metric', 'dimension', 'transform'].indexOf(node?.type) > -1 ? (
|
|
43
|
+
<ToggleSwitch
|
|
44
|
+
id="toggleSwitch"
|
|
45
|
+
checked={checked}
|
|
46
|
+
onChange={() => setChecked(toggle)}
|
|
47
|
+
toggleName="Show Compiled SQL"
|
|
48
|
+
/>
|
|
49
|
+
) : (
|
|
50
|
+
<></>
|
|
51
|
+
)}
|
|
24
52
|
<SyntaxHighlighter language="sql" style={foundation}>
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
})}
|
|
33
70
|
</SyntaxHighlighter>
|
|
34
71
|
</div>
|
|
35
72
|
</div>
|
|
@@ -38,50 +75,96 @@ export default class NodeInfoTab extends Component {
|
|
|
38
75
|
<></>
|
|
39
76
|
);
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<div className="
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
const cubeElementsDiv = node?.cube_elements ? (
|
|
79
|
+
<div className="list-group-item d-flex">
|
|
80
|
+
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
width: window.innerWidth * 0.8,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<h6 className="mb-0 w-100">Cube Elements</h6>
|
|
87
|
+
<div className={`list-group-item`}>
|
|
88
|
+
{node.cube_elements.map(cubeElem => (
|
|
89
|
+
<div className="button-3 cube-element">
|
|
90
|
+
<a href={`/nodes/${cubeElem.node_name}`}>
|
|
91
|
+
{cubeElem.type === 'metric'
|
|
92
|
+
? cubeElem.node_name
|
|
93
|
+
: cubeElem.name}
|
|
94
|
+
</a>
|
|
54
95
|
<span
|
|
55
|
-
className=
|
|
56
|
-
|
|
96
|
+
className={`badge node_type__${
|
|
97
|
+
cubeElem.type === 'metric' ? cubeElem.type : 'dimension'
|
|
98
|
+
}`}
|
|
57
99
|
>
|
|
58
|
-
{
|
|
100
|
+
{cubeElem.type === 'metric' ? cubeElem.type : 'dimension'}
|
|
59
101
|
</span>
|
|
60
|
-
</
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
) : (
|
|
109
|
+
<></>
|
|
110
|
+
);
|
|
111
|
+
return (
|
|
112
|
+
<div className="list-group align-items-center justify-content-between flex-md-row gap-2">
|
|
113
|
+
<ListGroupItem label="Description" value={node?.description} />
|
|
114
|
+
<div className="list-group-item d-flex">
|
|
115
|
+
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
116
|
+
<div>
|
|
117
|
+
<h6 className="mb-0 w-100">Version</h6>
|
|
118
|
+
|
|
119
|
+
<p className="mb-0 opacity-75">
|
|
120
|
+
<span
|
|
121
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
122
|
+
style={{ marginLeft: '0.5rem', fontSize: '100%' }}
|
|
123
|
+
>
|
|
124
|
+
{node?.version}
|
|
125
|
+
</span>
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
{node.type === 'source' ? (
|
|
68
129
|
<div>
|
|
69
|
-
<h6 className="mb-0 w-100">
|
|
130
|
+
<h6 className="mb-0 w-100">Table</h6>
|
|
70
131
|
<p className="mb-0 opacity-75">
|
|
71
|
-
|
|
132
|
+
{node?.catalog.name}.{node?.schema_}.{node?.table}
|
|
72
133
|
</p>
|
|
73
134
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
135
|
+
) : (
|
|
136
|
+
<></>
|
|
137
|
+
)}
|
|
138
|
+
<div>
|
|
139
|
+
<h6 className="mb-0 w-100">Status</h6>
|
|
140
|
+
<p className="mb-0 opacity-75">
|
|
141
|
+
<NodeStatus node={node} />
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
<h6 className="mb-0 w-100">Mode</h6>
|
|
146
|
+
<p className="mb-0 opacity-75">
|
|
147
|
+
<span className="status">{node?.mode}</span>
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div>
|
|
151
|
+
<h6 className="mb-0 w-100">Tags</h6>
|
|
152
|
+
<p className="mb-0 opacity-75">{nodeTags}</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div>
|
|
155
|
+
<h6 className="mb-0 w-100">Primary Key</h6>
|
|
156
|
+
<p className="mb-0 opacity-75">{node?.primary_key}</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
<h6 className="mb-0 w-100">Last Updated</h6>
|
|
160
|
+
<p className="mb-0 opacity-75">
|
|
161
|
+
{new Date(node?.updated_at).toDateString()}
|
|
162
|
+
</p>
|
|
78
163
|
</div>
|
|
79
|
-
</div>
|
|
80
|
-
{this.queryDiv}
|
|
81
|
-
<div className="list-group-item d-flex">
|
|
82
|
-
{this.props.node?.primary_key}
|
|
83
164
|
</div>
|
|
84
165
|
</div>
|
|
85
|
-
|
|
86
|
-
|
|
166
|
+
{node?.type !== 'cube' ? queryDiv : ''}
|
|
167
|
+
{cubeElementsDiv}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
87
170
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import ClientCodePopover from './ClientCodePopover';
|
|
3
|
+
import TableIcon from '../../icons/TableIcon';
|
|
4
|
+
|
|
5
|
+
const cronstrue = require('cronstrue');
|
|
6
|
+
|
|
7
|
+
export default function NodeMaterializationTab({ node, djClient }) {
|
|
8
|
+
const [materializations, setMaterializations] = useState([]);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const fetchData = async () => {
|
|
11
|
+
const data = await djClient.materializations(node.name);
|
|
12
|
+
setMaterializations(data);
|
|
13
|
+
};
|
|
14
|
+
fetchData().catch(console.error);
|
|
15
|
+
}, [djClient, node]);
|
|
16
|
+
|
|
17
|
+
const rangePartition = partition => {
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<span className="badge partition_value">
|
|
21
|
+
<span className="badge partition_value">{partition.range[0]}</span>to
|
|
22
|
+
<span className="badge partition_value">{partition.range[1]}</span>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const cron = materialization => {
|
|
29
|
+
var parsedCron = '';
|
|
30
|
+
try {
|
|
31
|
+
parsedCron = cronstrue.toString(materialization.schedule);
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
return parsedCron;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const materializationRows = materializations => {
|
|
37
|
+
return materializations.map(materialization => (
|
|
38
|
+
<tr>
|
|
39
|
+
<td className="text-start node_name">
|
|
40
|
+
<a href={materialization.urls[0]}>{materialization.name}</a>
|
|
41
|
+
<ClientCodePopover code={materialization.clientCode} />
|
|
42
|
+
</td>
|
|
43
|
+
<td>
|
|
44
|
+
<span className={`badge cron`}>{materialization.schedule}</span>
|
|
45
|
+
<div className={`cron-description`}>{cron(materialization)} </div>
|
|
46
|
+
</td>
|
|
47
|
+
<td>
|
|
48
|
+
{materialization.engine.name}
|
|
49
|
+
<br />
|
|
50
|
+
{materialization.engine.version}
|
|
51
|
+
</td>
|
|
52
|
+
<td>
|
|
53
|
+
{materialization.config.partitions ? (
|
|
54
|
+
materialization.config.partitions.map(partition =>
|
|
55
|
+
partition.type_ === 'categorical' ? (
|
|
56
|
+
<div className="partition__full">
|
|
57
|
+
<div className="partition__header">{partition.name}</div>
|
|
58
|
+
<div className="partition__body">
|
|
59
|
+
{partition.values !== null && partition.values.length > 0
|
|
60
|
+
? partition.values.map(val => (
|
|
61
|
+
<span className="badge partition_value">{val}</span>
|
|
62
|
+
))
|
|
63
|
+
: null}
|
|
64
|
+
{partition.range !== null && partition.range.length > 0
|
|
65
|
+
? rangePartition(partition)
|
|
66
|
+
: null}
|
|
67
|
+
{(partition.range === null && partition.values === null) ||
|
|
68
|
+
(partition.range.length === 0 &&
|
|
69
|
+
partition.values.length === 0) ? (
|
|
70
|
+
<span className={`badge partition_value_highlight`}>
|
|
71
|
+
ALL
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
) : null,
|
|
77
|
+
)
|
|
78
|
+
) : (
|
|
79
|
+
<br />
|
|
80
|
+
)}
|
|
81
|
+
</td>
|
|
82
|
+
<td>
|
|
83
|
+
{materialization.output_tables.map(table => (
|
|
84
|
+
<div className={`table__full`}>
|
|
85
|
+
<div className="table__header">
|
|
86
|
+
<TableIcon />{' '}
|
|
87
|
+
<span className={`entity-info`}>
|
|
88
|
+
{table.split('.')[0] + '.' + table.split('.')[1]}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className={`table__body upstream_tables`}>
|
|
92
|
+
{table.split('.')[2]}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
</td>
|
|
97
|
+
{/*<td>{Object.keys(materialization.config.spark).map(key => <li className={`list-group-item`}>{key}: {materialization.config.spark[key]}</li>)}</td>*/}
|
|
98
|
+
|
|
99
|
+
<td>
|
|
100
|
+
{materialization.config.partitions ? (
|
|
101
|
+
materialization.config.partitions.map(partition =>
|
|
102
|
+
partition.type_ === 'temporal' ? (
|
|
103
|
+
<div className="partition__full">
|
|
104
|
+
<div className="partition__header">{partition.name}</div>
|
|
105
|
+
<div className="partition__body">
|
|
106
|
+
{partition.values !== null && partition.values.length > 0
|
|
107
|
+
? partition.values.map(val => (
|
|
108
|
+
<span className="badge partition_value">{val}</span>
|
|
109
|
+
))
|
|
110
|
+
: null}
|
|
111
|
+
{partition.range !== null && partition.range.length > 0
|
|
112
|
+
? rangePartition(partition)
|
|
113
|
+
: null}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
) : null,
|
|
117
|
+
)
|
|
118
|
+
) : (
|
|
119
|
+
<br />
|
|
120
|
+
)}
|
|
121
|
+
</td>
|
|
122
|
+
<td>
|
|
123
|
+
{materialization.urls.map((url, idx) => (
|
|
124
|
+
<a href={url}>[{idx + 1}]</a>
|
|
125
|
+
))}
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
));
|
|
129
|
+
};
|
|
130
|
+
return (
|
|
131
|
+
<div className="table-responsive">
|
|
132
|
+
<table className="card-inner-table table">
|
|
133
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
134
|
+
<th className="text-start">Name</th>
|
|
135
|
+
<th>Schedule</th>
|
|
136
|
+
<th>Engine</th>
|
|
137
|
+
<th>Partitions</th>
|
|
138
|
+
<th>Output Tables</th>
|
|
139
|
+
<th>Backfills</th>
|
|
140
|
+
<th>URLs</th>
|
|
141
|
+
</thead>
|
|
142
|
+
{materializationRows(
|
|
143
|
+
materializations.filter(
|
|
144
|
+
materialization =>
|
|
145
|
+
!(materialization.name === 'default' && node.type === 'cube'),
|
|
146
|
+
),
|
|
147
|
+
)}
|
|
148
|
+
</table>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import Select from 'react-select';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
5
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
6
|
+
import { format } from 'sql-formatter';
|
|
7
|
+
|
|
8
|
+
const NodeSQLTab = djNode => {
|
|
9
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
10
|
+
const [query, setQuery] = useState('');
|
|
11
|
+
|
|
12
|
+
const [selection, setSelection] = useState({
|
|
13
|
+
dimensions: [],
|
|
14
|
+
filters: [],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchData = async () => {
|
|
19
|
+
const query = await djClient.sql(djNode.djNode.name, selection);
|
|
20
|
+
setQuery(query.sql);
|
|
21
|
+
};
|
|
22
|
+
fetchData().catch(console.error);
|
|
23
|
+
}, [djClient, djNode.djNode.name, selection]);
|
|
24
|
+
const dimensionsList = djNode.djNode.dimensions
|
|
25
|
+
? djNode.djNode.dimensions.map(dim => ({
|
|
26
|
+
value: dim.name,
|
|
27
|
+
label: dim.name + ` (${dim.type})`,
|
|
28
|
+
}))
|
|
29
|
+
: [''];
|
|
30
|
+
|
|
31
|
+
// const options = [
|
|
32
|
+
// { value: '>=', label: '>=' },
|
|
33
|
+
// { value: '<=', label: '<=' },
|
|
34
|
+
// { value: '>', label: '>' },
|
|
35
|
+
// { value: '<', label: '<' },
|
|
36
|
+
// { value: '=', label: '=' },
|
|
37
|
+
// { value: '!=', label: '!=' },
|
|
38
|
+
// { value: 'IN', label: 'IN' },
|
|
39
|
+
// ];
|
|
40
|
+
|
|
41
|
+
const handleSubmit = event => {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleChange = event => {
|
|
46
|
+
setSelection({ filters: [], dimensions: event.map(dim => dim.value) });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<form
|
|
51
|
+
id="retrieve-sql"
|
|
52
|
+
name="retrieve-sql"
|
|
53
|
+
onSubmit={handleSubmit.bind(this)}
|
|
54
|
+
>
|
|
55
|
+
<div>
|
|
56
|
+
<h4>Group By</h4>
|
|
57
|
+
<Select
|
|
58
|
+
name="dimensions"
|
|
59
|
+
options={dimensionsList}
|
|
60
|
+
isMulti
|
|
61
|
+
isClearable
|
|
62
|
+
onChange={handleChange}
|
|
63
|
+
/>
|
|
64
|
+
{/*<h4>Filters</h4>*/}
|
|
65
|
+
{/*<Select*/}
|
|
66
|
+
{/* name="filter_name"*/}
|
|
67
|
+
{/* options={dimensionsList}*/}
|
|
68
|
+
{/* className="filters_attribute"*/}
|
|
69
|
+
{/*/>*/}
|
|
70
|
+
{/*<Select*/}
|
|
71
|
+
{/* name="filter_operator"*/}
|
|
72
|
+
{/* options={options}*/}
|
|
73
|
+
{/* className="filters_attribute"*/}
|
|
74
|
+
{/*/>*/}
|
|
75
|
+
{/*<textarea name="filter_value" className="filters_attribute" />*/}
|
|
76
|
+
|
|
77
|
+
<div
|
|
78
|
+
style={{
|
|
79
|
+
width: window.innerWidth * 0.8,
|
|
80
|
+
marginTop: '2rem',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<h6 className="mb-0 w-100">Query</h6>
|
|
84
|
+
<SyntaxHighlighter language="sql" style={foundation}>
|
|
85
|
+
{format(query, {
|
|
86
|
+
language: 'spark',
|
|
87
|
+
tabWidth: 2,
|
|
88
|
+
keywordCase: 'upper',
|
|
89
|
+
denseOperators: true,
|
|
90
|
+
logicalOperatorNewline: 'before',
|
|
91
|
+
expressionWidth: 10,
|
|
92
|
+
})}
|
|
93
|
+
</SyntaxHighlighter>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</form>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default NodeSQLTab;
|