datajunction-ui 0.0.1-a48 → 0.0.1-a49.dev2
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 +2 -2
- package/src/app/components/QueryInfo.jsx +96 -1
- package/src/app/icons/LoadingIcon.jsx +1 -1
- package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +13 -5
- package/src/app/pages/NodePage/NodeColumnTab.jsx +5 -3
- package/src/app/pages/NodePage/NodeHistory.jsx +11 -0
- package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
- package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +0 -4
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +88 -6
- package/src/app/pages/NodePage/index.jsx +6 -7
- package/src/app/services/DJService.js +58 -7
- package/src/app/services/__tests__/DJService.test.jsx +62 -0
- package/src/mocks/mockNodes.jsx +63 -0
- package/src/styles/index.css +149 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-a49.dev2",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
],
|
|
165
165
|
"coverageThreshold": {
|
|
166
166
|
"global": {
|
|
167
|
-
"statements":
|
|
167
|
+
"statements": 87,
|
|
168
168
|
"branches": 75,
|
|
169
169
|
"lines": 80,
|
|
170
170
|
"functions": 85
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
2
|
+
import { solarizedDark } from 'react-syntax-highlighter/src/styles/hljs';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
1
5
|
export default function QueryInfo({
|
|
2
6
|
id,
|
|
3
7
|
state,
|
|
@@ -8,9 +12,11 @@ export default function QueryInfo({
|
|
|
8
12
|
output_table,
|
|
9
13
|
scheduled,
|
|
10
14
|
started,
|
|
15
|
+
finished,
|
|
11
16
|
numRows,
|
|
17
|
+
isList = false,
|
|
12
18
|
}) {
|
|
13
|
-
return (
|
|
19
|
+
return isList === false ? (
|
|
14
20
|
<div className="table-responsive">
|
|
15
21
|
<table className="card-inner-table table">
|
|
16
22
|
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
@@ -73,5 +79,94 @@ export default function QueryInfo({
|
|
|
73
79
|
</tbody>
|
|
74
80
|
</table>
|
|
75
81
|
</div>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="rightbottom">
|
|
84
|
+
<ul style={{ padding: '20px' }}>
|
|
85
|
+
<li className={'query-info'}>
|
|
86
|
+
<label>Query ID</label>{' '}
|
|
87
|
+
<span className="tag_value rounded-pill badge">
|
|
88
|
+
{links?.length ? (
|
|
89
|
+
<a
|
|
90
|
+
href={links[links.length - 1]}
|
|
91
|
+
target={'_blank'}
|
|
92
|
+
rel="noreferrer"
|
|
93
|
+
>
|
|
94
|
+
{id}
|
|
95
|
+
</a>
|
|
96
|
+
) : (
|
|
97
|
+
id
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
</li>
|
|
101
|
+
<li className={'query-info'}>
|
|
102
|
+
<label>State</label>
|
|
103
|
+
<span className="tag_value rounded-pill badge">{state}</span>
|
|
104
|
+
</li>
|
|
105
|
+
<li className={'query-info'}>
|
|
106
|
+
<label>Engine</label>{' '}
|
|
107
|
+
<span className="tag_value rounded-pill badge">
|
|
108
|
+
{engine_name}
|
|
109
|
+
{' - '}
|
|
110
|
+
{engine_version}
|
|
111
|
+
</span>
|
|
112
|
+
</li>
|
|
113
|
+
<li className={'query-info'}>
|
|
114
|
+
<label>Scheduled</label> {scheduled}
|
|
115
|
+
</li>
|
|
116
|
+
<li className={'query-info'}>
|
|
117
|
+
<label>Started</label> {started}
|
|
118
|
+
</li>
|
|
119
|
+
<li className={'query-info'}>
|
|
120
|
+
<label>Finished</label> {finished}
|
|
121
|
+
</li>
|
|
122
|
+
<li className={'query-info'}>
|
|
123
|
+
<label>Logs</label>{' '}
|
|
124
|
+
{errors?.length ? (
|
|
125
|
+
errors.map(error => (
|
|
126
|
+
<div
|
|
127
|
+
style={{
|
|
128
|
+
height: '800px',
|
|
129
|
+
width: '80%',
|
|
130
|
+
overflow: 'scroll',
|
|
131
|
+
borderRadius: '0',
|
|
132
|
+
border: '1px solid #ccc',
|
|
133
|
+
}}
|
|
134
|
+
className="queryrunner-query"
|
|
135
|
+
>
|
|
136
|
+
<SyntaxHighlighter
|
|
137
|
+
language="javascript"
|
|
138
|
+
style={solarizedDark}
|
|
139
|
+
wrapLines={true}
|
|
140
|
+
>
|
|
141
|
+
{error}
|
|
142
|
+
</SyntaxHighlighter>
|
|
143
|
+
</div>
|
|
144
|
+
))
|
|
145
|
+
) : (
|
|
146
|
+
<></>
|
|
147
|
+
)}
|
|
148
|
+
</li>
|
|
149
|
+
<li className={'query-info'}>
|
|
150
|
+
<label>Links:</label>{' '}
|
|
151
|
+
{links?.length ? (
|
|
152
|
+
links.map((link, idx) => (
|
|
153
|
+
<p key={idx}>
|
|
154
|
+
<a href={link} target="_blank" rel="noreferrer">
|
|
155
|
+
{link}
|
|
156
|
+
</a>
|
|
157
|
+
</p>
|
|
158
|
+
))
|
|
159
|
+
) : (
|
|
160
|
+
<></>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
<li className={'query-info'}>
|
|
164
|
+
<label>Output Table:</label> {output_table}
|
|
165
|
+
</li>
|
|
166
|
+
<li className={'query-info'}>
|
|
167
|
+
<label>Rows:</label> {numRows}
|
|
168
|
+
</li>
|
|
169
|
+
</ul>
|
|
170
|
+
</div>
|
|
76
171
|
);
|
|
77
172
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import CreatableSelect from 'react-select/creatable';
|
|
5
|
+
|
|
6
|
+
export default function DimensionFilter({ dimension, onChange }) {
|
|
7
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
8
|
+
const [dimensionValues, setDimensionValues] = useState([]);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const fetchData = async () => {
|
|
12
|
+
const dimensionNode = await djClient.node(dimension.metadata.node_name);
|
|
13
|
+
|
|
14
|
+
// Only include the primary keys as filterable dimensions for now, until we figure out how
|
|
15
|
+
// to build a manageable UI experience around all dimensional attributes
|
|
16
|
+
if (dimensionNode && dimensionNode.type === 'dimension') {
|
|
17
|
+
const primaryKey =
|
|
18
|
+
dimensionNode.name +
|
|
19
|
+
'.' +
|
|
20
|
+
dimensionNode.columns
|
|
21
|
+
.filter(col =>
|
|
22
|
+
col.attributes.some(
|
|
23
|
+
attr => attr.attribute_type.name === 'primary_key',
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
.map(col => col.name);
|
|
27
|
+
const label =
|
|
28
|
+
dimensionNode.name +
|
|
29
|
+
'.' +
|
|
30
|
+
dimensionNode.columns
|
|
31
|
+
.filter(col =>
|
|
32
|
+
col.attributes.some(attr => attr.attribute_type.name === 'label'),
|
|
33
|
+
)
|
|
34
|
+
.map(col => col.name);
|
|
35
|
+
|
|
36
|
+
const data = await djClient.nodeData(dimension.metadata.node_name);
|
|
37
|
+
/* istanbul ignore if */
|
|
38
|
+
if (dimensionNode && data.results && data.results.length > 0) {
|
|
39
|
+
const columnNames = data.results[0].columns.map(
|
|
40
|
+
column => column.semantic_entity,
|
|
41
|
+
);
|
|
42
|
+
const dimValues = data.results[0].rows.map(row => {
|
|
43
|
+
const rowData = { value: '', label: '' };
|
|
44
|
+
row.forEach((value, index) => {
|
|
45
|
+
if (columnNames[index] === primaryKey) {
|
|
46
|
+
rowData.value = value;
|
|
47
|
+
if (rowData.label === '') {
|
|
48
|
+
rowData.label = value;
|
|
49
|
+
}
|
|
50
|
+
} else if (columnNames[index] === label) {
|
|
51
|
+
rowData.label = value;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return rowData;
|
|
55
|
+
});
|
|
56
|
+
setDimensionValues(dimValues);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
fetchData().catch(console.error);
|
|
61
|
+
}, [dimension.metadata.node_name, djClient]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div key={dimension.metadata.node_name}>
|
|
65
|
+
{dimension.label.split('[').slice(0)[0]} (
|
|
66
|
+
{
|
|
67
|
+
<a href={`/nodes/${dimension.metadata.node_name}`}>
|
|
68
|
+
{dimension.metadata.node_display_name}
|
|
69
|
+
</a>
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
<CreatableSelect
|
|
73
|
+
name="dimensions"
|
|
74
|
+
options={dimensionValues}
|
|
75
|
+
isMulti
|
|
76
|
+
isClearable
|
|
77
|
+
onChange={event => {
|
|
78
|
+
onChange({
|
|
79
|
+
dimension: dimension.value,
|
|
80
|
+
values: event,
|
|
81
|
+
});
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
|
-
import { Form, Formik } from 'formik';
|
|
4
|
+
import { Field, Form, Formik } from 'formik';
|
|
5
5
|
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
6
6
|
import EditIcon from '../../icons/EditIcon';
|
|
7
7
|
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
8
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
8
9
|
|
|
9
10
|
export default function LinkDimensionPopover({
|
|
10
11
|
column,
|
|
@@ -35,11 +36,17 @@ export default function LinkDimensionPopover({
|
|
|
35
36
|
{ node, column, dimension },
|
|
36
37
|
{ setSubmitting, setStatus },
|
|
37
38
|
) => {
|
|
38
|
-
setSubmitting(false);
|
|
39
39
|
if (referencedDimensionNode && dimension === 'Remove') {
|
|
40
|
-
await unlinkDimension(
|
|
40
|
+
await unlinkDimension(
|
|
41
|
+
node,
|
|
42
|
+
column,
|
|
43
|
+
referencedDimensionNode,
|
|
44
|
+
setStatus,
|
|
45
|
+
).then(_ => setSubmitting(false));
|
|
41
46
|
} else {
|
|
42
|
-
await linkDimension(node, column, dimension, setStatus)
|
|
47
|
+
await linkDimension(node, column, dimension, setStatus).then(_ =>
|
|
48
|
+
setSubmitting(false),
|
|
49
|
+
);
|
|
43
50
|
}
|
|
44
51
|
onSubmit();
|
|
45
52
|
};
|
|
@@ -137,8 +144,9 @@ export default function LinkDimensionPopover({
|
|
|
137
144
|
type="submit"
|
|
138
145
|
aria-label="SaveLinkDimension"
|
|
139
146
|
aria-hidden="false"
|
|
147
|
+
disabled={isSubmitting}
|
|
140
148
|
>
|
|
141
|
-
Save
|
|
149
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
142
150
|
</button>
|
|
143
151
|
</Form>
|
|
144
152
|
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import ClientCodePopover from './ClientCodePopover';
|
|
3
2
|
import * as React from 'react';
|
|
4
3
|
import EditColumnPopover from './EditColumnPopover';
|
|
5
4
|
import LinkDimensionPopover from './LinkDimensionPopover';
|
|
@@ -35,8 +34,11 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
35
34
|
useEffect(() => {
|
|
36
35
|
const fetchData = async () => {
|
|
37
36
|
const dimensions = await djClient.dimensions();
|
|
38
|
-
const options = dimensions.map(
|
|
39
|
-
return {
|
|
37
|
+
const options = dimensions.map(dim => {
|
|
38
|
+
return {
|
|
39
|
+
value: dim.name,
|
|
40
|
+
label: `${dim.name} (${dim.indegree} links)`,
|
|
41
|
+
};
|
|
40
42
|
});
|
|
41
43
|
setDimensions(options);
|
|
42
44
|
};
|
|
@@ -74,6 +74,17 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
74
74
|
</div>
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
if (event.activity_type === 'refresh') {
|
|
78
|
+
return (
|
|
79
|
+
<div className="history-left">
|
|
80
|
+
<b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
|
|
81
|
+
{event.entity_type}{' '}
|
|
82
|
+
<b>
|
|
83
|
+
<a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
|
|
84
|
+
</b>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
77
88
|
if (event.activity_type === 'update' && event.entity_type === 'node') {
|
|
78
89
|
return (
|
|
79
90
|
<div className="history-left">
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import Select from 'react-select';
|
|
3
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
5
|
+
import { labelize } from '../../../utils/form';
|
|
6
|
+
import { Form, Formik } from 'formik';
|
|
7
|
+
import DimensionFilter from './DimensionFilter';
|
|
8
|
+
import QueryInfo from '../../components/QueryInfo';
|
|
9
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
10
|
+
|
|
11
|
+
export default function NodeValidateTab({ node, djClient }) {
|
|
12
|
+
const [query, setQuery] = useState('');
|
|
13
|
+
const [lookup, setLookup] = useState([]);
|
|
14
|
+
const [running, setRunning] = useState(false);
|
|
15
|
+
|
|
16
|
+
// These are a list of dimensions that are available for this node
|
|
17
|
+
const [dimensions, setDimensions] = useState([]);
|
|
18
|
+
|
|
19
|
+
// A separate structure used to store the selected dimensions to filter by and their values
|
|
20
|
+
const [selectedFilters, setSelectedFilters] = useState({});
|
|
21
|
+
|
|
22
|
+
// The set of dimensions and filters to pass to the /sql or /data endpoints for the node
|
|
23
|
+
const [selection, setSelection] = useState({
|
|
24
|
+
dimensions: [],
|
|
25
|
+
filters: [],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Any query result info retrieved when a node query is run
|
|
29
|
+
const [queryInfo, setQueryInfo] = useState(null);
|
|
30
|
+
|
|
31
|
+
const initialValues = {};
|
|
32
|
+
|
|
33
|
+
const [state, setState] = useState({
|
|
34
|
+
selectedTab: 'results',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const switchTab = tabName => {
|
|
38
|
+
setState({ selectedTab: tabName });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const fetchData = async () => {
|
|
43
|
+
if (node) {
|
|
44
|
+
// Find all the dimensions for this node
|
|
45
|
+
const dimensions = await djClient.nodeDimensions(node.name);
|
|
46
|
+
|
|
47
|
+
// Create a dimensions lookup object
|
|
48
|
+
const lookup = {};
|
|
49
|
+
dimensions.forEach(dimension => {
|
|
50
|
+
lookup[dimension.name] = dimension;
|
|
51
|
+
});
|
|
52
|
+
setLookup(lookup);
|
|
53
|
+
|
|
54
|
+
// Group the dimensions by dimension node
|
|
55
|
+
const grouped = Object.entries(
|
|
56
|
+
dimensions.reduce((group, dimension) => {
|
|
57
|
+
group[dimension.node_name + dimension.path] =
|
|
58
|
+
group[dimension.node_name + dimension.path] ?? [];
|
|
59
|
+
group[dimension.node_name + dimension.path].push(dimension);
|
|
60
|
+
return group;
|
|
61
|
+
}, {}),
|
|
62
|
+
);
|
|
63
|
+
setDimensions(grouped);
|
|
64
|
+
|
|
65
|
+
// Build the query for this node based on the user-provided dimensions and filters
|
|
66
|
+
const query = await djClient.sql(node.name, selection);
|
|
67
|
+
setQuery(query.sql);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
fetchData().catch(console.error);
|
|
71
|
+
}, [djClient, node, selection]);
|
|
72
|
+
|
|
73
|
+
const dimensionsList = dimensions.flatMap(grouping => {
|
|
74
|
+
const dimensionsInGroup = grouping[1];
|
|
75
|
+
return dimensionsInGroup
|
|
76
|
+
.filter(dim => dim.is_primary_key === true)
|
|
77
|
+
.map(dim => {
|
|
78
|
+
return {
|
|
79
|
+
value: dim.name,
|
|
80
|
+
label: (
|
|
81
|
+
<span>
|
|
82
|
+
{labelize(dim.name.split('.').slice(-1)[0])}{' '}
|
|
83
|
+
<small>{dim.name}</small>
|
|
84
|
+
</span>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Run the query and use SSE to stream the status of the query execution results
|
|
91
|
+
const runQuery = async (values, setStatus, setSubmitting) => {
|
|
92
|
+
setRunning(true);
|
|
93
|
+
const sse = await djClient.streamNodeData(node?.name, selection);
|
|
94
|
+
sse.onmessage = e => {
|
|
95
|
+
const messageData = JSON.parse(JSON.parse(e.data));
|
|
96
|
+
if (
|
|
97
|
+
messageData !== null &&
|
|
98
|
+
messageData?.state !== 'FINISHED' &&
|
|
99
|
+
messageData?.state !== 'CANCELED' &&
|
|
100
|
+
messageData?.state !== 'FAILED'
|
|
101
|
+
) {
|
|
102
|
+
setRunning(false);
|
|
103
|
+
}
|
|
104
|
+
if (messageData.results && messageData.results?.length > 0) {
|
|
105
|
+
messageData.numRows = messageData.results?.length
|
|
106
|
+
? messageData.results[0].rows.length
|
|
107
|
+
: [];
|
|
108
|
+
switchTab('results');
|
|
109
|
+
setRunning(false);
|
|
110
|
+
} else {
|
|
111
|
+
switchTab('info');
|
|
112
|
+
}
|
|
113
|
+
setQueryInfo(messageData);
|
|
114
|
+
};
|
|
115
|
+
sse.onerror = () => sse.close();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Handle form submission (runs the query)
|
|
119
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
120
|
+
await runQuery(values, setStatus, setSubmitting);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Handle when filter values are updated. This is available for all nodes.
|
|
124
|
+
const handleAddFilters = event => {
|
|
125
|
+
const updatedFilters = selectedFilters;
|
|
126
|
+
if (event.dimension in updatedFilters) {
|
|
127
|
+
updatedFilters[event.dimension].operator = event.operator;
|
|
128
|
+
updatedFilters[event.dimension].values = event.values;
|
|
129
|
+
} else {
|
|
130
|
+
updatedFilters[event.dimension] = {
|
|
131
|
+
operator: event.operator,
|
|
132
|
+
values: event.values,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
setSelectedFilters(updatedFilters);
|
|
136
|
+
const updatedDimensions = selection.dimensions.concat([event.dimension]);
|
|
137
|
+
setSelection({
|
|
138
|
+
filters: Object.entries(updatedFilters).map(obj =>
|
|
139
|
+
obj[1].values
|
|
140
|
+
? `${obj[0]} IN (${obj[1].values
|
|
141
|
+
.map(val =>
|
|
142
|
+
['int', 'bigint', 'float', 'double', 'long'].includes(
|
|
143
|
+
lookup[obj[0]].type,
|
|
144
|
+
)
|
|
145
|
+
? val.value
|
|
146
|
+
: "'" + val.value + "'",
|
|
147
|
+
)
|
|
148
|
+
.join(', ')})`
|
|
149
|
+
: '',
|
|
150
|
+
),
|
|
151
|
+
dimensions: updatedDimensions,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Handle when one or more dimensions are selected from the dropdown
|
|
156
|
+
// Note that this is only available to metrics
|
|
157
|
+
const handleAddDimensions = event => {
|
|
158
|
+
const updatedDimensions = event.map(
|
|
159
|
+
selectedDimension => selectedDimension.value,
|
|
160
|
+
);
|
|
161
|
+
setSelection({
|
|
162
|
+
filters: selection.filters,
|
|
163
|
+
dimensions: updatedDimensions,
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const filters = dimensions.map(grouping => {
|
|
168
|
+
const dimensionsInGroup = grouping[1];
|
|
169
|
+
const dimensionGroupOptions = dimensionsInGroup
|
|
170
|
+
.filter(dim => dim.is_primary_key === true)
|
|
171
|
+
.map(dim => {
|
|
172
|
+
return {
|
|
173
|
+
value: dim.name,
|
|
174
|
+
label: labelize(dim.name.split('.').slice(-1)[0]),
|
|
175
|
+
metadata: dim,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<div className="dimensionsList">
|
|
181
|
+
{dimensionGroupOptions.map(dimension => {
|
|
182
|
+
return (
|
|
183
|
+
<DimensionFilter
|
|
184
|
+
dimension={dimension}
|
|
185
|
+
onChange={handleAddFilters}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
|
196
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
197
|
+
return (
|
|
198
|
+
<Form>
|
|
199
|
+
<div className={'queryrunner'}>
|
|
200
|
+
<div className="queryrunner-filters left">
|
|
201
|
+
{node?.type === 'metric' ? (
|
|
202
|
+
<>
|
|
203
|
+
<span>
|
|
204
|
+
<label>Group By</label>
|
|
205
|
+
</span>
|
|
206
|
+
<Select
|
|
207
|
+
name="dimensions"
|
|
208
|
+
options={dimensionsList}
|
|
209
|
+
isMulti
|
|
210
|
+
isClearable
|
|
211
|
+
onChange={handleAddDimensions}
|
|
212
|
+
/>
|
|
213
|
+
<br />
|
|
214
|
+
</>
|
|
215
|
+
) : null}
|
|
216
|
+
<span>
|
|
217
|
+
<label>Add Filters</label>
|
|
218
|
+
</span>
|
|
219
|
+
{filters}
|
|
220
|
+
</div>
|
|
221
|
+
<div className={'righttop'}>
|
|
222
|
+
<label>Generated Query</label>
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
height: '200px',
|
|
226
|
+
width: '80%',
|
|
227
|
+
overflow: 'scroll',
|
|
228
|
+
borderRadius: '0',
|
|
229
|
+
border: '1px solid #ccc',
|
|
230
|
+
}}
|
|
231
|
+
className="queryrunner-query"
|
|
232
|
+
>
|
|
233
|
+
<SyntaxHighlighter
|
|
234
|
+
language="sql"
|
|
235
|
+
style={foundation}
|
|
236
|
+
wrapLines={true}
|
|
237
|
+
>
|
|
238
|
+
{query}
|
|
239
|
+
</SyntaxHighlighter>
|
|
240
|
+
</div>
|
|
241
|
+
<button
|
|
242
|
+
type="submit"
|
|
243
|
+
disabled={
|
|
244
|
+
running ||
|
|
245
|
+
(queryInfo !== null &&
|
|
246
|
+
queryInfo?.state !== 'FINISHED' &&
|
|
247
|
+
queryInfo?.state !== 'CANCELED' &&
|
|
248
|
+
queryInfo?.state !== 'FAILED')
|
|
249
|
+
}
|
|
250
|
+
className="button-3 execute-button"
|
|
251
|
+
style={{ marginTop: '1rem' }}
|
|
252
|
+
>
|
|
253
|
+
{isSubmitting || running === true ? <LoadingIcon /> : '► Run'}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
width: window.innerWidth * 0.8,
|
|
259
|
+
marginTop: '1rem',
|
|
260
|
+
marginLeft: '0.5rem',
|
|
261
|
+
height: 'calc(70% - 5.5px)',
|
|
262
|
+
}}
|
|
263
|
+
className={'rightbottom'}
|
|
264
|
+
>
|
|
265
|
+
<div
|
|
266
|
+
className={'align-items-center row'}
|
|
267
|
+
style={{
|
|
268
|
+
borderBottom: '1px solid #dddddd',
|
|
269
|
+
width: '86%',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<div
|
|
273
|
+
className={
|
|
274
|
+
'tab-item' +
|
|
275
|
+
(state.selectedTab === 'results' ? ' active' : '')
|
|
276
|
+
}
|
|
277
|
+
onClick={_ => switchTab('results')}
|
|
278
|
+
>
|
|
279
|
+
Results
|
|
280
|
+
</div>
|
|
281
|
+
<div
|
|
282
|
+
className={
|
|
283
|
+
'tab-item' +
|
|
284
|
+
(state.selectedTab === 'info' ? ' active' : '')
|
|
285
|
+
}
|
|
286
|
+
aria-label={'QueryInfo'}
|
|
287
|
+
role={'button'}
|
|
288
|
+
onClick={_ => switchTab('info')}
|
|
289
|
+
>
|
|
290
|
+
Info
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
{state.selectedTab === 'info' ? (
|
|
294
|
+
<div>
|
|
295
|
+
{queryInfo && queryInfo.id ? (
|
|
296
|
+
<QueryInfo {...queryInfo} isList={true} />
|
|
297
|
+
) : (
|
|
298
|
+
<></>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
) : null}
|
|
302
|
+
{state.selectedTab === 'results' ? (
|
|
303
|
+
<div>
|
|
304
|
+
{queryInfo !== null && queryInfo.state !== 'FINISHED' ? (
|
|
305
|
+
<div style={{ padding: '2rem' }}>
|
|
306
|
+
The query has status {queryInfo.state}! Check the INFO
|
|
307
|
+
tab for more details.
|
|
308
|
+
</div>
|
|
309
|
+
) : queryInfo !== null &&
|
|
310
|
+
queryInfo.results !== null &&
|
|
311
|
+
queryInfo.results.length === 0 ? (
|
|
312
|
+
<div style={{ padding: '2rem' }}>
|
|
313
|
+
The query finished but output no results.
|
|
314
|
+
</div>
|
|
315
|
+
) : queryInfo !== null &&
|
|
316
|
+
queryInfo.results !== null &&
|
|
317
|
+
queryInfo.results.length > 0 ? (
|
|
318
|
+
<div
|
|
319
|
+
className="table-responsive"
|
|
320
|
+
style={{
|
|
321
|
+
gridGap: '0',
|
|
322
|
+
width: '86%',
|
|
323
|
+
padding: '0',
|
|
324
|
+
maxHeight: '50%',
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<table
|
|
328
|
+
style={{ marginTop: '0 !important' }}
|
|
329
|
+
className="table"
|
|
330
|
+
>
|
|
331
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
332
|
+
<tr>
|
|
333
|
+
{queryInfo.results[0]?.columns.map(columnName => (
|
|
334
|
+
<th key={columnName.name}>
|
|
335
|
+
{columnName.column}
|
|
336
|
+
</th>
|
|
337
|
+
))}
|
|
338
|
+
</tr>
|
|
339
|
+
</thead>
|
|
340
|
+
<tbody>
|
|
341
|
+
{queryInfo.results[0]?.rows
|
|
342
|
+
.slice(0, 100)
|
|
343
|
+
.map((rowData, index) => (
|
|
344
|
+
<tr key={`data-row:${index}`}>
|
|
345
|
+
{rowData.map(rowValue => (
|
|
346
|
+
<td key={rowValue}>{rowValue}</td>
|
|
347
|
+
))}
|
|
348
|
+
</tr>
|
|
349
|
+
))}
|
|
350
|
+
</tbody>
|
|
351
|
+
</table>
|
|
352
|
+
</div>
|
|
353
|
+
) : (
|
|
354
|
+
<div style={{ padding: '2rem' }}>
|
|
355
|
+
Click "Run" to execute the query.
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
) : null}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</Form>
|
|
363
|
+
);
|
|
364
|
+
}}
|
|
365
|
+
</Formik>
|
|
366
|
+
);
|
|
367
|
+
}
|