datajunction-ui 0.0.1-rc.18 → 0.0.1-rc.19
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/dj-logo.svg +10 -0
- package/package.json +19 -3
- package/src/app/icons/AlertIcon.jsx +32 -0
- package/src/app/icons/DJLogo.jsx +36 -0
- package/src/app/icons/DeleteIcon.jsx +21 -0
- package/src/app/icons/EditIcon.jsx +18 -0
- package/src/app/index.tsx +18 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +33 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +36 -0
- package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
- package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +44 -0
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +29 -0
- package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +84 -0
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +353 -0
- package/src/app/pages/AddEditNodePage/index.jsx +349 -0
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +51 -0
- package/src/app/pages/NamespacePage/index.jsx +36 -0
- package/src/app/pages/NodePage/NodeHistory.jsx +0 -1
- package/src/app/pages/NodePage/index.jsx +43 -26
- package/src/app/pages/Root/index.tsx +3 -3
- package/src/app/services/DJService.js +72 -1
- package/src/setupTests.ts +1 -1
- package/src/styles/index.css +82 -5
- package/src/styles/node-creation.scss +190 -0
- package/src/styles/styles.scss.d.ts +9 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
3
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import fetchMock from 'jest-fetch-mock';
|
|
5
|
+
import { AddEditNodePage } from '../index.jsx';
|
|
6
|
+
import DJClientContext from '../../../providers/djclient';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
8
|
+
|
|
9
|
+
fetchMock.enableMocks();
|
|
10
|
+
|
|
11
|
+
describe('AddEditNodePage', () => {
|
|
12
|
+
const initializeMockDJClient = () => {
|
|
13
|
+
return {
|
|
14
|
+
DataJunctionAPI: {
|
|
15
|
+
namespace: _ => {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
name: 'default.contractors',
|
|
19
|
+
display_name: 'Default: Contractors',
|
|
20
|
+
version: 'v1.0',
|
|
21
|
+
type: 'source',
|
|
22
|
+
status: 'valid',
|
|
23
|
+
mode: 'published',
|
|
24
|
+
updated_at: '2023-08-21T16:48:53.246914+00:00',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'default.num_repair_orders',
|
|
28
|
+
display_name: 'Default: Num Repair Orders',
|
|
29
|
+
version: 'v1.0',
|
|
30
|
+
type: 'metric',
|
|
31
|
+
status: 'valid',
|
|
32
|
+
mode: 'published',
|
|
33
|
+
updated_at: '2023-08-21T16:48:56.841704+00:00',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
},
|
|
37
|
+
metrics: {},
|
|
38
|
+
namespaces: () => {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
namespace: 'default',
|
|
42
|
+
num_nodes: 33,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
},
|
|
46
|
+
createNode: jest.fn(),
|
|
47
|
+
patchNode: jest.fn(),
|
|
48
|
+
node: jest.fn(),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const testElement = djClient => {
|
|
54
|
+
return (
|
|
55
|
+
<DJClientContext.Provider value={djClient}>
|
|
56
|
+
<AddEditNodePage />
|
|
57
|
+
</DJClientContext.Provider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mockMetricNode = {
|
|
62
|
+
namespace: 'default',
|
|
63
|
+
node_revision_id: 23,
|
|
64
|
+
node_id: 23,
|
|
65
|
+
type: 'metric',
|
|
66
|
+
name: 'default.num_repair_orders',
|
|
67
|
+
display_name: 'Default: Num Repair Orders',
|
|
68
|
+
version: 'v1.0',
|
|
69
|
+
status: 'valid',
|
|
70
|
+
mode: 'published',
|
|
71
|
+
catalog: {
|
|
72
|
+
id: 1,
|
|
73
|
+
uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
|
|
74
|
+
created_at: '2023-08-21T16:48:51.146121+00:00',
|
|
75
|
+
updated_at: '2023-08-21T16:48:51.146122+00:00',
|
|
76
|
+
extra_params: {},
|
|
77
|
+
name: 'warehouse',
|
|
78
|
+
},
|
|
79
|
+
schema_: null,
|
|
80
|
+
table: null,
|
|
81
|
+
description: 'Number of repair orders',
|
|
82
|
+
query:
|
|
83
|
+
'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
|
|
84
|
+
availability: null,
|
|
85
|
+
columns: [
|
|
86
|
+
{
|
|
87
|
+
name: 'default_DOT_num_repair_orders',
|
|
88
|
+
type: 'bigint',
|
|
89
|
+
attributes: [],
|
|
90
|
+
dimension: null,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
updated_at: '2023-08-21T16:48:56.841704+00:00',
|
|
94
|
+
materializations: [],
|
|
95
|
+
parents: [
|
|
96
|
+
{
|
|
97
|
+
name: 'default.repair_orders',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
created_at: '2023-08-21T16:48:56.841631+00:00',
|
|
101
|
+
tags: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
fetchMock.resetMocks();
|
|
106
|
+
jest.clearAllMocks();
|
|
107
|
+
window.scrollTo = jest.fn();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const renderCreateNode = element => {
|
|
111
|
+
return render(
|
|
112
|
+
<MemoryRouter initialEntries={['/create/dimension/default']}>
|
|
113
|
+
<Routes>
|
|
114
|
+
<Route path="create/:nodeType/:initialNamespace" element={element} />
|
|
115
|
+
</Routes>
|
|
116
|
+
</MemoryRouter>,
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const renderEditNode = element => {
|
|
121
|
+
return render(
|
|
122
|
+
<MemoryRouter initialEntries={['/nodes/default.num_repair_orders/edit']}>
|
|
123
|
+
<Routes>
|
|
124
|
+
<Route path="nodes/:name/edit" element={element} />
|
|
125
|
+
</Routes>
|
|
126
|
+
</MemoryRouter>,
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
it('Create node page renders with the selected nodeType and namespace', () => {
|
|
131
|
+
const mockDjClient = initializeMockDJClient();
|
|
132
|
+
const element = testElement(mockDjClient);
|
|
133
|
+
const { container } = renderCreateNode(element);
|
|
134
|
+
|
|
135
|
+
// The node type should be included in the page title
|
|
136
|
+
expect(
|
|
137
|
+
container.getElementsByClassName('node_type__metric'),
|
|
138
|
+
).toMatchSnapshot();
|
|
139
|
+
|
|
140
|
+
// The namespace should be set to the one provided in params
|
|
141
|
+
expect(screen.getByText('default')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('Edit node page renders with the selected node', async () => {
|
|
145
|
+
const mockDjClient = initializeMockDJClient();
|
|
146
|
+
mockDjClient.DataJunctionAPI.node.mockReturnValue(mockMetricNode);
|
|
147
|
+
|
|
148
|
+
const element = testElement(mockDjClient);
|
|
149
|
+
renderEditNode(element);
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
// Should be an edit node page
|
|
153
|
+
expect(screen.getByText('Edit')).toBeInTheDocument();
|
|
154
|
+
|
|
155
|
+
// The node name should be loaded onto the page
|
|
156
|
+
expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
|
|
157
|
+
|
|
158
|
+
// The node type should be loaded onto the page
|
|
159
|
+
expect(screen.getByText('metric')).toBeInTheDocument();
|
|
160
|
+
|
|
161
|
+
// The description should be populated
|
|
162
|
+
expect(screen.getByText('Number of repair orders')).toBeInTheDocument();
|
|
163
|
+
|
|
164
|
+
// The query should be populated
|
|
165
|
+
expect(
|
|
166
|
+
screen.getByText(
|
|
167
|
+
'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
|
|
168
|
+
),
|
|
169
|
+
).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('Verify create node page user interaction and successful form submission', async () => {
|
|
174
|
+
const mockDjClient = initializeMockDJClient();
|
|
175
|
+
mockDjClient.DataJunctionAPI.createNode.mockReturnValue({
|
|
176
|
+
status: 200,
|
|
177
|
+
json: { name: 'default.special_forces_contractors', type: 'dimension' },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const element = testElement(mockDjClient);
|
|
181
|
+
const { container } = renderCreateNode(element);
|
|
182
|
+
|
|
183
|
+
const query =
|
|
184
|
+
'select \n' +
|
|
185
|
+
' C.contractor_id,\n' +
|
|
186
|
+
' C.address, C.contact_title, C.contact_name, C.city from default.contractors C \n' +
|
|
187
|
+
"where C.contact_title = 'special forces agent'";
|
|
188
|
+
|
|
189
|
+
// Fill in display name
|
|
190
|
+
await userEvent.type(
|
|
191
|
+
screen.getByLabelText('Display Name'),
|
|
192
|
+
'Special Forces Contractors',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// After typing in a display name, the full name should be updated based on the display name
|
|
196
|
+
expect(
|
|
197
|
+
screen.getByDisplayValue('default.special_forces_contractors'),
|
|
198
|
+
).toBeInTheDocument();
|
|
199
|
+
|
|
200
|
+
// Fill in the rest of the fields and submit
|
|
201
|
+
await userEvent.type(screen.getByLabelText('Query'), query);
|
|
202
|
+
await userEvent.type(
|
|
203
|
+
screen.getByLabelText('Description'),
|
|
204
|
+
'A curated list of special forces contractors',
|
|
205
|
+
);
|
|
206
|
+
await userEvent.type(screen.getByLabelText('Primary Key'), 'contractor_id');
|
|
207
|
+
await userEvent.click(screen.getByText('Create dimension'));
|
|
208
|
+
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledTimes(1);
|
|
211
|
+
expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledWith(
|
|
212
|
+
'dimension',
|
|
213
|
+
'default.special_forces_contractors',
|
|
214
|
+
'Special Forces Contractors',
|
|
215
|
+
'A curated list of special forces contractors',
|
|
216
|
+
query,
|
|
217
|
+
'draft',
|
|
218
|
+
'default',
|
|
219
|
+
['contractor_id'],
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// After successful creation, it should return a success message
|
|
224
|
+
expect(container.getElementsByClassName('success')).toMatchSnapshot();
|
|
225
|
+
}, 60000);
|
|
226
|
+
|
|
227
|
+
it('Verify create node page form failed submission', async () => {
|
|
228
|
+
const mockDjClient = initializeMockDJClient();
|
|
229
|
+
mockDjClient.DataJunctionAPI.createNode.mockReturnValue({
|
|
230
|
+
status: 500,
|
|
231
|
+
json: { message: 'Some columns in the primary key [] were not found' },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const element = testElement(mockDjClient);
|
|
235
|
+
const { container } = renderCreateNode(element);
|
|
236
|
+
|
|
237
|
+
await userEvent.type(
|
|
238
|
+
screen.getByLabelText('Display Name'),
|
|
239
|
+
'Some Test Metric',
|
|
240
|
+
);
|
|
241
|
+
await userEvent.type(screen.getByLabelText('Query'), 'SELECT * FROM test');
|
|
242
|
+
await userEvent.click(screen.getByText('Create dimension'));
|
|
243
|
+
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledTimes(1);
|
|
246
|
+
expect(mockDjClient.DataJunctionAPI.createNode).toBeCalledWith(
|
|
247
|
+
'dimension',
|
|
248
|
+
'default.some_test_metric',
|
|
249
|
+
'Some Test Metric',
|
|
250
|
+
'',
|
|
251
|
+
'SELECT * FROM test',
|
|
252
|
+
'draft',
|
|
253
|
+
'default',
|
|
254
|
+
null,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// After failed creation, it should return a failure message
|
|
259
|
+
expect(container.getElementsByClassName('alert')).toMatchSnapshot();
|
|
260
|
+
}, 60000);
|
|
261
|
+
|
|
262
|
+
it('Verify edit node page form submission success', async () => {
|
|
263
|
+
const mockDjClient = initializeMockDJClient();
|
|
264
|
+
|
|
265
|
+
mockDjClient.DataJunctionAPI.node.mockReturnValue(mockMetricNode);
|
|
266
|
+
mockDjClient.DataJunctionAPI.patchNode = jest.fn();
|
|
267
|
+
mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({
|
|
268
|
+
status: 201,
|
|
269
|
+
json: { name: 'default.num_repair_orders', type: 'metric' },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const element = testElement(mockDjClient);
|
|
273
|
+
renderEditNode(element);
|
|
274
|
+
|
|
275
|
+
await userEvent.type(await screen.getByLabelText('Display Name'), '!!!');
|
|
276
|
+
await userEvent.type(await screen.getByLabelText('Description'), '!!!');
|
|
277
|
+
await userEvent.click(await screen.getByText('Save'));
|
|
278
|
+
|
|
279
|
+
await waitFor(async () => {
|
|
280
|
+
expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1);
|
|
281
|
+
expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledWith(
|
|
282
|
+
'default.num_repair_orders',
|
|
283
|
+
'Default: Num Repair Orders!!!',
|
|
284
|
+
'Number of repair orders!!!',
|
|
285
|
+
'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
|
|
286
|
+
'published',
|
|
287
|
+
null,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
}, 60000);
|
|
291
|
+
|
|
292
|
+
it('Verify edit node page form submission failure displays alert', async () => {
|
|
293
|
+
const mockDjClient = initializeMockDJClient();
|
|
294
|
+
mockDjClient.DataJunctionAPI.node.mockReturnValue(mockMetricNode);
|
|
295
|
+
mockDjClient.DataJunctionAPI.patchNode.mockReturnValue({
|
|
296
|
+
status: 500,
|
|
297
|
+
json: { message: 'Update failed' },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const element = testElement(mockDjClient);
|
|
301
|
+
renderEditNode(element);
|
|
302
|
+
|
|
303
|
+
await userEvent.type(await screen.getByLabelText('Display Name'), '!!!');
|
|
304
|
+
await userEvent.type(await screen.getByLabelText('Description'), '!!!');
|
|
305
|
+
await userEvent.click(await screen.getByText('Save'));
|
|
306
|
+
|
|
307
|
+
await waitFor(async () => {
|
|
308
|
+
expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1);
|
|
309
|
+
expect(await screen.getByText('Update failed')).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('Verify edit page node not found', async () => {
|
|
314
|
+
const mockDjClient = initializeMockDJClient();
|
|
315
|
+
mockDjClient.DataJunctionAPI.node = jest.fn();
|
|
316
|
+
mockDjClient.DataJunctionAPI.node.mockReturnValue({
|
|
317
|
+
message: 'A node with name `default.num_repair_orders` does not exist.',
|
|
318
|
+
errors: [],
|
|
319
|
+
warnings: [],
|
|
320
|
+
});
|
|
321
|
+
const element = testElement(mockDjClient);
|
|
322
|
+
renderEditNode(element);
|
|
323
|
+
|
|
324
|
+
await waitFor(() => {
|
|
325
|
+
expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
|
|
326
|
+
expect(
|
|
327
|
+
screen.getByText('Node default.num_repair_orders does not exist!'),
|
|
328
|
+
).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
}, 60000);
|
|
331
|
+
|
|
332
|
+
it('Verify only transforms, metrics, and dimensions can be edited', async () => {
|
|
333
|
+
const mockDjClient = initializeMockDJClient();
|
|
334
|
+
mockDjClient.DataJunctionAPI.node = jest.fn();
|
|
335
|
+
mockDjClient.DataJunctionAPI.node.mockReturnValue({
|
|
336
|
+
namespace: 'default',
|
|
337
|
+
type: 'source',
|
|
338
|
+
name: 'default.repair_orders',
|
|
339
|
+
display_name: 'Default: Repair Orders',
|
|
340
|
+
});
|
|
341
|
+
const element = testElement(mockDjClient);
|
|
342
|
+
renderEditNode(element);
|
|
343
|
+
|
|
344
|
+
await waitFor(() => {
|
|
345
|
+
expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
|
|
346
|
+
expect(
|
|
347
|
+
screen.getByText(
|
|
348
|
+
'Node default.num_repair_orders is of type source and cannot be edited',
|
|
349
|
+
),
|
|
350
|
+
).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
}, 60000);
|
|
353
|
+
});
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
|
|
3
|
+
* node types is largely the same, with minor differences handled server-side. For the `query`
|
|
4
|
+
* field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
|
|
5
|
+
*/
|
|
6
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
7
|
+
|
|
8
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
9
|
+
import { useContext, useEffect, useState } from 'react';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import 'styles/node-creation.scss';
|
|
12
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
13
|
+
import ValidIcon from '../../icons/ValidIcon';
|
|
14
|
+
import { useParams } from 'react-router-dom';
|
|
15
|
+
import { FullNameField } from './FullNameField';
|
|
16
|
+
import { FormikSelect } from './FormikSelect';
|
|
17
|
+
import { NodeQueryField } from './NodeQueryField';
|
|
18
|
+
|
|
19
|
+
class Action {
|
|
20
|
+
static Add = new Action('add');
|
|
21
|
+
static Edit = new Action('edit');
|
|
22
|
+
|
|
23
|
+
constructor(name) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AddEditNodePage() {
|
|
29
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
30
|
+
|
|
31
|
+
let { nodeType, initialNamespace, name } = useParams();
|
|
32
|
+
const action = name !== undefined ? Action.Edit : Action.Add;
|
|
33
|
+
|
|
34
|
+
const [namespaces, setNamespaces] = useState([]);
|
|
35
|
+
|
|
36
|
+
const initialValues = {
|
|
37
|
+
name: action === Action.Edit ? name : '',
|
|
38
|
+
namespace: action === Action.Add ? initialNamespace : '',
|
|
39
|
+
display_name: '',
|
|
40
|
+
query: '',
|
|
41
|
+
node_type: '',
|
|
42
|
+
description: '',
|
|
43
|
+
primary_key: '',
|
|
44
|
+
mode: 'draft',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const validator = values => {
|
|
48
|
+
const errors = {};
|
|
49
|
+
if (!values.name) {
|
|
50
|
+
errors.name = 'Required';
|
|
51
|
+
}
|
|
52
|
+
if (!values.query) {
|
|
53
|
+
errors.query = 'Required';
|
|
54
|
+
}
|
|
55
|
+
return errors;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
59
|
+
if (action === Action.Add) {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
createNode(values, setStatus);
|
|
62
|
+
setSubmitting(false);
|
|
63
|
+
}, 400);
|
|
64
|
+
} else {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
patchNode(values, setStatus);
|
|
67
|
+
setSubmitting(false);
|
|
68
|
+
}, 400);
|
|
69
|
+
}
|
|
70
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const displayMessageAfterSubmit = status => {
|
|
74
|
+
return status?.success !== undefined ? (
|
|
75
|
+
<div className="message success">
|
|
76
|
+
<ValidIcon />
|
|
77
|
+
{status?.success}
|
|
78
|
+
</div>
|
|
79
|
+
) : status?.failure !== undefined ? (
|
|
80
|
+
<div className="message alert">
|
|
81
|
+
<AlertIcon />
|
|
82
|
+
{status?.failure}
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
''
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const pageTitle =
|
|
90
|
+
action === Action.Add ? (
|
|
91
|
+
<h2>
|
|
92
|
+
Create{' '}
|
|
93
|
+
<span className={`node_type__${nodeType} node_type_creation_heading`}>
|
|
94
|
+
{nodeType}
|
|
95
|
+
</span>
|
|
96
|
+
</h2>
|
|
97
|
+
) : (
|
|
98
|
+
<h2>Edit</h2>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const staticFieldsInEdit = node => (
|
|
102
|
+
<>
|
|
103
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
104
|
+
<label htmlFor="name">Name</label> {name}
|
|
105
|
+
</div>
|
|
106
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
107
|
+
<label htmlFor="name">Type</label> {node.type}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const createNode = async (values, setStatus) => {
|
|
113
|
+
const { status, json } = await djClient.createNode(
|
|
114
|
+
nodeType,
|
|
115
|
+
values.name,
|
|
116
|
+
values.display_name,
|
|
117
|
+
values.description,
|
|
118
|
+
values.query,
|
|
119
|
+
values.mode,
|
|
120
|
+
values.namespace,
|
|
121
|
+
values.primary_key ? values.primary_key.split(',') : null,
|
|
122
|
+
);
|
|
123
|
+
if (status === 200 || status === 201) {
|
|
124
|
+
setStatus({
|
|
125
|
+
success: (
|
|
126
|
+
<>
|
|
127
|
+
Successfully created {json.type} node{' '}
|
|
128
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
129
|
+
</>
|
|
130
|
+
),
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
setStatus({
|
|
134
|
+
failure: `${json.message}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const patchNode = async (values, setStatus) => {
|
|
140
|
+
const { status, json } = await djClient.patchNode(
|
|
141
|
+
values.name,
|
|
142
|
+
values.display_name,
|
|
143
|
+
values.description,
|
|
144
|
+
values.query,
|
|
145
|
+
values.mode,
|
|
146
|
+
values.primary_key ? values.primary_key.split(',') : null,
|
|
147
|
+
);
|
|
148
|
+
if (status === 200 || status === 201) {
|
|
149
|
+
setStatus({
|
|
150
|
+
success: (
|
|
151
|
+
<>
|
|
152
|
+
Successfully updated {json.type} node{' '}
|
|
153
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
setStatus({
|
|
159
|
+
failure: `${json.message}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const namespaceInput = (
|
|
165
|
+
<div className="NamespaceInput">
|
|
166
|
+
<ErrorMessage name="namespace" component="span" />
|
|
167
|
+
<label htmlFor="react-select-3-input">Namespace</label>
|
|
168
|
+
<FormikSelect
|
|
169
|
+
selectOptions={namespaces}
|
|
170
|
+
formikFieldName="namespace"
|
|
171
|
+
placeholder="Choose Namespace"
|
|
172
|
+
defaultValue={{
|
|
173
|
+
value: initialNamespace,
|
|
174
|
+
label: initialNamespace,
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const fullNameInput = (
|
|
181
|
+
<div className="FullNameInput NodeCreationInput">
|
|
182
|
+
<ErrorMessage name="name" component="span" />
|
|
183
|
+
<label htmlFor="FullName">Full Name</label>
|
|
184
|
+
<FullNameField type="text" name="name" />
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const nodeCanBeEdited = nodeType => {
|
|
189
|
+
return new Set(['transform', 'metric', 'dimension']).has(nodeType);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const updateFieldsWithNodeData = (data, setFieldValue) => {
|
|
193
|
+
const fields = [
|
|
194
|
+
'display_name',
|
|
195
|
+
'query',
|
|
196
|
+
'type',
|
|
197
|
+
'description',
|
|
198
|
+
'primary_key',
|
|
199
|
+
'mode',
|
|
200
|
+
];
|
|
201
|
+
fields.forEach(field => {
|
|
202
|
+
if (field === 'primary_key' && data[field] !== undefined) {
|
|
203
|
+
data[field] = data[field].join(',');
|
|
204
|
+
}
|
|
205
|
+
setFieldValue(field, data[field], false);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const alertMessage = message => {
|
|
210
|
+
return (
|
|
211
|
+
<div className="message alert">
|
|
212
|
+
<AlertIcon />
|
|
213
|
+
{message}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Get namespaces, only necessary when creating a node
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (action === Action.Add) {
|
|
221
|
+
const fetchData = async () => {
|
|
222
|
+
const namespaces = await djClient.namespaces();
|
|
223
|
+
setNamespaces(
|
|
224
|
+
namespaces.map(m => ({
|
|
225
|
+
value: m['namespace'],
|
|
226
|
+
label: m['namespace'],
|
|
227
|
+
})),
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
fetchData().catch(console.error);
|
|
231
|
+
}
|
|
232
|
+
}, [action, djClient, djClient.metrics]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="mid">
|
|
236
|
+
<NamespaceHeader namespace="" />
|
|
237
|
+
<div className="card">
|
|
238
|
+
<div className="card-header">
|
|
239
|
+
{pageTitle}
|
|
240
|
+
<center>
|
|
241
|
+
<Formik
|
|
242
|
+
initialValues={initialValues}
|
|
243
|
+
validate={validator}
|
|
244
|
+
onSubmit={handleSubmit}
|
|
245
|
+
>
|
|
246
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
247
|
+
const [node, setNode] = useState([]);
|
|
248
|
+
const [message, setMessage] = useState('');
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const fetchData = async () => {
|
|
251
|
+
if (action === Action.Edit) {
|
|
252
|
+
const data = await djClient.node(name);
|
|
253
|
+
|
|
254
|
+
// Check if node exists
|
|
255
|
+
if (data.message !== undefined) {
|
|
256
|
+
setNode(null);
|
|
257
|
+
setMessage(`Node ${name} does not exist!`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if node type can be edited
|
|
262
|
+
if (!nodeCanBeEdited(data.type)) {
|
|
263
|
+
setNode(null);
|
|
264
|
+
setMessage(
|
|
265
|
+
`Node ${name} is of type ${data.type} and cannot be edited`,
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Update fields with existing data to prepare for edit
|
|
271
|
+
updateFieldsWithNodeData(data, setFieldValue);
|
|
272
|
+
setNode(data);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
fetchData().catch(console.error);
|
|
276
|
+
}, [setFieldValue]);
|
|
277
|
+
return (
|
|
278
|
+
<Form>
|
|
279
|
+
{displayMessageAfterSubmit(status)}
|
|
280
|
+
{action === Action.Edit && message ? (
|
|
281
|
+
alertMessage(message)
|
|
282
|
+
) : (
|
|
283
|
+
<>
|
|
284
|
+
{action === Action.Add
|
|
285
|
+
? namespaceInput
|
|
286
|
+
: staticFieldsInEdit(node)}
|
|
287
|
+
<div className="DisplayNameInput NodeCreationInput">
|
|
288
|
+
<ErrorMessage name="display_name" component="span" />
|
|
289
|
+
<label htmlFor="displayName">Display Name</label>
|
|
290
|
+
<Field
|
|
291
|
+
type="text"
|
|
292
|
+
name="display_name"
|
|
293
|
+
id="displayName"
|
|
294
|
+
placeholder="Human readable display name"
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
{action === Action.Add ? fullNameInput : ''}
|
|
298
|
+
<div className="DescriptionInput NodeCreationInput">
|
|
299
|
+
<ErrorMessage name="description" component="span" />
|
|
300
|
+
<label htmlFor="Description">Description</label>
|
|
301
|
+
<Field
|
|
302
|
+
type="textarea"
|
|
303
|
+
as="textarea"
|
|
304
|
+
name="description"
|
|
305
|
+
id="Description"
|
|
306
|
+
placeholder="Describe your node"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="QueryInput NodeCreationInput">
|
|
310
|
+
<ErrorMessage name="query" component="span" />
|
|
311
|
+
<label htmlFor="Query">Query</label>
|
|
312
|
+
<NodeQueryField
|
|
313
|
+
djClient={djClient}
|
|
314
|
+
value={node.query ? node.query : ''}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
<div className="PrimaryKeyInput NodeCreationInput">
|
|
318
|
+
<ErrorMessage name="primary_key" component="span" />
|
|
319
|
+
<label htmlFor="primaryKey">Primary Key</label>
|
|
320
|
+
<Field
|
|
321
|
+
type="text"
|
|
322
|
+
name="primary_key"
|
|
323
|
+
id="primaryKey"
|
|
324
|
+
placeholder="Comma-separated list of PKs"
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="NodeModeInput NodeCreationInput">
|
|
328
|
+
<ErrorMessage name="mode" component="span" />
|
|
329
|
+
<label htmlFor="Mode">Mode</label>
|
|
330
|
+
<Field as="select" name="mode" id="Mode">
|
|
331
|
+
<option value="draft">Draft</option>
|
|
332
|
+
<option value="published">Published</option>
|
|
333
|
+
</Field>
|
|
334
|
+
</div>
|
|
335
|
+
<button type="submit" disabled={isSubmitting}>
|
|
336
|
+
{action === Action.Add ? 'Create' : 'Save'} {nodeType}
|
|
337
|
+
</button>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</Form>
|
|
341
|
+
);
|
|
342
|
+
}}
|
|
343
|
+
</Formik>
|
|
344
|
+
</center>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|