datajunction-ui 0.0.1-rc.10 → 0.0.1-rc.12
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 +317 -0
- package/src/app/services/DJService.js +125 -1
- package/src/styles/dag.css +111 -5
- package/src/styles/index.css +328 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.12",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@types/react": "^18.0.20",
|
|
33
33
|
"@types/react-dom": "^18.0.6",
|
|
34
34
|
"@types/react-redux": "^7.1.24",
|
|
35
|
+
"@types/react-select": "5.0.1",
|
|
35
36
|
"@types/react-test-renderer": "^18.0.0",
|
|
36
37
|
"@types/rimraf": "^3.0.2",
|
|
37
38
|
"@types/shelljs": "^0.8.11",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"@types/webpack-env": "^1.18.0",
|
|
41
42
|
"babel-loader": "9.1.2",
|
|
42
43
|
"chalk": "4.1.2",
|
|
44
|
+
"cronstrue": "2.27.0",
|
|
43
45
|
"cross-env": "7.0.3",
|
|
44
46
|
"css-loader": "6.7.3",
|
|
45
47
|
"dagre": "^0.8.5",
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
"react-redux": "7.2.8",
|
|
66
68
|
"react-router-dom": "6.3.0",
|
|
67
69
|
"react-scripts": "5.0.1",
|
|
70
|
+
"react-select": "5.7.3",
|
|
68
71
|
"react-syntax-highlighter": "^15.5.0",
|
|
69
72
|
"react-test-renderer": "18.2.0",
|
|
70
73
|
"reactflow": "^11.7.0",
|
|
@@ -18,13 +18,26 @@ exports[`<App /> should render and match the snapshot 1`] = `
|
|
|
18
18
|
value={
|
|
19
19
|
Object {
|
|
20
20
|
"DataJunctionAPI": Object {
|
|
21
|
+
"clientCode": [Function],
|
|
22
|
+
"columns": [Function],
|
|
23
|
+
"commonDimensions": [Function],
|
|
24
|
+
"compiledSql": [Function],
|
|
25
|
+
"cube": [Function],
|
|
21
26
|
"dag": [Function],
|
|
27
|
+
"data": [Function],
|
|
22
28
|
"downstreams": [Function],
|
|
29
|
+
"history": [Function],
|
|
23
30
|
"lineage": [Function],
|
|
31
|
+
"materializations": [Function],
|
|
24
32
|
"metric": [Function],
|
|
33
|
+
"metrics": [Function],
|
|
25
34
|
"namespace": [Function],
|
|
26
35
|
"namespaces": [Function],
|
|
27
36
|
"node": [Function],
|
|
37
|
+
"node_dag": [Function],
|
|
38
|
+
"revisions": [Function],
|
|
39
|
+
"sql": [Function],
|
|
40
|
+
"sqls": [Function],
|
|
28
41
|
"upstreams": [Function],
|
|
29
42
|
},
|
|
30
43
|
}
|
|
@@ -45,22 +58,7 @@ exports[`<App /> should render and match the snapshot 1`] = `
|
|
|
45
58
|
/>
|
|
46
59
|
</Route>
|
|
47
60
|
<Route
|
|
48
|
-
element={
|
|
49
|
-
<ListNamespacesPage
|
|
50
|
-
djClient={
|
|
51
|
-
Object {
|
|
52
|
-
"dag": [Function],
|
|
53
|
-
"downstreams": [Function],
|
|
54
|
-
"lineage": [Function],
|
|
55
|
-
"metric": [Function],
|
|
56
|
-
"namespace": [Function],
|
|
57
|
-
"namespaces": [Function],
|
|
58
|
-
"node": [Function],
|
|
59
|
-
"upstreams": [Function],
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/>
|
|
63
|
-
}
|
|
61
|
+
element={<NamespacePage />}
|
|
64
62
|
path="/"
|
|
65
63
|
/>
|
|
66
64
|
<Route
|
|
@@ -71,6 +69,10 @@ exports[`<App /> should render and match the snapshot 1`] = `
|
|
|
71
69
|
path=":namespace"
|
|
72
70
|
/>
|
|
73
71
|
</Route>
|
|
72
|
+
<Route
|
|
73
|
+
element={<SQLBuilderPage />}
|
|
74
|
+
path="sql"
|
|
75
|
+
/>
|
|
74
76
|
</React.Fragment>
|
|
75
77
|
</Route>
|
|
76
78
|
<Route
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component } from 'react';
|
|
2
|
+
import HorizontalHierarchyIcon from '../icons/HorizontalHierarchyIcon';
|
|
2
3
|
|
|
3
4
|
export default class NamespaceHeader extends Component {
|
|
4
5
|
render() {
|
|
@@ -6,7 +7,7 @@ export default class NamespaceHeader extends Component {
|
|
|
6
7
|
const namespaceParts = namespace.split('.');
|
|
7
8
|
const namespaceList = namespaceParts.map((piece, index) => {
|
|
8
9
|
return (
|
|
9
|
-
<li className="breadcrumb-item">
|
|
10
|
+
<li className="breadcrumb-item" key={index}>
|
|
10
11
|
<a
|
|
11
12
|
className="link-body-emphasis"
|
|
12
13
|
href={'/namespaces/' + namespaceParts.slice(0, index + 1).join('.')}
|
|
@@ -19,18 +20,8 @@ export default class NamespaceHeader extends Component {
|
|
|
19
20
|
return (
|
|
20
21
|
<ol className="breadcrumb breadcrumb-chevron p-3 bg-body-tertiary rounded-3">
|
|
21
22
|
<li className="breadcrumb-item">
|
|
22
|
-
<a href="/
|
|
23
|
-
<
|
|
24
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
-
width="16"
|
|
26
|
-
height="16"
|
|
27
|
-
fill="currentColor"
|
|
28
|
-
className="bi bi-house-door-fill"
|
|
29
|
-
viewBox="0 0 16 16"
|
|
30
|
-
style={{ paddingBottom: '0.2rem' }}
|
|
31
|
-
>
|
|
32
|
-
<path d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z" />
|
|
33
|
-
</svg>
|
|
23
|
+
<a href="/">
|
|
24
|
+
<HorizontalHierarchyIcon />
|
|
34
25
|
</a>
|
|
35
26
|
</li>
|
|
36
27
|
{namespaceList}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export default function QueryInfo({
|
|
2
|
+
id,
|
|
3
|
+
state,
|
|
4
|
+
engine_name,
|
|
5
|
+
engine_version,
|
|
6
|
+
errors,
|
|
7
|
+
links,
|
|
8
|
+
output_table,
|
|
9
|
+
scheduled,
|
|
10
|
+
started,
|
|
11
|
+
numRows,
|
|
12
|
+
}) {
|
|
13
|
+
const stateIcon =
|
|
14
|
+
state === 'FINISHED' ? (
|
|
15
|
+
<span className="status__valid status" style={{ alignContent: 'center' }}>
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
width="25"
|
|
19
|
+
height="25"
|
|
20
|
+
fill="currentColor"
|
|
21
|
+
className="bi bi-check-circle-fill"
|
|
22
|
+
viewBox="0 0 16 16"
|
|
23
|
+
>
|
|
24
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
|
|
25
|
+
</svg>
|
|
26
|
+
</span>
|
|
27
|
+
) : (
|
|
28
|
+
<span
|
|
29
|
+
className="status__invalid status"
|
|
30
|
+
style={{ alignContent: 'center' }}
|
|
31
|
+
>
|
|
32
|
+
<svg
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
+
width="16"
|
|
35
|
+
height="16"
|
|
36
|
+
fill="currentColor"
|
|
37
|
+
className="bi bi-x-circle-fill"
|
|
38
|
+
viewBox="0 0 16 16"
|
|
39
|
+
>
|
|
40
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
|
41
|
+
</svg>
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="table-responsive">
|
|
47
|
+
<table className="card-inner-table table">
|
|
48
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
49
|
+
<tr>
|
|
50
|
+
<th>Query ID</th>
|
|
51
|
+
<th>Engine</th>
|
|
52
|
+
<th>State</th>
|
|
53
|
+
<th>Scheduled</th>
|
|
54
|
+
<th>Started</th>
|
|
55
|
+
<th>Errors</th>
|
|
56
|
+
<th>Links</th>
|
|
57
|
+
<th>Output Table</th>
|
|
58
|
+
<th>Number of Rows</th>
|
|
59
|
+
</tr>
|
|
60
|
+
</thead>
|
|
61
|
+
<tbody>
|
|
62
|
+
<tr>
|
|
63
|
+
<td>
|
|
64
|
+
<span className="rounded-pill badge bg-secondary-soft">{id}</span>
|
|
65
|
+
</td>
|
|
66
|
+
<td>
|
|
67
|
+
<span className="rounded-pill badge bg-secondary-soft">
|
|
68
|
+
{engine_name}
|
|
69
|
+
{' - '}
|
|
70
|
+
{engine_version}
|
|
71
|
+
</span>
|
|
72
|
+
</td>
|
|
73
|
+
<td>{stateIcon}</td>
|
|
74
|
+
<td>{scheduled}</td>
|
|
75
|
+
<td>{started}</td>
|
|
76
|
+
<td>
|
|
77
|
+
{errors.length ? (
|
|
78
|
+
errors.map(e => (
|
|
79
|
+
<p>
|
|
80
|
+
<span className="rounded-pill badge bg-secondary-error">
|
|
81
|
+
{e}
|
|
82
|
+
</span>
|
|
83
|
+
</p>
|
|
84
|
+
))
|
|
85
|
+
) : (
|
|
86
|
+
<></>
|
|
87
|
+
)}
|
|
88
|
+
</td>
|
|
89
|
+
<td>
|
|
90
|
+
{links?.length ? (
|
|
91
|
+
links.map(link => (
|
|
92
|
+
<p>
|
|
93
|
+
<a href={link} target="_blank" rel="noreferrer">
|
|
94
|
+
{link}
|
|
95
|
+
</a>
|
|
96
|
+
</p>
|
|
97
|
+
))
|
|
98
|
+
) : (
|
|
99
|
+
<></>
|
|
100
|
+
)}
|
|
101
|
+
</td>
|
|
102
|
+
<td>{output_table}</td>
|
|
103
|
+
<td>{numRows}</td>
|
|
104
|
+
</tr>
|
|
105
|
+
</tbody>
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const ToggleSwitch = ({ checked, onChange, toggleName }) => (
|
|
4
|
+
<>
|
|
5
|
+
<input
|
|
6
|
+
id="show-compiled-sql-toggle"
|
|
7
|
+
type="checkbox"
|
|
8
|
+
className="checkbox"
|
|
9
|
+
checked={checked}
|
|
10
|
+
onChange={e => onChange(e.target.checked)}
|
|
11
|
+
/>
|
|
12
|
+
<label htmlFor="show-compiled-sql-toggle" className="switch"></label>{' '}
|
|
13
|
+
{toggleName}
|
|
14
|
+
</>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export default ToggleSwitch;
|
|
@@ -8,25 +8,9 @@ exports[`<NamespaceHeader /> should render and match the snapshot 1`] = `
|
|
|
8
8
|
className="breadcrumb-item"
|
|
9
9
|
>
|
|
10
10
|
<a
|
|
11
|
-
href="/
|
|
11
|
+
href="/"
|
|
12
12
|
>
|
|
13
|
-
<
|
|
14
|
-
className="bi bi-house-door-fill"
|
|
15
|
-
fill="currentColor"
|
|
16
|
-
height="16"
|
|
17
|
-
style={
|
|
18
|
-
Object {
|
|
19
|
-
"paddingBottom": "0.2rem",
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
viewBox="0 0 16 16"
|
|
23
|
-
width="16"
|
|
24
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
-
>
|
|
26
|
-
<path
|
|
27
|
-
d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z"
|
|
28
|
-
/>
|
|
29
|
-
</svg>
|
|
13
|
+
<HorizontalHierarchyIcon />
|
|
30
14
|
</a>
|
|
31
15
|
</li>
|
|
32
16
|
<li
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DJNodeDimensions } from './DJNodeDimensions';
|
|
3
|
+
import { DJNodeColumns } from './DJNodeColumns';
|
|
4
|
+
|
|
5
|
+
export default function Collapse({ collapsed, text, data }) {
|
|
6
|
+
const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<div className="collapse">
|
|
11
|
+
{data.type === 'metric' ? (
|
|
12
|
+
<button
|
|
13
|
+
className="collapse-button"
|
|
14
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
15
|
+
>
|
|
16
|
+
{isCollapsed ? '\u25B6 Show' : '\u25BC Hide'} {text}
|
|
17
|
+
</button>
|
|
18
|
+
) : (
|
|
19
|
+
''
|
|
20
|
+
)}
|
|
21
|
+
<div
|
|
22
|
+
className={`collapse-content ${
|
|
23
|
+
isCollapsed && data.type === 'metric' ? 'collapsed' : 'expanded'
|
|
24
|
+
}`}
|
|
25
|
+
aria-expanded={isCollapsed}
|
|
26
|
+
>
|
|
27
|
+
{data.type !== 'metric'
|
|
28
|
+
? isCollapsed
|
|
29
|
+
? DJNodeColumns({ data: data, limit: 10 })
|
|
30
|
+
: DJNodeColumns({ data: data, limit: 100 })
|
|
31
|
+
: DJNodeDimensions(data)}
|
|
32
|
+
</div>
|
|
33
|
+
{data.type !== 'metric' && data.column_names.length > 10 ? (
|
|
34
|
+
<button
|
|
35
|
+
className="collapse-button"
|
|
36
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
37
|
+
>
|
|
38
|
+
{isCollapsed ? '\u25B6 More' : '\u25BC Less'} {text}
|
|
39
|
+
</button>
|
|
40
|
+
) : (
|
|
41
|
+
''
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,81 +1,57 @@
|
|
|
1
1
|
import React, { memo } from 'react';
|
|
2
2
|
import { Handle, Position } from 'reactflow';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
switch (param) {
|
|
6
|
-
case 'source':
|
|
7
|
-
return { backgroundColor: '#7EB46150', color: '#7EB461' };
|
|
8
|
-
case 'transform':
|
|
9
|
-
return { backgroundColor: '#6DAAA750', color: '#6DAAA7' };
|
|
10
|
-
case 'dimension':
|
|
11
|
-
return { backgroundColor: '#CF7D2950', color: '#CF7D29' };
|
|
12
|
-
case 'metric':
|
|
13
|
-
return { backgroundColor: '#A27E8650', color: '#A27E86' };
|
|
14
|
-
case 'cube':
|
|
15
|
-
return { backgroundColor: '#C2180750', color: '#C21807' };
|
|
16
|
-
default:
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
3
|
+
import { DJNodeDimensions } from './DJNodeDimensions';
|
|
4
|
+
import Collapse from './Collapse';
|
|
20
5
|
|
|
21
6
|
function capitalize(string) {
|
|
22
7
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
23
8
|
}
|
|
24
9
|
|
|
25
|
-
const Collapse = ({ collapsed, text, children }) => {
|
|
26
|
-
const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<>
|
|
30
|
-
<div className="collapse">
|
|
31
|
-
<button
|
|
32
|
-
className="collapse-button"
|
|
33
|
-
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
34
|
-
>
|
|
35
|
-
{isCollapsed ? '\u25B6 Show' : '\u25BC Hide'} {text}
|
|
36
|
-
</button>
|
|
37
|
-
<div
|
|
38
|
-
className={`collapse-content ${
|
|
39
|
-
isCollapsed ? 'collapsed' : 'expanded'
|
|
40
|
-
}`}
|
|
41
|
-
aria-expanded={isCollapsed}
|
|
42
|
-
>
|
|
43
|
-
{children}
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
10
|
export function DJNode({ id, data }) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</td>
|
|
61
|
-
<td style={{ textAlign: 'right' }}>{col.type}</td>
|
|
62
|
-
</tr>
|
|
63
|
-
));
|
|
64
|
-
// const dimensionsRenderer = data =>
|
|
65
|
-
// data.dimensions.map(dim => (
|
|
66
|
-
// <tr>
|
|
67
|
-
// <td>{dim}</td>
|
|
68
|
-
// </tr>
|
|
69
|
-
// ));
|
|
11
|
+
const handleWrapperStyle = {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
height: '100%',
|
|
15
|
+
flexDirection: 'column',
|
|
16
|
+
top: '50%',
|
|
17
|
+
justifyContent: 'space-between',
|
|
18
|
+
};
|
|
19
|
+
const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
|
|
70
20
|
|
|
21
|
+
const handleStyle = {
|
|
22
|
+
width: '12px',
|
|
23
|
+
height: '12px',
|
|
24
|
+
borderRadius: '12px',
|
|
25
|
+
background: 'transparent',
|
|
26
|
+
border: '4px solid transparent',
|
|
27
|
+
cursor: 'pointer',
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
top: '0px',
|
|
30
|
+
left: 0,
|
|
31
|
+
};
|
|
32
|
+
const handleStyleLeft = percentage => {
|
|
33
|
+
return {
|
|
34
|
+
...handleStyle,
|
|
35
|
+
...{
|
|
36
|
+
transform: 'translate(-' + percentage + '%, -50%)',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const highlightNodeClass =
|
|
41
|
+
data.is_current === true ? ' dj-node_highlight' : '';
|
|
71
42
|
return (
|
|
72
43
|
<>
|
|
73
|
-
<
|
|
74
|
-
type
|
|
75
|
-
|
|
76
|
-
style={
|
|
77
|
-
|
|
78
|
-
|
|
44
|
+
<div
|
|
45
|
+
className={'dj-node__full node_type__' + data.type + highlightNodeClass}
|
|
46
|
+
>
|
|
47
|
+
<div style={handleWrapperStyle}>
|
|
48
|
+
<Handle
|
|
49
|
+
type="target"
|
|
50
|
+
id={data.name}
|
|
51
|
+
position={Position.Left}
|
|
52
|
+
style={handleStyleLeft(100)}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
79
55
|
<div className="dj-node__header">
|
|
80
56
|
<div className="serif">
|
|
81
57
|
{data.name
|
|
@@ -86,24 +62,24 @@ export function DJNode({ id, data }) {
|
|
|
86
62
|
</div>
|
|
87
63
|
<div className="dj-node__body">
|
|
88
64
|
<b>{capitalize(data.type)}</b>:{' '}
|
|
89
|
-
{data.
|
|
65
|
+
<a href={`/nodes/${data.name}`}>
|
|
66
|
+
{data.type === 'source' ? data.table : data.display_name}
|
|
67
|
+
</a>
|
|
90
68
|
<Collapse
|
|
91
69
|
collapsed={true}
|
|
92
70
|
text={data.type !== 'metric' ? 'columns' : 'dimensions'}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
data={data}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
<div style={handleWrapperStyleRight}>
|
|
75
|
+
<Handle
|
|
76
|
+
type="source"
|
|
77
|
+
id={data.name}
|
|
78
|
+
position={Position.Right}
|
|
79
|
+
style={handleStyleLeft(90)}
|
|
80
|
+
/>
|
|
100
81
|
</div>
|
|
101
82
|
</div>
|
|
102
|
-
<Handle
|
|
103
|
-
type="source"
|
|
104
|
-
position={Position.Right}
|
|
105
|
-
style={{ backgroundColor: '#ccc' }}
|
|
106
|
-
/>
|
|
107
83
|
</>
|
|
108
84
|
);
|
|
109
85
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Handle } from 'reactflow';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export function DJNodeColumns({ data, limit }) {
|
|
5
|
+
const handleWrapperStyle = {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
position: 'absolute',
|
|
8
|
+
height: '100%',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
top: '50%',
|
|
11
|
+
justifyContent: 'space-between',
|
|
12
|
+
};
|
|
13
|
+
const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
|
|
14
|
+
|
|
15
|
+
const handleStyle = {
|
|
16
|
+
width: '12px',
|
|
17
|
+
height: '12px',
|
|
18
|
+
borderRadius: '12px',
|
|
19
|
+
background: 'transparent',
|
|
20
|
+
border: '4px solid transparent',
|
|
21
|
+
cursor: 'pointer',
|
|
22
|
+
position: 'absolute',
|
|
23
|
+
top: '0px',
|
|
24
|
+
left: 0,
|
|
25
|
+
};
|
|
26
|
+
const handleStyleLeft = percentage => {
|
|
27
|
+
return {
|
|
28
|
+
...handleStyle,
|
|
29
|
+
...{
|
|
30
|
+
transform: 'translate(-' + percentage + '%, -50%)',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
return data.column_names.slice(0, limit).map(col => (
|
|
35
|
+
<div className={'custom-node-subheader node_type__' + data.type}>
|
|
36
|
+
<div style={handleWrapperStyle}>
|
|
37
|
+
<Handle
|
|
38
|
+
type="target"
|
|
39
|
+
position="left"
|
|
40
|
+
id={data.name + '.' + col.name}
|
|
41
|
+
style={handleStyleLeft(100)}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
className="custom-node-port"
|
|
46
|
+
id={data.name + '.' + col.name}
|
|
47
|
+
key={'i-' + data.name + '.' + col.name}
|
|
48
|
+
>
|
|
49
|
+
{data.primary_key.includes(col.name) ? (
|
|
50
|
+
<b>{col.name} (PK)</b>
|
|
51
|
+
) : (
|
|
52
|
+
<>{col.name}</>
|
|
53
|
+
)}
|
|
54
|
+
<span style={{ marginLeft: '0.25rem' }} className={'badge'}>
|
|
55
|
+
{col.type}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div style={handleWrapperStyleRight}>
|
|
59
|
+
<Handle
|
|
60
|
+
type="source"
|
|
61
|
+
position="right"
|
|
62
|
+
id={data.name + '.' + col.name}
|
|
63
|
+
style={handleStyle}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
));
|
|
68
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import Collapse from './Collapse';
|
|
4
|
+
|
|
5
|
+
export function DJNodeDimensions(data) {
|
|
6
|
+
const [dimensions, setDimensions] = useState([]);
|
|
7
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (data.type === 'metric') {
|
|
10
|
+
async function getDimensions() {
|
|
11
|
+
try {
|
|
12
|
+
const metricData = await djClient.metric(data.name);
|
|
13
|
+
setDimensions(metricData.dimensions);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.log(err);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getDimensions();
|
|
19
|
+
}
|
|
20
|
+
}, [data, djClient]);
|
|
21
|
+
const dimensionsToObject = dimensions => {
|
|
22
|
+
return dimensions.map(dim => {
|
|
23
|
+
const [attribute, ...nodeName] = dim.name.split('.').reverse();
|
|
24
|
+
return {
|
|
25
|
+
dimension: nodeName.reverse().join('.'),
|
|
26
|
+
path: dim.path,
|
|
27
|
+
column: attribute,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const groupedDimensions = dims =>
|
|
32
|
+
dims.reduce((acc, current) => {
|
|
33
|
+
const dimKey = current.dimension + ' via ' + current.path.slice(-1);
|
|
34
|
+
acc[dimKey] = acc[dimKey] || {
|
|
35
|
+
dimension: current.dimension,
|
|
36
|
+
path: current.path.slice(-1),
|
|
37
|
+
columns: [],
|
|
38
|
+
};
|
|
39
|
+
acc[dimKey].columns.push(current.column);
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
const dimensionsRenderer = grouped =>
|
|
43
|
+
Object.entries(grouped).map(([dimKey, dimValue]) => {
|
|
44
|
+
if (Array.isArray(dimValue.columns)) {
|
|
45
|
+
const attributes = dimValue.columns.map(col => {
|
|
46
|
+
return <span className={'badge white_badge'}>{col}</span>;
|
|
47
|
+
});
|
|
48
|
+
return (
|
|
49
|
+
<div className={'custom-node-subheader node_type__' + data.type}>
|
|
50
|
+
<div className="custom-node-port">
|
|
51
|
+
<a href={`/nodes/${dimValue.dimension}`}>{dimValue.dimension}</a>{' '}
|
|
52
|
+
<div className={'badge node_type__metric text-black'}>
|
|
53
|
+
{dimValue.path}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div className={'dimension_attributes'}>{attributes}</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return <></>;
|
|
61
|
+
});
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{dimensions.length <= 0
|
|
65
|
+
? ''
|
|
66
|
+
: dimensionsRenderer(groupedDimensions(dimensionsToObject(dimensions)))}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|