datajunction-ui 0.0.1-a31 → 0.0.1-a32
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 -1
- package/src/app/components/Search.jsx +96 -0
- package/src/app/components/__tests__/Search.test.jsx +63 -0
- package/src/app/components/search.css +17 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +9 -4
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +13 -8
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +7 -0
- package/src/app/pages/AddEditNodePage/index.jsx +77 -6
- package/src/app/pages/NodePage/NodeInfoTab.jsx +38 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +1 -1
- package/src/app/pages/NodePage/index.jsx +1 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +2 -0
- package/src/app/pages/Root/index.tsx +2 -0
- package/src/app/services/DJService.js +40 -1
- package/src/app/services/__tests__/DJService.test.jsx +7 -0
- package/src/mocks/mockNodes.jsx +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-a32",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"file-loader": "6.2.0",
|
|
55
55
|
"fontfaceobserver": "2.3.0",
|
|
56
56
|
"formik": "2.4.3",
|
|
57
|
+
"fuse.js": "6.6.2",
|
|
57
58
|
"husky": "8.0.1",
|
|
58
59
|
"i18next": "21.9.2",
|
|
59
60
|
"i18next-browser-languagedetector": "6.1.5",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import DJClientContext from '../providers/djclient';
|
|
3
|
+
import Fuse from 'fuse.js';
|
|
4
|
+
|
|
5
|
+
import './search.css';
|
|
6
|
+
|
|
7
|
+
export default function Search() {
|
|
8
|
+
const [fuse, setFuse] = useState();
|
|
9
|
+
const [searchValue, setSearchValue] = useState('');
|
|
10
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
11
|
+
|
|
12
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
13
|
+
|
|
14
|
+
const truncate = str => {
|
|
15
|
+
return str.length > 100 ? str.substring(0, 90) + '...' : str;
|
|
16
|
+
};
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchNodes = async () => {
|
|
19
|
+
const data = (await djClient.nodeDetails()) || [];
|
|
20
|
+
const tags = (await djClient.listTags()) || [];
|
|
21
|
+
const allEntities = data.concat(
|
|
22
|
+
tags.map(tag => {
|
|
23
|
+
tag.type = 'tag';
|
|
24
|
+
return tag;
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
const fuse = new Fuse(allEntities || [], {
|
|
28
|
+
keys: [
|
|
29
|
+
'name', // will be assigned a `weight` of 1
|
|
30
|
+
{
|
|
31
|
+
name: 'description',
|
|
32
|
+
weight: 2,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'display_name',
|
|
36
|
+
weight: 3,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'type',
|
|
40
|
+
weight: 4,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'tag_type',
|
|
44
|
+
weight: 5,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
setFuse(fuse);
|
|
49
|
+
};
|
|
50
|
+
fetchNodes();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleChange = e => {
|
|
54
|
+
setSearchValue(e.target.value);
|
|
55
|
+
if (fuse) {
|
|
56
|
+
setSearchResults(fuse.search(e.target.value).map(result => result.item));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<form
|
|
63
|
+
className="search-box"
|
|
64
|
+
onSubmit={e => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<input
|
|
69
|
+
type="text"
|
|
70
|
+
placeholder="Search"
|
|
71
|
+
name="search"
|
|
72
|
+
value={searchValue}
|
|
73
|
+
onChange={handleChange}
|
|
74
|
+
/>
|
|
75
|
+
</form>
|
|
76
|
+
<div className="search-results">
|
|
77
|
+
{searchResults.map(item => {
|
|
78
|
+
const itemUrl =
|
|
79
|
+
item.type !== 'tag' ? `/nodes/${item.name}` : `/tags/${item.name}`;
|
|
80
|
+
return (
|
|
81
|
+
<a href={itemUrl}>
|
|
82
|
+
<div key={item.name} className="search-result-item">
|
|
83
|
+
<span className={`node_type__${item.type} badge node_type`}>
|
|
84
|
+
{item.type}
|
|
85
|
+
</span>
|
|
86
|
+
{item.display_name} (<b>{item.name}</b>){' '}
|
|
87
|
+
{item.description ? '- ' : ' '}
|
|
88
|
+
{truncate(item.description)}
|
|
89
|
+
</div>
|
|
90
|
+
</a>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import Search from '../Search';
|
|
4
|
+
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import { Root } from '../../pages/Root';
|
|
6
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
7
|
+
|
|
8
|
+
describe('<Search />', () => {
|
|
9
|
+
const mockDjClient = {
|
|
10
|
+
logout: jest.fn(),
|
|
11
|
+
nodeDetails: async () => [
|
|
12
|
+
{
|
|
13
|
+
name: 'default.repair_orders',
|
|
14
|
+
display_name: 'Default: Repair Orders',
|
|
15
|
+
description: 'Repair orders',
|
|
16
|
+
version: 'v1.0',
|
|
17
|
+
type: 'source',
|
|
18
|
+
status: 'valid',
|
|
19
|
+
mode: 'published',
|
|
20
|
+
updated_at: '2023-08-21T16:48:52.880498+00:00',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'default.repair_order_details',
|
|
24
|
+
display_name: 'Default: Repair Order Details',
|
|
25
|
+
description: 'Details on repair orders',
|
|
26
|
+
version: 'v1.0',
|
|
27
|
+
type: 'source',
|
|
28
|
+
status: 'valid',
|
|
29
|
+
mode: 'published',
|
|
30
|
+
updated_at: '2023-08-21T16:48:52.981201+00:00',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
listTags: async () => [
|
|
34
|
+
{
|
|
35
|
+
description: 'something',
|
|
36
|
+
display_name: 'Report A',
|
|
37
|
+
tag_metadata: {},
|
|
38
|
+
name: 'report.a',
|
|
39
|
+
tag_type: 'report',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: 'report B',
|
|
43
|
+
display_name: 'Report B',
|
|
44
|
+
tag_metadata: {},
|
|
45
|
+
name: 'report.b',
|
|
46
|
+
tag_type: 'report',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
it('displays search results correctly', () => {
|
|
52
|
+
render(
|
|
53
|
+
<HelmetProvider>
|
|
54
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
55
|
+
<Root />
|
|
56
|
+
</DJClientContext.Provider>
|
|
57
|
+
</HelmetProvider>,
|
|
58
|
+
);
|
|
59
|
+
const searchInput = screen.queryByPlaceholderText('Search');
|
|
60
|
+
fireEvent.change(searchInput, { target: { value: 'Repair' } });
|
|
61
|
+
expect(searchInput.value).toBe('Repair');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.search-box {
|
|
2
|
+
display: flex;
|
|
3
|
+
height: 50%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.search-results {
|
|
7
|
+
position: absolute;
|
|
8
|
+
z-index: 1000;
|
|
9
|
+
width: 75%;
|
|
10
|
+
background-color: rgba(244, 244, 244, 0.8);
|
|
11
|
+
border-radius: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.search-result-item {
|
|
15
|
+
text-decoration: wavy;
|
|
16
|
+
padding: 0.5rem;
|
|
17
|
+
}
|
|
@@ -32,10 +32,13 @@ describe('AddEditNodePage submission failed', () => {
|
|
|
32
32
|
const { container } = renderCreateNode(element);
|
|
33
33
|
|
|
34
34
|
await userEvent.type(
|
|
35
|
-
screen.getByLabelText('Display Name'),
|
|
35
|
+
screen.getByLabelText('Display Name *'),
|
|
36
36
|
'Some Test Metric',
|
|
37
37
|
);
|
|
38
|
-
await userEvent.type(
|
|
38
|
+
await userEvent.type(
|
|
39
|
+
screen.getByLabelText('Query *'),
|
|
40
|
+
'SELECT * FROM test',
|
|
41
|
+
);
|
|
39
42
|
await userEvent.click(screen.getByText('Create dimension'));
|
|
40
43
|
|
|
41
44
|
await waitFor(() => {
|
|
@@ -49,6 +52,8 @@ describe('AddEditNodePage submission failed', () => {
|
|
|
49
52
|
'draft',
|
|
50
53
|
'default',
|
|
51
54
|
null,
|
|
55
|
+
undefined,
|
|
56
|
+
undefined,
|
|
52
57
|
);
|
|
53
58
|
expect(
|
|
54
59
|
screen.getByText(/Some columns in the primary key \[] were not found/),
|
|
@@ -80,7 +85,7 @@ describe('AddEditNodePage submission failed', () => {
|
|
|
80
85
|
const element = testElement(mockDjClient);
|
|
81
86
|
renderEditNode(element);
|
|
82
87
|
|
|
83
|
-
await userEvent.type(screen.getByLabelText('Display Name'), '!!!');
|
|
88
|
+
await userEvent.type(screen.getByLabelText('Display Name *'), '!!!');
|
|
84
89
|
await userEvent.type(screen.getByLabelText('Description'), '!!!');
|
|
85
90
|
await userEvent.click(screen.getByText('Save'));
|
|
86
91
|
await waitFor(async () => {
|
|
@@ -88,7 +93,7 @@ describe('AddEditNodePage submission failed', () => {
|
|
|
88
93
|
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled();
|
|
89
94
|
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
|
|
90
95
|
'default.num_repair_orders',
|
|
91
|
-
[
|
|
96
|
+
['purpose'],
|
|
92
97
|
);
|
|
93
98
|
expect(mockDjClient.DataJunctionAPI.tagsNode).toReturnWith({
|
|
94
99
|
json: { message: 'Some tags were not found' },
|
|
@@ -42,10 +42,13 @@ describe('AddEditNodePage submission succeeded', () => {
|
|
|
42
42
|
const { container } = renderCreateNode(element);
|
|
43
43
|
|
|
44
44
|
await userEvent.type(
|
|
45
|
-
screen.getByLabelText('Display Name'),
|
|
45
|
+
screen.getByLabelText('Display Name *'),
|
|
46
46
|
'Some Test Metric',
|
|
47
47
|
);
|
|
48
|
-
await userEvent.type(
|
|
48
|
+
await userEvent.type(
|
|
49
|
+
screen.getByLabelText('Query *'),
|
|
50
|
+
'SELECT * FROM test',
|
|
51
|
+
);
|
|
49
52
|
await userEvent.click(screen.getByText('Create dimension'));
|
|
50
53
|
|
|
51
54
|
await waitFor(() => {
|
|
@@ -59,10 +62,7 @@ describe('AddEditNodePage submission succeeded', () => {
|
|
|
59
62
|
'draft',
|
|
60
63
|
'default',
|
|
61
64
|
null,
|
|
62
|
-
|
|
63
|
-
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled();
|
|
64
|
-
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
|
|
65
|
-
'default.some_test_metric',
|
|
65
|
+
undefined,
|
|
66
66
|
undefined,
|
|
67
67
|
);
|
|
68
68
|
expect(screen.getByText(/default.some_test_metric/)).toBeInTheDocument();
|
|
@@ -97,7 +97,7 @@ describe('AddEditNodePage submission succeeded', () => {
|
|
|
97
97
|
const element = testElement(mockDjClient);
|
|
98
98
|
const { getByTestId } = renderEditNode(element);
|
|
99
99
|
|
|
100
|
-
await userEvent.type(screen.getByLabelText('Display Name'), '!!!');
|
|
100
|
+
await userEvent.type(screen.getByLabelText('Display Name *'), '!!!');
|
|
101
101
|
await userEvent.type(screen.getByLabelText('Description'), '!!!');
|
|
102
102
|
await userEvent.click(screen.getByText('Save'));
|
|
103
103
|
|
|
@@ -114,13 +114,18 @@ describe('AddEditNodePage submission succeeded', () => {
|
|
|
114
114
|
'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
|
|
115
115
|
'published',
|
|
116
116
|
['repair_order_id', 'country'],
|
|
117
|
+
'neutral',
|
|
118
|
+
'unitless',
|
|
117
119
|
);
|
|
118
120
|
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
|
|
119
121
|
expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
|
|
120
122
|
'default.num_repair_orders',
|
|
121
|
-
[
|
|
123
|
+
['purpose'],
|
|
122
124
|
);
|
|
123
125
|
|
|
126
|
+
expect(mockDjClient.DataJunctionAPI.listMetricMetadata).toBeCalledTimes(
|
|
127
|
+
1,
|
|
128
|
+
);
|
|
124
129
|
expect(
|
|
125
130
|
await screen.getByDisplayValue('repair_order_id, country'),
|
|
126
131
|
).toBeInTheDocument();
|
|
@@ -53,6 +53,13 @@ export const initializeMockDJClient = () => {
|
|
|
53
53
|
node: jest.fn(),
|
|
54
54
|
tagsNode: jest.fn(),
|
|
55
55
|
listTags: jest.fn(),
|
|
56
|
+
listMetricMetadata: jest.fn().mockReturnValue({
|
|
57
|
+
directions: ['higher_is_better', 'lower_is_better', 'neutral'],
|
|
58
|
+
units: [
|
|
59
|
+
{ name: 'dollar', label: 'Dollar' },
|
|
60
|
+
{ name: 'second', label: 'Second' },
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
56
63
|
},
|
|
57
64
|
};
|
|
58
65
|
};
|
|
@@ -33,6 +33,8 @@ export function AddEditNodePage() {
|
|
|
33
33
|
|
|
34
34
|
const [namespaces, setNamespaces] = useState([]);
|
|
35
35
|
const [tags, setTags] = useState([]);
|
|
36
|
+
const [metricUnits, setMetricUnits] = useState([]);
|
|
37
|
+
const [metricDirections, setMetricDirections] = useState([]);
|
|
36
38
|
|
|
37
39
|
const initialValues = {
|
|
38
40
|
name: action === Action.Edit ? name : '',
|
|
@@ -108,9 +110,13 @@ export function AddEditNodePage() {
|
|
|
108
110
|
values.mode,
|
|
109
111
|
values.namespace,
|
|
110
112
|
values.primary_key ? primaryKeyToList(values.primary_key) : null,
|
|
113
|
+
values.metric_direction,
|
|
114
|
+
values.metric_unit,
|
|
111
115
|
);
|
|
112
116
|
if (status === 200 || status === 201) {
|
|
113
|
-
|
|
117
|
+
if (values.tags) {
|
|
118
|
+
await djClient.tagsNode(values.name, values.tags);
|
|
119
|
+
}
|
|
114
120
|
setStatus({
|
|
115
121
|
success: (
|
|
116
122
|
<>
|
|
@@ -134,8 +140,13 @@ export function AddEditNodePage() {
|
|
|
134
140
|
values.query,
|
|
135
141
|
values.mode,
|
|
136
142
|
values.primary_key ? primaryKeyToList(values.primary_key) : null,
|
|
143
|
+
values.metric_direction,
|
|
144
|
+
values.metric_unit,
|
|
145
|
+
);
|
|
146
|
+
const tagsResponse = await djClient.tagsNode(
|
|
147
|
+
values.name,
|
|
148
|
+
values.tags.map(tag => tag.name),
|
|
137
149
|
);
|
|
138
|
-
const tagsResponse = await djClient.tagsNode(values.name, values.tags);
|
|
139
150
|
if ((status === 200 || status === 201) && tagsResponse.status === 200) {
|
|
140
151
|
setStatus({
|
|
141
152
|
success: (
|
|
@@ -155,7 +166,7 @@ export function AddEditNodePage() {
|
|
|
155
166
|
const namespaceInput = (
|
|
156
167
|
<div className="NamespaceInput">
|
|
157
168
|
<ErrorMessage name="namespace" component="span" />
|
|
158
|
-
<label htmlFor="react-select-3-input">Namespace
|
|
169
|
+
<label htmlFor="react-select-3-input">Namespace *</label>
|
|
159
170
|
<FormikSelect
|
|
160
171
|
selectOptions={namespaces}
|
|
161
172
|
formikFieldName="namespace"
|
|
@@ -171,7 +182,7 @@ export function AddEditNodePage() {
|
|
|
171
182
|
const fullNameInput = (
|
|
172
183
|
<div className="FullNameInput NodeCreationInput">
|
|
173
184
|
<ErrorMessage name="name" component="span" />
|
|
174
|
-
<label htmlFor="FullName">Full Name
|
|
185
|
+
<label htmlFor="FullName">Full Name *</label>
|
|
175
186
|
<FullNameField type="text" name="name" />
|
|
176
187
|
</div>
|
|
177
188
|
);
|
|
@@ -201,6 +212,15 @@ export function AddEditNodePage() {
|
|
|
201
212
|
setFieldValue(field, data[field] || '', false);
|
|
202
213
|
}
|
|
203
214
|
});
|
|
215
|
+
if (data.metric_metadata?.direction) {
|
|
216
|
+
setFieldValue('metric_direction', data.metric_metadata.direction);
|
|
217
|
+
}
|
|
218
|
+
if (data.metric_metadata?.unit) {
|
|
219
|
+
setFieldValue(
|
|
220
|
+
'metric_unit',
|
|
221
|
+
data.metric_metadata.unit.name.toLowerCase(),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
204
224
|
};
|
|
205
225
|
|
|
206
226
|
const alertMessage = message => {
|
|
@@ -242,6 +262,16 @@ export function AddEditNodePage() {
|
|
|
242
262
|
fetchData().catch(console.error);
|
|
243
263
|
}, [djClient, djClient.listTags]);
|
|
244
264
|
|
|
265
|
+
// Get metric metadata values
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
const fetchData = async () => {
|
|
268
|
+
const metadata = await djClient.listMetricMetadata();
|
|
269
|
+
setMetricDirections(metadata.directions);
|
|
270
|
+
setMetricUnits(metadata.units);
|
|
271
|
+
};
|
|
272
|
+
fetchData().catch(console.error);
|
|
273
|
+
}, [djClient]);
|
|
274
|
+
|
|
245
275
|
return (
|
|
246
276
|
<div className="mid">
|
|
247
277
|
<NamespaceHeader namespace="" />
|
|
@@ -281,6 +311,43 @@ export function AddEditNodePage() {
|
|
|
281
311
|
</div>
|
|
282
312
|
);
|
|
283
313
|
|
|
314
|
+
const metricMetadataInput = (
|
|
315
|
+
<>
|
|
316
|
+
<div
|
|
317
|
+
className="MetricDirectionInput NodeCreationInput"
|
|
318
|
+
style={{ width: '25%' }}
|
|
319
|
+
>
|
|
320
|
+
<ErrorMessage name="metric_direction" component="span" />
|
|
321
|
+
<label htmlFor="MetricDirection">Metric Direction</label>
|
|
322
|
+
<Field
|
|
323
|
+
as="select"
|
|
324
|
+
name="metric_direction"
|
|
325
|
+
id="MetricDirection"
|
|
326
|
+
>
|
|
327
|
+
<option value=""></option>
|
|
328
|
+
{metricDirections.map(direction => (
|
|
329
|
+
<option value={direction}>
|
|
330
|
+
{labelize(direction)}
|
|
331
|
+
</option>
|
|
332
|
+
))}
|
|
333
|
+
</Field>
|
|
334
|
+
</div>
|
|
335
|
+
<div
|
|
336
|
+
className="MetricUnitInput NodeCreationInput"
|
|
337
|
+
style={{ width: '25%' }}
|
|
338
|
+
>
|
|
339
|
+
<ErrorMessage name="metric_unit" component="span" />
|
|
340
|
+
<label htmlFor="MetricUnit">Metric Unit</label>
|
|
341
|
+
<Field as="select" name="metric_unit" id="MetricUnit">
|
|
342
|
+
<option value=""></option>
|
|
343
|
+
{metricUnits.map(unit => (
|
|
344
|
+
<option value={unit.name}>{unit.label}</option>
|
|
345
|
+
))}
|
|
346
|
+
</Field>
|
|
347
|
+
</div>
|
|
348
|
+
</>
|
|
349
|
+
);
|
|
350
|
+
|
|
284
351
|
useEffect(() => {
|
|
285
352
|
const fetchData = async () => {
|
|
286
353
|
if (action === Action.Edit) {
|
|
@@ -332,7 +399,7 @@ export function AddEditNodePage() {
|
|
|
332
399
|
: staticFieldsInEdit(node)}
|
|
333
400
|
<div className="DisplayNameInput NodeCreationInput">
|
|
334
401
|
<ErrorMessage name="display_name" component="span" />
|
|
335
|
-
<label htmlFor="displayName">Display Name
|
|
402
|
+
<label htmlFor="displayName">Display Name *</label>
|
|
336
403
|
<Field
|
|
337
404
|
type="text"
|
|
338
405
|
name="display_name"
|
|
@@ -352,9 +419,12 @@ export function AddEditNodePage() {
|
|
|
352
419
|
placeholder="Describe your node"
|
|
353
420
|
/>
|
|
354
421
|
</div>
|
|
422
|
+
{nodeType === 'metric' || node.type === 'metric'
|
|
423
|
+
? metricMetadataInput
|
|
424
|
+
: ''}
|
|
355
425
|
<div className="QueryInput NodeCreationInput">
|
|
356
426
|
<ErrorMessage name="query" component="span" />
|
|
357
|
-
<label htmlFor="Query">Query
|
|
427
|
+
<label htmlFor="Query">Query *</label>
|
|
358
428
|
<NodeQueryField
|
|
359
429
|
djClient={djClient}
|
|
360
430
|
value={node.query ? node.query : ''}
|
|
@@ -379,6 +449,7 @@ export function AddEditNodePage() {
|
|
|
379
449
|
<option value="published">Published</option>
|
|
380
450
|
</Field>
|
|
381
451
|
</div>
|
|
452
|
+
|
|
382
453
|
<button type="submit" disabled={isSubmitting}>
|
|
383
454
|
{action === Action.Add ? 'Create' : 'Save'} {nodeType}
|
|
384
455
|
</button>
|
|
@@ -6,6 +6,7 @@ import NodeStatus from './NodeStatus';
|
|
|
6
6
|
import ListGroupItem from '../../components/ListGroupItem';
|
|
7
7
|
import ToggleSwitch from '../../components/ToggleSwitch';
|
|
8
8
|
import DJClientContext from '../../providers/djclient';
|
|
9
|
+
import { labelize } from '../../../utils/form';
|
|
9
10
|
|
|
10
11
|
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
11
12
|
foundation.hljs['padding'] = '2rem';
|
|
@@ -88,6 +89,42 @@ export default function NodeInfoTab({ node }) {
|
|
|
88
89
|
);
|
|
89
90
|
};
|
|
90
91
|
|
|
92
|
+
const metricMetadataDiv =
|
|
93
|
+
node?.type === 'metric' ? (
|
|
94
|
+
<div className="list-group-item d-flex">
|
|
95
|
+
<div className="d-flex gap-2 w-100 py-3">
|
|
96
|
+
<div>
|
|
97
|
+
<h6 className="mb-0 w-100">Direction</h6>
|
|
98
|
+
<p
|
|
99
|
+
className="mb-0 opacity-75"
|
|
100
|
+
role="dialog"
|
|
101
|
+
aria-hidden="false"
|
|
102
|
+
aria-label="MetricDirection"
|
|
103
|
+
>
|
|
104
|
+
{node?.metric_metadata?.direction
|
|
105
|
+
? labelize(node?.metric_metadata?.direction)
|
|
106
|
+
: 'None'}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div style={{ marginRight: '2rem' }}>
|
|
110
|
+
<h6 className="mb-0 w-100">Unit</h6>
|
|
111
|
+
<p
|
|
112
|
+
className="mb-0 opacity-75"
|
|
113
|
+
role="dialog"
|
|
114
|
+
aria-hidden="false"
|
|
115
|
+
aria-label="MetricUnit"
|
|
116
|
+
>
|
|
117
|
+
{node?.metric_metadata?.unit
|
|
118
|
+
? labelize(node?.metric_metadata?.unit?.label)
|
|
119
|
+
: 'None'}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
''
|
|
126
|
+
);
|
|
127
|
+
|
|
91
128
|
const cubeElementsDiv = node?.cube_elements ? (
|
|
92
129
|
<div className="list-group-item d-flex">
|
|
93
130
|
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
@@ -205,6 +242,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
205
242
|
</div>
|
|
206
243
|
</div>
|
|
207
244
|
</div>
|
|
245
|
+
{metricMetadataDiv}
|
|
208
246
|
{node?.type !== 'cube' ? queryDiv : ''}
|
|
209
247
|
{cubeElementsDiv}
|
|
210
248
|
</div>
|
|
@@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom';
|
|
|
3
3
|
import DJLogo from '../../icons/DJLogo';
|
|
4
4
|
import { Helmet } from 'react-helmet-async';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
|
+
import Search from '../../components/Search';
|
|
6
7
|
|
|
7
8
|
export function Root() {
|
|
8
9
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -28,6 +29,7 @@ export function Root() {
|
|
|
28
29
|
Data<b>Junction</b>
|
|
29
30
|
</h2>
|
|
30
31
|
</div>
|
|
32
|
+
<Search />
|
|
31
33
|
<div className="menu">
|
|
32
34
|
<div className="menu-item here menu-here-bg menu-lg-down-accordion me-0 me-lg-2 fw-semibold">
|
|
33
35
|
<span className="menu-link">
|
|
@@ -52,8 +52,17 @@ export const DataJunctionAPI = {
|
|
|
52
52
|
},
|
|
53
53
|
|
|
54
54
|
nodes: async function (prefix) {
|
|
55
|
+
const queryParams = prefix ? `?prefix=${prefix}` : '';
|
|
55
56
|
return await (
|
|
56
|
-
await fetch(`${DJ_URL}/nodes
|
|
57
|
+
await fetch(`${DJ_URL}/nodes/${queryParams}`, {
|
|
58
|
+
credentials: 'include',
|
|
59
|
+
})
|
|
60
|
+
).json();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
nodeDetails: async () => {
|
|
64
|
+
return await (
|
|
65
|
+
await fetch(`${DJ_URL}/nodes/details/`, {
|
|
57
66
|
credentials: 'include',
|
|
58
67
|
})
|
|
59
68
|
).json();
|
|
@@ -68,7 +77,16 @@ export const DataJunctionAPI = {
|
|
|
68
77
|
mode,
|
|
69
78
|
namespace,
|
|
70
79
|
primary_key,
|
|
80
|
+
metric_direction,
|
|
81
|
+
metric_unit,
|
|
71
82
|
) {
|
|
83
|
+
const metricMetadata =
|
|
84
|
+
metric_direction || metric_unit
|
|
85
|
+
? {
|
|
86
|
+
direction: metric_direction,
|
|
87
|
+
unit: metric_unit,
|
|
88
|
+
}
|
|
89
|
+
: null;
|
|
72
90
|
const response = await fetch(`${DJ_URL}/nodes/${nodeType}`, {
|
|
73
91
|
method: 'POST',
|
|
74
92
|
headers: {
|
|
@@ -82,6 +100,7 @@ export const DataJunctionAPI = {
|
|
|
82
100
|
mode: mode,
|
|
83
101
|
namespace: namespace,
|
|
84
102
|
primary_key: primary_key,
|
|
103
|
+
metric_metadata: metricMetadata,
|
|
85
104
|
}),
|
|
86
105
|
credentials: 'include',
|
|
87
106
|
});
|
|
@@ -95,8 +114,17 @@ export const DataJunctionAPI = {
|
|
|
95
114
|
query,
|
|
96
115
|
mode,
|
|
97
116
|
primary_key,
|
|
117
|
+
metric_direction,
|
|
118
|
+
metric_unit,
|
|
98
119
|
) {
|
|
99
120
|
try {
|
|
121
|
+
const metricMetadata =
|
|
122
|
+
metric_direction || metric_unit
|
|
123
|
+
? {
|
|
124
|
+
direction: metric_direction,
|
|
125
|
+
unit: metric_unit,
|
|
126
|
+
}
|
|
127
|
+
: null;
|
|
100
128
|
const response = await fetch(`${DJ_URL}/nodes/${name}`, {
|
|
101
129
|
method: 'PATCH',
|
|
102
130
|
headers: {
|
|
@@ -108,6 +136,7 @@ export const DataJunctionAPI = {
|
|
|
108
136
|
query: query,
|
|
109
137
|
mode: mode,
|
|
110
138
|
primary_key: primary_key,
|
|
139
|
+
metric_metadata: metricMetadata,
|
|
111
140
|
}),
|
|
112
141
|
credentials: 'include',
|
|
113
142
|
});
|
|
@@ -662,4 +691,14 @@ export const DataJunctionAPI = {
|
|
|
662
691
|
);
|
|
663
692
|
return { status: response.status, json: await response.json() };
|
|
664
693
|
},
|
|
694
|
+
listMetricMetadata: async function () {
|
|
695
|
+
const response = await fetch(`${DJ_URL}/metrics/metadata`, {
|
|
696
|
+
method: 'GET',
|
|
697
|
+
headers: {
|
|
698
|
+
'Content-Type': 'application/json',
|
|
699
|
+
},
|
|
700
|
+
credentials: 'include',
|
|
701
|
+
});
|
|
702
|
+
return await response.json();
|
|
703
|
+
},
|
|
665
704
|
};
|
|
@@ -91,6 +91,7 @@ describe('DataJunctionAPI', () => {
|
|
|
91
91
|
mode: sampleArgs[5],
|
|
92
92
|
namespace: sampleArgs[6],
|
|
93
93
|
primary_key: sampleArgs[7],
|
|
94
|
+
metric_metadata: null,
|
|
94
95
|
}),
|
|
95
96
|
credentials: 'include',
|
|
96
97
|
});
|
|
@@ -104,6 +105,8 @@ describe('DataJunctionAPI', () => {
|
|
|
104
105
|
'query',
|
|
105
106
|
'mode',
|
|
106
107
|
'primary_key',
|
|
108
|
+
'neutral',
|
|
109
|
+
'',
|
|
107
110
|
];
|
|
108
111
|
fetch.mockResponseOnce(JSON.stringify({}));
|
|
109
112
|
await DataJunctionAPI.patchNode(...sampleArgs);
|
|
@@ -118,6 +121,10 @@ describe('DataJunctionAPI', () => {
|
|
|
118
121
|
query: sampleArgs[3],
|
|
119
122
|
mode: sampleArgs[4],
|
|
120
123
|
primary_key: sampleArgs[5],
|
|
124
|
+
metric_metadata: {
|
|
125
|
+
direction: 'neutral',
|
|
126
|
+
unit: '',
|
|
127
|
+
},
|
|
121
128
|
}),
|
|
122
129
|
credentials: 'include',
|
|
123
130
|
});
|
package/src/mocks/mockNodes.jsx
CHANGED
|
@@ -209,6 +209,13 @@ export const mocks = {
|
|
|
209
209
|
label: 'default.us_state.state_region_description (string)',
|
|
210
210
|
},
|
|
211
211
|
],
|
|
212
|
+
metric_metadata: {
|
|
213
|
+
unit: {
|
|
214
|
+
name: 'unitless',
|
|
215
|
+
label: 'Unitless',
|
|
216
|
+
},
|
|
217
|
+
direction: 'neutral',
|
|
218
|
+
},
|
|
212
219
|
},
|
|
213
220
|
attributes: [
|
|
214
221
|
{
|
|
@@ -1013,6 +1020,13 @@ export const mocks = {
|
|
|
1013
1020
|
],
|
|
1014
1021
|
},
|
|
1015
1022
|
],
|
|
1023
|
+
metric_metadata: {
|
|
1024
|
+
unit: {
|
|
1025
|
+
name: 'unitless',
|
|
1026
|
+
label: 'Unitless',
|
|
1027
|
+
},
|
|
1028
|
+
direction: 'neutral',
|
|
1029
|
+
},
|
|
1016
1030
|
},
|
|
1017
1031
|
mockNodeDAG: [
|
|
1018
1032
|
{
|