datajunction-ui 0.0.1-rc.1 → 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/.github/workflows/ci.yml +3 -3
- package/.prettierignore +3 -1
- package/package.json +16 -8
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +67 -25
- package/src/app/components/NamespaceHeader.jsx +4 -13
- package/src/app/components/QueryInfo.jsx +109 -0
- package/src/app/components/Tab.jsx +1 -8
- 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 +28 -15
- package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
- package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +39 -17
- package/src/app/pages/NamespacePage/index.jsx +90 -28
- package/src/app/pages/NodePage/ClientCodePopover.jsx +30 -0
- package/src/app/pages/NodePage/Loadable.jsx +9 -7
- package/src/app/pages/NodePage/NodeColumnTab.jsx +36 -20
- package/src/app/pages/NodePage/NodeGraphTab.jsx +83 -54
- 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 +49 -13
- 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/providers/djclient.jsx +5 -0
- package/src/app/services/DJService.js +125 -1
- package/src/styles/dag.css +111 -5
- package/src/styles/index.css +343 -25
- package/tsconfig.json +1 -1
- package/webpack.config.js +22 -6
- 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/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
- package/dist/index.html +0 -26
- package/dist/main.js +0 -23303
- package/dist/vendor.js +0 -281
- package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
- package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
|
@@ -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;
|
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
import { Component } from 'react';
|
|
2
|
+
import ValidIcon from '../../icons/ValidIcon';
|
|
3
|
+
import InvalidIcon from '../../icons/InvalidIcon';
|
|
2
4
|
|
|
3
5
|
export default class NodeStatus extends Component {
|
|
4
6
|
render() {
|
|
5
7
|
const { node } = this.props;
|
|
6
8
|
return (
|
|
7
|
-
|
|
9
|
+
<>
|
|
8
10
|
{node?.status === 'valid' ? (
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
height="25"
|
|
13
|
-
fill="currentColor"
|
|
14
|
-
className="bi bi-check-circle-fill"
|
|
15
|
-
viewBox="0 0 16 16"
|
|
11
|
+
<span
|
|
12
|
+
className="status__valid status"
|
|
13
|
+
style={{ alignContent: 'center' }}
|
|
16
14
|
>
|
|
17
|
-
<
|
|
18
|
-
</
|
|
15
|
+
<ValidIcon />
|
|
16
|
+
</span>
|
|
19
17
|
) : (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
height="16"
|
|
24
|
-
fill="currentColor"
|
|
25
|
-
className="bi bi-x-circle-fill"
|
|
26
|
-
viewBox="0 0 16 16"
|
|
18
|
+
<span
|
|
19
|
+
className="status__invalid status"
|
|
20
|
+
style={{ alignContent: 'center' }}
|
|
27
21
|
>
|
|
28
|
-
<
|
|
29
|
-
</
|
|
22
|
+
<InvalidIcon />
|
|
23
|
+
</span>
|
|
30
24
|
)}
|
|
31
|
-
|
|
25
|
+
</>
|
|
32
26
|
);
|
|
33
27
|
}
|
|
34
28
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useParams } from 'react-router-dom';
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { DataJunctionAPI } from '../../services/DJService';
|
|
3
|
+
import { useContext, useEffect, useState } from 'react';
|
|
5
4
|
import Tab from '../../components/Tab';
|
|
6
5
|
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
7
6
|
import NodeInfoTab from './NodeInfoTab';
|
|
8
7
|
import NodeColumnTab from './NodeColumnTab';
|
|
9
8
|
import NodeLineage from './NodeGraphTab';
|
|
9
|
+
import NodeHistory from './NodeHistory';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import NodeSQLTab from './NodeSQLTab';
|
|
12
|
+
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
13
|
+
import ClientCodePopover from './ClientCodePopover';
|
|
10
14
|
|
|
11
15
|
export function NodePage() {
|
|
16
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
12
17
|
const [state, setState] = useState({
|
|
13
18
|
selectedTab: 0,
|
|
14
19
|
});
|
|
@@ -35,11 +40,22 @@ export function NodePage() {
|
|
|
35
40
|
|
|
36
41
|
useEffect(() => {
|
|
37
42
|
const fetchData = async () => {
|
|
38
|
-
const data = await
|
|
43
|
+
const data = await djClient.node(name);
|
|
44
|
+
data.createNodeClientCode = await djClient.clientCode(name);
|
|
39
45
|
setNode(data);
|
|
46
|
+
if (data.type === 'metric') {
|
|
47
|
+
const metric = await djClient.metric(name);
|
|
48
|
+
data.dimensions = metric.dimensions;
|
|
49
|
+
setNode(data);
|
|
50
|
+
}
|
|
51
|
+
if (data.type === 'cube') {
|
|
52
|
+
const cube = await djClient.cube(name);
|
|
53
|
+
data.cube_elements = cube.cube_elements;
|
|
54
|
+
setNode(data);
|
|
55
|
+
}
|
|
40
56
|
};
|
|
41
57
|
fetchData().catch(console.error);
|
|
42
|
-
}, [name]);
|
|
58
|
+
}, [djClient, name]);
|
|
43
59
|
|
|
44
60
|
const TabsJson = [
|
|
45
61
|
{
|
|
@@ -54,6 +70,18 @@ export function NodePage() {
|
|
|
54
70
|
id: 2,
|
|
55
71
|
name: 'Graph',
|
|
56
72
|
},
|
|
73
|
+
{
|
|
74
|
+
id: 3,
|
|
75
|
+
name: 'History',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 4,
|
|
79
|
+
name: 'SQL',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 5,
|
|
83
|
+
name: 'Materializations',
|
|
84
|
+
},
|
|
57
85
|
];
|
|
58
86
|
//
|
|
59
87
|
//
|
|
@@ -63,10 +91,20 @@ export function NodePage() {
|
|
|
63
91
|
tabToDisplay = node ? <NodeInfoTab node={node} /> : '';
|
|
64
92
|
break;
|
|
65
93
|
case 1:
|
|
66
|
-
tabToDisplay = <NodeColumnTab node={node} />;
|
|
94
|
+
tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
|
|
67
95
|
break;
|
|
68
96
|
case 2:
|
|
69
|
-
tabToDisplay = <NodeLineage djNode={node} />;
|
|
97
|
+
tabToDisplay = <NodeLineage djNode={node} djClient={djClient} />;
|
|
98
|
+
break;
|
|
99
|
+
case 3:
|
|
100
|
+
tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
|
|
101
|
+
break;
|
|
102
|
+
case 4:
|
|
103
|
+
tabToDisplay =
|
|
104
|
+
node.type === 'metric' ? <NodeSQLTab djNode={node} /> : <br />;
|
|
105
|
+
break;
|
|
106
|
+
case 5:
|
|
107
|
+
tabToDisplay = <NodeMaterializationTab node={node} djClient={djClient} />;
|
|
70
108
|
break;
|
|
71
109
|
default:
|
|
72
110
|
tabToDisplay = <NodeInfoTab node={node} />;
|
|
@@ -78,17 +116,15 @@ export function NodePage() {
|
|
|
78
116
|
<NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
|
|
79
117
|
<div className="card">
|
|
80
118
|
<div className="card-header">
|
|
81
|
-
<h3
|
|
119
|
+
<h3
|
|
120
|
+
className="card-title align-items-start flex-column"
|
|
121
|
+
style={{ display: 'inline-block' }}
|
|
122
|
+
>
|
|
82
123
|
<span className="card-label fw-bold text-gray-800">
|
|
83
124
|
{node?.display_name}
|
|
84
125
|
</span>
|
|
85
126
|
</h3>
|
|
86
|
-
<
|
|
87
|
-
className="fs-6 fw-semibold text-gray-400"
|
|
88
|
-
style={{ marginTop: '-4rem' }}
|
|
89
|
-
>
|
|
90
|
-
Updated {new Date(node?.updated_at).toDateString()}
|
|
91
|
-
</span>
|
|
127
|
+
<ClientCodePopover code={node?.createNodeClientCode} />
|
|
92
128
|
<div className="align-items-center row">
|
|
93
129
|
{TabsJson.map(buildTabs)}
|
|
94
130
|
</div>
|
|
@@ -27,6 +27,11 @@ export function Root() {
|
|
|
27
27
|
<a href="/">Explore</a>
|
|
28
28
|
</span>
|
|
29
29
|
</span>
|
|
30
|
+
<span className="menu-link">
|
|
31
|
+
<span className="menu-title">
|
|
32
|
+
<a href="/sql">SQL</a>
|
|
33
|
+
</span>
|
|
34
|
+
</span>
|
|
30
35
|
<span className="menu-link">
|
|
31
36
|
<span className="menu-title">
|
|
32
37
|
<a href="/">Help</a>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Node page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const SQLBuilderPage = props => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.SQLBuilderPage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)(props);
|
|
16
|
+
};
|