datajunction-ui 0.0.1-a45.dev5 → 0.0.1-a46.dev1
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 +1 -1
- package/src/app/pages/AddEditNodePage/QueryTesterSection.jsx +62 -0
- package/src/app/pages/NamespacePage/index.jsx +1 -1
- package/src/app/pages/NodePage/NodeHistory.jsx +1 -1
- package/src/app/pages/NodePage/NodeInfoTab.jsx +33 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +94 -21
- package/src/app/pages/NodePage/PartitionValueForm.jsx +63 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +1 -1
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +1 -0
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +32 -47
- package/src/app/pages/NodePage/index.jsx +1 -0
- package/src/app/pages/QueryRunner.jsx +31 -0
- package/src/app/services/DJService.js +11 -0
- package/src/app/services/__tests__/DJService.test.jsx +16 -0
- package/src/mocks/mockNodes.jsx +1 -0
- package/src/styles/index.css +15 -0
- package/src/styles/node-creation.scss +9 -0
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query tester component
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorMessage, useFormikContext } from 'formik';
|
|
5
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
6
|
+
import DJClientContext from '../../providers/djclient';
|
|
7
|
+
import { FormikSelect } from './FormikSelect';
|
|
8
|
+
import QueryBuilder from 'react-querybuilder';
|
|
9
|
+
|
|
10
|
+
export const QueryTesterSection = ({}) => {
|
|
11
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
12
|
+
|
|
13
|
+
const [fields, setFields] = useState([]);
|
|
14
|
+
const [filters, setFilters] = useState({
|
|
15
|
+
combinator: 'and',
|
|
16
|
+
rules: [],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Used to pull out current form values for node validation
|
|
20
|
+
const { values } = useFormikContext();
|
|
21
|
+
|
|
22
|
+
// Select options, i.e., the available dimensions
|
|
23
|
+
const [selectOptions, setSelectOptions] = useState([]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const fetchData = async () => {
|
|
27
|
+
if (values.query) {
|
|
28
|
+
const data = await djClient.node(values.upstream_node);
|
|
29
|
+
setSelectOptions(
|
|
30
|
+
data.columns.map(col => {
|
|
31
|
+
return {
|
|
32
|
+
value: col.name,
|
|
33
|
+
label: col.name,
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
fetchData().catch(console.error);
|
|
40
|
+
}, [djClient, values.upstream_node]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<h4>Test Query</h4>
|
|
45
|
+
<label>Add Filters</label>
|
|
46
|
+
<QueryBuilder
|
|
47
|
+
fields={fields}
|
|
48
|
+
query={filters}
|
|
49
|
+
onQueryChange={q => setFilters(q)}
|
|
50
|
+
/>
|
|
51
|
+
<span
|
|
52
|
+
className="button-3 execute-button"
|
|
53
|
+
// onClick={getData}
|
|
54
|
+
role="button"
|
|
55
|
+
aria-label="RunQuery"
|
|
56
|
+
aria-hidden="false"
|
|
57
|
+
>
|
|
58
|
+
{'Run Query'}
|
|
59
|
+
</span>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -11,7 +11,7 @@ export default function NodeHistory({ node, djClient }) {
|
|
|
11
11
|
const fetchData = async () => {
|
|
12
12
|
if (node) {
|
|
13
13
|
const data = await djClient.history('node', node.name);
|
|
14
|
-
setHistory(data
|
|
14
|
+
setHistory(data);
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
fetchData().catch(console.error);
|
|
@@ -7,6 +7,9 @@ import ListGroupItem from '../../components/ListGroupItem';
|
|
|
7
7
|
import ToggleSwitch from '../../components/ToggleSwitch';
|
|
8
8
|
import DJClientContext from '../../providers/djclient';
|
|
9
9
|
import { labelize } from '../../../utils/form';
|
|
10
|
+
import { AlertMessage } from '../AddEditNodePage/AlertMessage';
|
|
11
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
12
|
+
import InvalidIcon from '../../icons/InvalidIcon';
|
|
10
13
|
|
|
11
14
|
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
12
15
|
foundation.hljs['padding'] = '2rem';
|
|
@@ -39,6 +42,35 @@ export default function NodeInfoTab({ node }) {
|
|
|
39
42
|
function toggle(value) {
|
|
40
43
|
return !value;
|
|
41
44
|
}
|
|
45
|
+
const metricsWarning =
|
|
46
|
+
node?.type === 'metric' && node?.incompatible_druid_functions.length > 0 ? (
|
|
47
|
+
<div className="message warning" style={{ marginTop: '0.7rem' }}>
|
|
48
|
+
⚠{' '}
|
|
49
|
+
<small>
|
|
50
|
+
The following functions used in the metric definition may not be
|
|
51
|
+
compatible with Druid SQL:{' '}
|
|
52
|
+
{node?.incompatible_druid_functions.map(func => (
|
|
53
|
+
<li style={{ listStyleType: 'none', margin: '0.7rem 0.7rem' }}>
|
|
54
|
+
⇢{' '}
|
|
55
|
+
<span style={{ background: '#fff', padding: '0.3rem' }}>
|
|
56
|
+
{func}
|
|
57
|
+
</span>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
If you need your metrics to be compatible with Druid, please use{' '}
|
|
61
|
+
<a
|
|
62
|
+
href={
|
|
63
|
+
'https://druid.apache.org/docs/latest/querying/sql-functions/'
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
these supported functions
|
|
67
|
+
</a>
|
|
68
|
+
.
|
|
69
|
+
</small>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
''
|
|
73
|
+
);
|
|
42
74
|
const metricQueryDiv = (
|
|
43
75
|
<div className="list-group-item d-flex">
|
|
44
76
|
<div className="gap-2 w-100 justify-content-between py-3">
|
|
@@ -205,6 +237,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
205
237
|
className="list-group align-items-center justify-content-between flex-md-row gap-2"
|
|
206
238
|
style={{ minWidth: '700px' }}
|
|
207
239
|
>
|
|
240
|
+
{node?.type === 'metric' ? metricsWarning : ''}
|
|
208
241
|
<ListGroupItem label="Description" value={node?.description} />
|
|
209
242
|
<div className="list-group-item d-flex">
|
|
210
243
|
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
@@ -1,28 +1,101 @@
|
|
|
1
|
-
import { Component } from 'react';
|
|
1
|
+
import { Component, useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import ValidIcon from '../../icons/ValidIcon';
|
|
3
3
|
import InvalidIcon from '../../icons/InvalidIcon';
|
|
4
|
+
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
6
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
7
|
+
import markdown from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { AlertMessage } from '../AddEditNodePage/AlertMessage';
|
|
10
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
11
|
+
import { labelize } from '../../../utils/form';
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
SyntaxHighlighter.registerLanguage('markdown', markdown);
|
|
14
|
+
|
|
15
|
+
export default function NodeStatus({ node, revalidate = true }) {
|
|
16
|
+
const MAX_ERROR_LENGTH = 200;
|
|
17
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
18
|
+
const [validation, setValidation] = useState([]);
|
|
19
|
+
|
|
20
|
+
const [codeAnchor, setCodeAnchor] = useState(false);
|
|
21
|
+
const ref = useRef(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (revalidate) {
|
|
25
|
+
const fetchData = async () => {
|
|
26
|
+
setValidation(await djClient.revalidate(node.name));
|
|
27
|
+
};
|
|
28
|
+
fetchData().catch(console.error);
|
|
29
|
+
}
|
|
30
|
+
}, [djClient, node, revalidate]);
|
|
31
|
+
|
|
32
|
+
const displayValidation =
|
|
33
|
+
revalidate && validation?.errors?.length > 0 ? (
|
|
9
34
|
<>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
<button
|
|
36
|
+
className="badge"
|
|
37
|
+
style={{
|
|
38
|
+
backgroundColor: '#b34b00',
|
|
39
|
+
fontSize: '15px',
|
|
40
|
+
outline: '0',
|
|
41
|
+
border: '0',
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
}}
|
|
44
|
+
onClick={() => setCodeAnchor(!codeAnchor)}
|
|
45
|
+
>
|
|
46
|
+
⚠ {validation?.errors?.length} error
|
|
47
|
+
{validation?.errors?.length > 1 ? 's' : ''}
|
|
48
|
+
</button>
|
|
49
|
+
<div
|
|
50
|
+
className="popover"
|
|
51
|
+
ref={ref}
|
|
52
|
+
style={{
|
|
53
|
+
display: codeAnchor === false ? 'none' : 'block',
|
|
54
|
+
border: 'none',
|
|
55
|
+
paddingTop: '0px !important',
|
|
56
|
+
marginTop: '0px',
|
|
57
|
+
backgroundColor: 'transparent',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{validation?.errors?.map((error, idx) => (
|
|
61
|
+
<div className="validation_error">
|
|
62
|
+
<b
|
|
63
|
+
style={{
|
|
64
|
+
color: '#b34b00',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{labelize(error.type.toLowerCase())}:
|
|
68
|
+
</b>{' '}
|
|
69
|
+
{error.message.length > MAX_ERROR_LENGTH
|
|
70
|
+
? error.message.slice(0, MAX_ERROR_LENGTH - 1) + '...'
|
|
71
|
+
: error.message}
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
25
75
|
</>
|
|
76
|
+
) : (
|
|
77
|
+
<></>
|
|
26
78
|
);
|
|
27
|
-
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{revalidate && validation?.errors?.length > 0 ? (
|
|
83
|
+
displayValidation
|
|
84
|
+
) : validation?.status === 'valid' || node?.status === 'valid' ? (
|
|
85
|
+
<span
|
|
86
|
+
className="status__valid status"
|
|
87
|
+
style={{ alignContent: 'center' }}
|
|
88
|
+
>
|
|
89
|
+
<ValidIcon />
|
|
90
|
+
</span>
|
|
91
|
+
) : (
|
|
92
|
+
<span
|
|
93
|
+
className="status__invalid status"
|
|
94
|
+
style={{ alignContent: 'center' }}
|
|
95
|
+
>
|
|
96
|
+
<InvalidIcon />
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
28
101
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { Field, Form, Formik } from 'formik';
|
|
5
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
6
|
+
|
|
7
|
+
export default function PartitionValueForm({ col, materialization }) {
|
|
8
|
+
if (col.partition.type_ === 'temporal') {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div
|
|
12
|
+
className="partition__full"
|
|
13
|
+
key={col.name}
|
|
14
|
+
style={{ width: '50%' }}
|
|
15
|
+
>
|
|
16
|
+
<div className="partition__header">{col.display_name}</div>
|
|
17
|
+
<div className="partition__body">
|
|
18
|
+
<span style={{ padding: '0.5rem' }}>From</span>{' '}
|
|
19
|
+
<Field
|
|
20
|
+
type="text"
|
|
21
|
+
name={`partitionValues.['${col.name}'].from`}
|
|
22
|
+
id={`${col.name}.from`}
|
|
23
|
+
placeholder="20230101"
|
|
24
|
+
default="20230101"
|
|
25
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
26
|
+
/>{' '}
|
|
27
|
+
<span style={{ padding: '0.5rem' }}>To</span>
|
|
28
|
+
<Field
|
|
29
|
+
type="text"
|
|
30
|
+
name={`partitionValues.['${col.name}'].to`}
|
|
31
|
+
id={`${col.name}.to`}
|
|
32
|
+
placeholder="20230102"
|
|
33
|
+
default="20230102"
|
|
34
|
+
style={{ width: '7rem' }}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<div
|
|
44
|
+
className="partition__full"
|
|
45
|
+
key={col.name}
|
|
46
|
+
style={{ width: '50%' }}
|
|
47
|
+
>
|
|
48
|
+
<div className="partition__header">{col.display_name}</div>
|
|
49
|
+
<div className="partition__body">
|
|
50
|
+
<Field
|
|
51
|
+
type="text"
|
|
52
|
+
name={`partitionValues.['${col.name}']`}
|
|
53
|
+
id={col.name}
|
|
54
|
+
placeholder=""
|
|
55
|
+
default=""
|
|
56
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -149,7 +149,7 @@ export default function RevisionDiff() {
|
|
|
149
149
|
</div>
|
|
150
150
|
{Object.keys(diffObjects).map(field => {
|
|
151
151
|
return (
|
|
152
|
-
<div className="diff">
|
|
152
|
+
<div className="diff" aria-label={'DiffView'} role={'gridcell'}>
|
|
153
153
|
<h4>
|
|
154
154
|
{labelize(field)}{' '}
|
|
155
155
|
<small className="no-change-banner">
|
|
@@ -88,6 +88,7 @@ describe('<NodePage />', () => {
|
|
|
88
88
|
created_at: '2023-08-21T16:48:56.932162+00:00',
|
|
89
89
|
tags: [{ name: 'purpose', display_name: 'Purpose' }],
|
|
90
90
|
primary_key: [],
|
|
91
|
+
incompatible_druid_functions: ['IF'],
|
|
91
92
|
createNodeClientCode:
|
|
92
93
|
'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)',
|
|
93
94
|
dimensions: [
|
|
@@ -14,11 +14,11 @@ describe('<RevisionDiff />', () => {
|
|
|
14
14
|
|
|
15
15
|
const mockNodesWithDimension = [
|
|
16
16
|
{
|
|
17
|
-
node_revision_id:
|
|
18
|
-
node_id:
|
|
19
|
-
type: '
|
|
20
|
-
name: 'default.
|
|
21
|
-
display_name: '
|
|
17
|
+
node_revision_id: 1,
|
|
18
|
+
node_id: 1,
|
|
19
|
+
type: 'dimension',
|
|
20
|
+
name: 'default.repair_order',
|
|
21
|
+
display_name: 'Repair Orders',
|
|
22
22
|
version: 'v1.0',
|
|
23
23
|
status: 'valid',
|
|
24
24
|
mode: 'published',
|
|
@@ -30,10 +30,10 @@ describe('<RevisionDiff />', () => {
|
|
|
30
30
|
extra_params: {},
|
|
31
31
|
name: 'warehouse',
|
|
32
32
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
description: 'Repair order dimension',
|
|
34
|
+
query:
|
|
35
|
+
'SELECT repair_order_id, municipality_id, hard_hat_id, order_date, ' +
|
|
36
|
+
'dispatcher_id FROM default.repair_orders',
|
|
37
37
|
availability: null,
|
|
38
38
|
columns: [
|
|
39
39
|
{
|
|
@@ -45,26 +45,26 @@ describe('<RevisionDiff />', () => {
|
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
|
-
name: '
|
|
48
|
+
name: 'municipality_id',
|
|
49
49
|
type: 'int',
|
|
50
50
|
attributes: [],
|
|
51
51
|
dimension: null,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
name: '
|
|
55
|
-
type: '
|
|
54
|
+
name: 'hard_hat_id',
|
|
55
|
+
type: 'int',
|
|
56
56
|
attributes: [],
|
|
57
57
|
dimension: null,
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
name: '
|
|
61
|
-
type: '
|
|
60
|
+
name: 'order_date',
|
|
61
|
+
type: 'date',
|
|
62
62
|
attributes: [],
|
|
63
63
|
dimension: null,
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
name: '
|
|
67
|
-
type: '
|
|
66
|
+
name: 'dispatcher_id',
|
|
67
|
+
type: 'int',
|
|
68
68
|
attributes: [],
|
|
69
69
|
dimension: null,
|
|
70
70
|
},
|
|
@@ -74,12 +74,12 @@ describe('<RevisionDiff />', () => {
|
|
|
74
74
|
parents: [],
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
|
-
node_revision_id:
|
|
78
|
-
node_id:
|
|
79
|
-
type: '
|
|
80
|
-
name: 'default.
|
|
81
|
-
display_name: '
|
|
82
|
-
version: '
|
|
77
|
+
node_revision_id: 2,
|
|
78
|
+
node_id: 2,
|
|
79
|
+
type: 'dimension',
|
|
80
|
+
name: 'default.repair_order',
|
|
81
|
+
display_name: 'Repair Orders',
|
|
82
|
+
version: 'v2.0',
|
|
83
83
|
status: 'valid',
|
|
84
84
|
mode: 'published',
|
|
85
85
|
catalog: {
|
|
@@ -90,10 +90,10 @@ describe('<RevisionDiff />', () => {
|
|
|
90
90
|
extra_params: {},
|
|
91
91
|
name: 'warehouse',
|
|
92
92
|
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
description: 'Repair order dimension',
|
|
94
|
+
query:
|
|
95
|
+
'SELECT repair_order_id, municipality_id, hard_hat_id, ' +
|
|
96
|
+
'dispatcher_id FROM default.repair_orders',
|
|
97
97
|
availability: null,
|
|
98
98
|
columns: [
|
|
99
99
|
{
|
|
@@ -106,7 +106,7 @@ describe('<RevisionDiff />', () => {
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
name: 'municipality_id',
|
|
109
|
-
type: '
|
|
109
|
+
type: 'int',
|
|
110
110
|
attributes: [],
|
|
111
111
|
dimension: null,
|
|
112
112
|
},
|
|
@@ -116,24 +116,6 @@ describe('<RevisionDiff />', () => {
|
|
|
116
116
|
attributes: [],
|
|
117
117
|
dimension: null,
|
|
118
118
|
},
|
|
119
|
-
{
|
|
120
|
-
name: 'order_date',
|
|
121
|
-
type: 'date',
|
|
122
|
-
attributes: [],
|
|
123
|
-
dimension: null,
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: 'required_date',
|
|
127
|
-
type: 'date',
|
|
128
|
-
attributes: [],
|
|
129
|
-
dimension: null,
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
name: 'dispatched_date',
|
|
133
|
-
type: 'date',
|
|
134
|
-
attributes: [],
|
|
135
|
-
dimension: null,
|
|
136
|
-
},
|
|
137
119
|
{
|
|
138
120
|
name: 'dispatcher_id',
|
|
139
121
|
type: 'int',
|
|
@@ -141,7 +123,7 @@ describe('<RevisionDiff />', () => {
|
|
|
141
123
|
dimension: null,
|
|
142
124
|
},
|
|
143
125
|
],
|
|
144
|
-
updated_at: '2023-08-21T16:48:52.
|
|
126
|
+
updated_at: '2023-08-21T16:48:52.981201+00:00',
|
|
145
127
|
materializations: [],
|
|
146
128
|
parents: [],
|
|
147
129
|
},
|
|
@@ -163,7 +145,7 @@ describe('<RevisionDiff />', () => {
|
|
|
163
145
|
);
|
|
164
146
|
const { container } = render(
|
|
165
147
|
<MemoryRouter
|
|
166
|
-
initialEntries={['/nodes/default.repair_orders_cube/revisions/
|
|
148
|
+
initialEntries={['/nodes/default.repair_orders_cube/revisions/v2.0']}
|
|
167
149
|
>
|
|
168
150
|
<Routes>
|
|
169
151
|
<Route path="nodes/:name/revisions/:revision" element={element} />
|
|
@@ -174,6 +156,9 @@ describe('<RevisionDiff />', () => {
|
|
|
174
156
|
expect(mockDjClient.DataJunctionAPI.revisions).toHaveBeenCalledWith(
|
|
175
157
|
'default.repair_orders_cube',
|
|
176
158
|
);
|
|
159
|
+
|
|
160
|
+
const diffViews = screen.getAllByRole('gridcell', 'DiffView');
|
|
161
|
+
diffViews.map(diffView => expect(diffView).toBeInTheDocument());
|
|
177
162
|
});
|
|
178
163
|
});
|
|
179
164
|
});
|
|
@@ -58,6 +58,7 @@ export function NodePage() {
|
|
|
58
58
|
data.required_dimensions = metric.required_dimensions;
|
|
59
59
|
data.upstream_node = metric.upstream_node;
|
|
60
60
|
data.expression = metric.expression;
|
|
61
|
+
data.incompatible_druid_functions = metric.incompatible_druid_functions;
|
|
61
62
|
}
|
|
62
63
|
if (data.type === 'cube') {
|
|
63
64
|
const cube = await djClient.cube(name);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import QueryBuilder from 'react-querybuilder';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export function QueryRunner() {
|
|
5
|
+
const [fields, setFields] = useState([]);
|
|
6
|
+
const [filters, setFilters] = useState({
|
|
7
|
+
combinator: 'and',
|
|
8
|
+
rules: [],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<h4>Test Query</h4>
|
|
14
|
+
<label>Add Filters</label>
|
|
15
|
+
<QueryBuilder
|
|
16
|
+
fields={fields}
|
|
17
|
+
query={filters}
|
|
18
|
+
onQueryChange={q => setFilters(q)}
|
|
19
|
+
/>
|
|
20
|
+
<span
|
|
21
|
+
className="button-3 execute-button"
|
|
22
|
+
// onClick={getData}
|
|
23
|
+
role="button"
|
|
24
|
+
aria-label="RunQuery"
|
|
25
|
+
aria-hidden="false"
|
|
26
|
+
>
|
|
27
|
+
{'Run Query'}
|
|
28
|
+
</span>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -844,4 +844,15 @@ export const DataJunctionAPI = {
|
|
|
844
844
|
})
|
|
845
845
|
).json();
|
|
846
846
|
},
|
|
847
|
+
revalidate: async function (node) {
|
|
848
|
+
return await (
|
|
849
|
+
await fetch(`${DJ_URL}/nodes/${node}/validate`, {
|
|
850
|
+
method: 'POST',
|
|
851
|
+
headers: {
|
|
852
|
+
'Content-Type': 'application/json',
|
|
853
|
+
},
|
|
854
|
+
credentials: 'include',
|
|
855
|
+
})
|
|
856
|
+
).json();
|
|
857
|
+
},
|
|
847
858
|
};
|
|
@@ -34,6 +34,22 @@ describe('DataJunctionAPI', () => {
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it('calls catalogs correctly', async () => {
|
|
38
|
+
fetch.mockResponseOnce(JSON.stringify({}));
|
|
39
|
+
await DataJunctionAPI.catalogs();
|
|
40
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/catalogs`, {
|
|
41
|
+
credentials: 'include',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('calls engines correctly', async () => {
|
|
46
|
+
fetch.mockResponseOnce(JSON.stringify({}));
|
|
47
|
+
await DataJunctionAPI.engines();
|
|
48
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/engines`, {
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
37
53
|
it('calls node correctly', async () => {
|
|
38
54
|
fetch.mockResponseOnce(JSON.stringify(mocks.mockMetricNode));
|
|
39
55
|
const nodeData = await DataJunctionAPI.node(mocks.mockMetricNode.name);
|
package/src/mocks/mockNodes.jsx
CHANGED
|
@@ -103,6 +103,7 @@ export const mocks = {
|
|
|
103
103
|
created_at: '2023-08-21T16:48:56.841631+00:00',
|
|
104
104
|
tags: [{ name: 'purpose', display_name: 'Purpose' }],
|
|
105
105
|
dimension_links: [],
|
|
106
|
+
incompatible_druid_functions: ['IF'],
|
|
106
107
|
dimensions: [
|
|
107
108
|
{
|
|
108
109
|
value: 'default.date_dim.dateint',
|
package/src/styles/index.css
CHANGED
|
@@ -1202,3 +1202,18 @@ pre {
|
|
|
1202
1202
|
text-transform: uppercase;
|
|
1203
1203
|
padding-left: 10px;
|
|
1204
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
.validation_error {
|
|
1207
|
+
border: #b34b0025 1px solid;
|
|
1208
|
+
border-left: #b34b00 5px solid;
|
|
1209
|
+
padding-left: 20px;
|
|
1210
|
+
padding-top: 5px;
|
|
1211
|
+
padding-bottom: 5px;
|
|
1212
|
+
font-size: small;
|
|
1213
|
+
width: 600px;
|
|
1214
|
+
word-wrap: break-word;
|
|
1215
|
+
margin-top: 3px;
|
|
1216
|
+
background-color: #ffffff;
|
|
1217
|
+
margin-bottom: 3px;
|
|
1218
|
+
margin-left: -20px;
|
|
1219
|
+
}
|
|
@@ -254,6 +254,15 @@ form {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
.warning {
|
|
258
|
+
background-color: rgb(253, 248, 242);
|
|
259
|
+
color: rgb(190, 105, 37);
|
|
260
|
+
svg {
|
|
261
|
+
filter: invert(16%) sepia(68%) saturate(2827%) hue-rotate(344deg)
|
|
262
|
+
brightness(96%) contrast(100%);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
.SourceCreationInput {
|
|
258
267
|
margin: 0.5rem 0;
|
|
259
268
|
display: inline-grid;
|