datajunction-ui 0.0.1-a1 → 0.0.1-a101

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.
Files changed (110) hide show
  1. package/Makefile +7 -1
  2. package/package.json +18 -7
  3. package/public/index.html +1 -1
  4. package/src/app/components/AddNodeDropdown.jsx +44 -0
  5. package/src/app/components/ListGroupItem.jsx +2 -1
  6. package/src/app/components/NodeListActions.jsx +69 -0
  7. package/src/app/components/NodeMaterializationDelete.jsx +80 -0
  8. package/src/app/components/QueryInfo.jsx +96 -1
  9. package/src/app/components/Search.jsx +94 -0
  10. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  11. package/src/app/components/__tests__/Search.test.jsx +63 -0
  12. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
  13. package/src/app/components/djgraph/Collapse.jsx +3 -2
  14. package/src/app/components/djgraph/DJNode.jsx +1 -1
  15. package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
  16. package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
  17. package/src/app/components/forms/Action.jsx +8 -0
  18. package/src/app/components/forms/NodeNameField.jsx +64 -0
  19. package/src/app/components/search.css +17 -0
  20. package/src/app/icons/AddItemIcon.jsx +16 -0
  21. package/src/app/icons/CommitIcon.jsx +45 -0
  22. package/src/app/icons/DiffIcon.jsx +63 -0
  23. package/src/app/icons/EyeIcon.jsx +20 -0
  24. package/src/app/icons/FilterIcon.jsx +7 -0
  25. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  26. package/src/app/icons/LoadingIcon.jsx +10 -10
  27. package/src/app/icons/PythonIcon.jsx +6 -44
  28. package/src/app/index.tsx +24 -0
  29. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  30. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  31. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  32. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  33. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
  34. package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
  35. package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
  36. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  37. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  38. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  39. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  40. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
  41. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  42. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  43. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  44. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
  46. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
  47. package/src/app/pages/AddEditNodePage/index.jsx +275 -194
  48. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
  49. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  50. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
  51. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
  52. package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
  53. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
  54. package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
  55. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  56. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  57. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  58. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  59. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
  60. package/src/app/pages/NamespacePage/index.jsx +272 -89
  61. package/src/app/pages/NodePage/AddBackfillPopover.jsx +60 -61
  62. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
  63. package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
  64. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  65. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  66. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
  67. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  68. package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
  69. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  70. package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
  71. package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
  72. package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
  73. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
  74. package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
  75. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  76. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  77. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
  78. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  79. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  80. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  81. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
  82. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  83. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  84. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  85. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
  86. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  87. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  88. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
  89. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
  90. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
  91. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
  92. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  93. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
  94. package/src/app/pages/NodePage/index.jsx +94 -57
  95. package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
  96. package/src/app/pages/Root/index.tsx +62 -12
  97. package/src/app/services/DJService.js +587 -55
  98. package/src/app/services/__tests__/DJService.test.jsx +382 -45
  99. package/src/index.tsx +1 -0
  100. package/src/mocks/mockNodes.jsx +265 -227
  101. package/src/styles/dag.css +4 -2
  102. package/src/styles/index.css +474 -10
  103. package/src/styles/loading.css +1 -1
  104. package/src/styles/node-creation.scss +84 -5
  105. package/src/styles/node-list.css +4 -0
  106. package/src/styles/sorted-table.css +15 -0
  107. package/src/app/components/DeleteNode.jsx +0 -55
  108. package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
  109. package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
  110. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
@@ -3,18 +3,27 @@
3
3
  * node types is largely the same, with minor differences handled server-side. For the `query`
4
4
  * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
5
5
  */
6
- import { ErrorMessage, Field, Form, Formik } from 'formik';
7
-
6
+ import { ErrorMessage, Form, Formik } from 'formik';
8
7
  import NamespaceHeader from '../../components/NamespaceHeader';
9
8
  import { useContext, useEffect, useState } from 'react';
10
9
  import DJClientContext from '../../providers/djclient';
11
10
  import 'styles/node-creation.scss';
12
- import AlertIcon from '../../icons/AlertIcon';
13
- import { useParams } from 'react-router-dom';
11
+ import { useParams, useNavigate } from 'react-router-dom';
14
12
  import { FullNameField } from './FullNameField';
15
- import { FormikSelect } from './FormikSelect';
13
+ import { MetricQueryField } from './MetricQueryField';
14
+ import { displayMessageAfterSubmit } from '../../../utils/form';
16
15
  import { NodeQueryField } from './NodeQueryField';
17
- import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
16
+ import { MetricMetadataFields } from './MetricMetadataFields';
17
+ import { UpstreamNodeField } from './UpstreamNodeField';
18
+ import { TagsField } from './TagsField';
19
+ import { NamespaceField } from './NamespaceField';
20
+ import { AlertMessage } from './AlertMessage';
21
+ import { DisplayNameField } from './DisplayNameField';
22
+ import { DescriptionField } from './DescriptionField';
23
+ import { NodeModeField } from './NodeModeField';
24
+ import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
25
+ import LoadingIcon from '../../icons/LoadingIcon';
26
+ import { ColumnsSelect } from './ColumnsSelect';
18
27
 
19
28
  class Action {
20
29
  static Add = new Action('add');
@@ -25,24 +34,22 @@ class Action {
25
34
  }
26
35
  }
27
36
 
28
- export function AddEditNodePage() {
37
+ export function AddEditNodePage({ extensions = {} }) {
29
38
  const djClient = useContext(DJClientContext).DataJunctionAPI;
39
+ const navigate = useNavigate();
30
40
 
31
41
  let { nodeType, initialNamespace, name } = useParams();
32
42
  const action = name !== undefined ? Action.Edit : Action.Add;
33
43
 
34
- const [namespaces, setNamespaces] = useState([]);
35
- const [tags, setTags] = useState([]);
36
-
37
44
  const initialValues = {
38
45
  name: action === Action.Edit ? name : '',
39
46
  namespace: action === Action.Add ? initialNamespace : '',
40
47
  display_name: '',
41
48
  query: '',
42
- node_type: '',
49
+ type: nodeType,
43
50
  description: '',
44
51
  primary_key: '',
45
- mode: 'draft',
52
+ mode: 'published',
46
53
  };
47
54
 
48
55
  const validator = values => {
@@ -50,26 +57,26 @@ export function AddEditNodePage() {
50
57
  if (!values.name) {
51
58
  errors.name = 'Required';
52
59
  }
53
- if (!values.query) {
60
+ if (values.type !== 'metric' && !values.query) {
54
61
  errors.query = 'Required';
55
62
  }
56
63
  return errors;
57
64
  };
58
65
 
59
- const handleSubmit = (values, { setSubmitting, setStatus }) => {
66
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
60
67
  if (action === Action.Add) {
61
- setTimeout(() => {
62
- createNode(values, setStatus);
68
+ await createNode(values, setStatus).then(_ => {
69
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
63
70
  setSubmitting(false);
64
- }, 400);
71
+ });
65
72
  } else {
66
- setTimeout(() => {
67
- patchNode(values, setStatus);
73
+ await patchNode(values, setStatus).then(_ => {
74
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
68
75
  setSubmitting(false);
69
- }, 400);
76
+ });
70
77
  }
71
- window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
72
78
  };
79
+ const submitHandlers = [handleSubmit];
73
80
 
74
81
  const pageTitle =
75
82
  action === Action.Add ? (
@@ -86,16 +93,16 @@ export function AddEditNodePage() {
86
93
  const staticFieldsInEdit = node => (
87
94
  <>
88
95
  <div className="NodeNameInput NodeCreationInput">
89
- <label htmlFor="name">Name</label> {name}
96
+ <label>Name</label> {name}
90
97
  </div>
91
98
  <div className="NodeNameInput NodeCreationInput">
92
- <label htmlFor="name">Type</label> {node.type}
99
+ <label>Type</label> {node.type}
93
100
  </div>
94
101
  </>
95
102
  );
96
103
 
97
104
  const primaryKeyToList = primaryKey => {
98
- return primaryKey.split(',').map(columnName => columnName.trim());
105
+ return primaryKey.map(columnName => columnName.trim());
99
106
  };
100
107
 
101
108
  const createNode = async (values, setStatus) => {
@@ -104,13 +111,20 @@ export function AddEditNodePage() {
104
111
  values.name,
105
112
  values.display_name,
106
113
  values.description,
107
- values.query,
114
+ values.type === 'metric'
115
+ ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}`
116
+ : values.query,
108
117
  values.mode,
109
118
  values.namespace,
110
119
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
120
+ values.metric_direction,
121
+ values.metric_unit,
122
+ values.required_dimensions,
111
123
  );
112
124
  if (status === 200 || status === 201) {
113
- await djClient.tagsNode(values.name, values.tags);
125
+ if (values.tags) {
126
+ await djClient.tagsNode(values.name, values.tags);
127
+ }
114
128
  setStatus({
115
129
  success: (
116
130
  <>
@@ -131,11 +145,20 @@ export function AddEditNodePage() {
131
145
  values.name,
132
146
  values.display_name,
133
147
  values.description,
134
- values.query,
148
+ values.type === 'metric'
149
+ ? `SELECT ${values.aggregate_expression} FROM ${values.upstream_node}`
150
+ : values.query,
135
151
  values.mode,
136
152
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
153
+ values.metric_direction,
154
+ values.metric_unit,
155
+ values.significant_digits,
156
+ values.required_dimensions,
157
+ );
158
+ const tagsResponse = await djClient.tagsNode(
159
+ values.name,
160
+ values.tags.map(tag => tag),
137
161
  );
138
- const tagsResponse = await djClient.tagsNode(values.name, values.tags);
139
162
  if ((status === 200 || status === 201) && tagsResponse.status === 200) {
140
163
  setStatus({
141
164
  success: (
@@ -147,31 +170,15 @@ export function AddEditNodePage() {
147
170
  });
148
171
  } else {
149
172
  setStatus({
150
- failure: `${json.message}, ${tagsResponse.json.message}`,
173
+ failure: `${json.message}`,
151
174
  });
152
175
  }
153
176
  };
154
177
 
155
- const namespaceInput = (
156
- <div className="NamespaceInput">
157
- <ErrorMessage name="namespace" component="span" />
158
- <label htmlFor="react-select-3-input">Namespace</label>
159
- <FormikSelect
160
- selectOptions={namespaces}
161
- formikFieldName="namespace"
162
- placeholder="Choose Namespace"
163
- defaultValue={{
164
- value: initialNamespace,
165
- label: initialNamespace,
166
- }}
167
- />
168
- </div>
169
- );
170
-
171
178
  const fullNameInput = (
172
179
  <div className="FullNameInput NodeCreationInput">
173
180
  <ErrorMessage name="name" component="span" />
174
- <label htmlFor="FullName">Full Name</label>
181
+ <label htmlFor="FullName">Full Name *</label>
175
182
  <FullNameField type="text" name="name" />
176
183
  </div>
177
184
  );
@@ -180,7 +187,65 @@ export function AddEditNodePage() {
180
187
  return new Set(['transform', 'metric', 'dimension']).has(nodeType);
181
188
  };
182
189
 
183
- const updateFieldsWithNodeData = (data, setFieldValue) => {
190
+ const getExistingNodeData = async name => {
191
+ const node = await djClient.getNodeForEditing(name);
192
+ if (node === null) {
193
+ return { message: `Node ${name} does not exist` };
194
+ }
195
+ const baseData = {
196
+ name: node.name,
197
+ type: node.type.toLowerCase(),
198
+ display_name: node.current.displayName,
199
+ description: node.current.description,
200
+ primary_key: node.current.primaryKey,
201
+ query: node.current.query,
202
+ tags: node.tags,
203
+ mode: node.current.mode.toLowerCase(),
204
+ };
205
+
206
+ if (node.type === 'METRIC') {
207
+ return {
208
+ ...baseData,
209
+ metric_direction: node.current.metricMetadata?.direction?.toLowerCase(),
210
+ metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
211
+ significant_digits: node.current.metricMetadata?.significantDigits,
212
+ required_dimensions: node.current.requiredDimensions.map(
213
+ dim => dim.name,
214
+ ),
215
+ upstream_node: node.current.parents[0]?.name,
216
+ aggregate_expression: node.current.metricMetadata?.expression,
217
+ };
218
+ }
219
+ return baseData;
220
+ };
221
+
222
+ const runValidityChecks = (data, setNode, setMessage) => {
223
+ // Check if node exists
224
+ if (data.message !== undefined) {
225
+ setNode(null);
226
+ setMessage(`Node ${name} does not exist!`);
227
+ return;
228
+ }
229
+ // Check if node type can be edited
230
+ if (!nodeCanBeEdited(data.type)) {
231
+ setNode(null);
232
+ if (data.type === 'cube') {
233
+ navigate(`/nodes/${data.name}/edit-cube`);
234
+ }
235
+ setMessage(`Node ${name} is of type ${data.type} and cannot be edited`);
236
+ }
237
+ };
238
+
239
+ const updateFieldsWithNodeData = (
240
+ data,
241
+ setFieldValue,
242
+ setNode,
243
+ setSelectTags,
244
+ setSelectPrimaryKey,
245
+ setSelectUpstreamNode,
246
+ setSelectRequiredDims,
247
+ ) => {
248
+ // Update fields with existing data to prepare for edit
184
249
  const fields = [
185
250
  'display_name',
186
251
  'query',
@@ -189,62 +254,71 @@ export function AddEditNodePage() {
189
254
  'primary_key',
190
255
  'mode',
191
256
  'tags',
257
+ 'aggregate_expression',
258
+ 'upstream_node',
259
+ 'metric_unit',
260
+ 'metric_direction',
261
+ 'significant_digits',
192
262
  ];
193
263
  fields.forEach(field => {
194
- if (
195
- field === 'primary_key' &&
196
- data[field] !== undefined &&
197
- Array.isArray(data[field])
198
- ) {
199
- data[field] = data[field].join(', ');
264
+ if (field === 'tags') {
265
+ setFieldValue(
266
+ field,
267
+ data[field].map(tag => tag.name),
268
+ );
200
269
  } else {
201
270
  setFieldValue(field, data[field] || '', false);
202
271
  }
203
272
  });
204
- };
273
+ setNode(data);
205
274
 
206
- const alertMessage = message => {
207
- return (
208
- <div className="message alert">
209
- <AlertIcon />
210
- {message}
211
- </div>
275
+ // For react-select fields, we have to explicitly set the entire
276
+ // field rather than just the values
277
+ setSelectTags(
278
+ <TagsField
279
+ defaultValue={data.tags.map(t => {
280
+ return { value: t.name, label: t.displayName };
281
+ })}
282
+ />,
212
283
  );
213
- };
214
-
215
- // Get namespaces, only necessary when creating a node
216
- useEffect(() => {
217
- if (action === Action.Add) {
218
- const fetchData = async () => {
219
- const namespaces = await djClient.namespaces();
220
- setNamespaces(
221
- namespaces.map(m => ({
222
- value: m['namespace'],
223
- label: m['namespace'],
224
- })),
225
- );
226
- };
227
- fetchData().catch(console.error);
228
- }
229
- }, [action, djClient, djClient.metrics]);
230
-
231
- // Get list of tags
232
- useEffect(() => {
233
- const fetchData = async () => {
234
- const tags = await djClient.listTags();
235
- setTags(
236
- tags.map(tag => ({
237
- value: tag.name,
238
- label: tag.display_name,
239
- })),
284
+ setSelectPrimaryKey(
285
+ <ColumnsSelect
286
+ defaultValue={data.primary_key}
287
+ fieldName="primary_key"
288
+ label="Primary Key"
289
+ isMulti={true}
290
+ />,
291
+ );
292
+ if (data.required_dimensions) {
293
+ setSelectRequiredDims(
294
+ <RequiredDimensionsSelect
295
+ defaultValue={data.required_dimensions.map(dim => {
296
+ return { value: dim, label: dim };
297
+ })}
298
+ />,
240
299
  );
241
- };
242
- fetchData().catch(console.error);
243
- }, [djClient, djClient.listTags]);
300
+ }
301
+ setSelectUpstreamNode(
302
+ <UpstreamNodeField
303
+ defaultValue={{
304
+ value: data.upstream_node,
305
+ label: data.upstream_node,
306
+ }}
307
+ />,
308
+ );
309
+ };
244
310
 
245
311
  return (
246
312
  <div className="mid">
247
- <NamespaceHeader namespace="" />
313
+ <NamespaceHeader
314
+ namespace={
315
+ initialNamespace
316
+ ? initialNamespace
317
+ : name
318
+ ? name.substring(0, name.lastIndexOf('.'))
319
+ : ''
320
+ }
321
+ />
248
322
  <div className="card">
249
323
  <div className="card-header">
250
324
  {pageTitle}
@@ -252,135 +326,142 @@ export function AddEditNodePage() {
252
326
  <Formik
253
327
  initialValues={initialValues}
254
328
  validate={validator}
255
- onSubmit={handleSubmit}
329
+ onSubmit={async (values, { setSubmitting, setStatus }) => {
330
+ try {
331
+ for (const handler of submitHandlers) {
332
+ await handler(values, { setSubmitting, setStatus });
333
+ }
334
+ } catch (error) {
335
+ console.error('Error in submission', error);
336
+ } finally {
337
+ setSubmitting(false);
338
+ }
339
+ }}
256
340
  >
257
341
  {function Render({ isSubmitting, status, setFieldValue }) {
258
342
  const [node, setNode] = useState([]);
343
+ const [selectPrimaryKey, setSelectPrimaryKey] = useState(null);
344
+ const [selectRequiredDims, setSelectRequiredDims] =
345
+ useState(null);
346
+ const [selectUpstreamNode, setSelectUpstreamNode] =
347
+ useState(null);
259
348
  const [selectTags, setSelectTags] = useState(null);
260
349
  const [message, setMessage] = useState('');
261
350
 
262
- const tagsInput = (
263
- <div
264
- className="TagsInput"
265
- style={{ width: '25%', margin: '1rem 0 1rem 1.2rem' }}
266
- >
267
- <ErrorMessage name="tags" component="span" />
268
- <label htmlFor="react-select-3-input">Tags</label>
269
- <span data-testid="select-tags">
270
- {action === Action.Edit ? (
271
- selectTags
272
- ) : (
273
- <FormikSelect
274
- isMulti={true}
275
- selectOptions={tags}
276
- formikFieldName="tags"
277
- placeholder="Choose Tags"
278
- />
279
- )}
280
- </span>
281
- </div>
282
- );
283
-
284
351
  useEffect(() => {
285
352
  const fetchData = async () => {
286
353
  if (action === Action.Edit) {
287
- const data = await djClient.node(name);
288
-
289
- // Check if node exists
290
- if (data.message !== undefined) {
291
- setNode(null);
292
- setMessage(`Node ${name} does not exist!`);
293
- return;
294
- }
295
-
296
- // Check if node type can be edited
297
- if (!nodeCanBeEdited(data.type)) {
298
- setNode(null);
299
- setMessage(
300
- `Node ${name} is of type ${data.type} and cannot be edited`,
301
- );
302
- return;
303
- }
304
-
305
- // Update fields with existing data to prepare for edit
306
- updateFieldsWithNodeData(data, setFieldValue);
307
- setNode(data);
308
- setSelectTags(
309
- <FormikSelect
310
- isMulti={true}
311
- selectOptions={tags}
312
- formikFieldName="tags"
313
- placeholder="Choose Tags"
314
- defaultValue={data.tags.map(t => {
315
- return { value: t.name, label: t.display_name };
316
- })}
317
- />,
354
+ const data = await getExistingNodeData(name);
355
+ runValidityChecks(data, setNode, setMessage);
356
+ updateFieldsWithNodeData(
357
+ data,
358
+ setFieldValue,
359
+ setNode,
360
+ setSelectTags,
361
+ setSelectPrimaryKey,
362
+ setSelectUpstreamNode,
363
+ setSelectRequiredDims,
318
364
  );
319
365
  }
320
366
  };
321
367
  fetchData().catch(console.error);
322
- }, [setFieldValue, tags]);
368
+ }, [setFieldValue]);
323
369
  return (
324
370
  <Form>
325
371
  {displayMessageAfterSubmit(status)}
326
372
  {action === Action.Edit && message ? (
327
- alertMessage(message)
373
+ <AlertMessage message={message} />
328
374
  ) : (
329
375
  <>
330
- {action === Action.Add
331
- ? namespaceInput
332
- : staticFieldsInEdit(node)}
333
- <div className="DisplayNameInput NodeCreationInput">
334
- <ErrorMessage name="display_name" component="span" />
335
- <label htmlFor="displayName">Display Name</label>
336
- <Field
337
- type="text"
338
- name="display_name"
339
- id="displayName"
340
- placeholder="Human readable display name"
341
- />
342
- </div>
376
+ {action === Action.Add ? (
377
+ <NamespaceField initialNamespace={initialNamespace} />
378
+ ) : (
379
+ staticFieldsInEdit(node)
380
+ )}
381
+ <DisplayNameField />
343
382
  {action === Action.Add ? fullNameInput : ''}
344
- <div className="DescriptionInput NodeCreationInput">
345
- <ErrorMessage name="description" component="span" />
346
- <label htmlFor="Description">Description</label>
347
- <Field
348
- type="textarea"
349
- as="textarea"
350
- name="description"
351
- id="Description"
352
- placeholder="Describe your node"
383
+ <DescriptionField />
384
+ <br />
385
+ {nodeType === 'metric' || node?.type === 'metric' ? (
386
+ action === Action.Edit ? (
387
+ selectUpstreamNode
388
+ ) : (
389
+ <UpstreamNodeField />
390
+ )
391
+ ) : (
392
+ ''
393
+ )}
394
+ <br />
395
+ <br />
396
+ {nodeType === 'metric' || node.type === 'metric' ? (
397
+ <MetricQueryField
398
+ djClient={djClient}
399
+ value={
400
+ node.aggregate_expression
401
+ ? node.aggregate_expression
402
+ : ''
403
+ }
353
404
  />
354
- </div>
355
- <div className="QueryInput NodeCreationInput">
356
- <ErrorMessage name="query" component="span" />
357
- <label htmlFor="Query">Query</label>
405
+ ) : (
358
406
  <NodeQueryField
359
407
  djClient={djClient}
360
408
  value={node.query ? node.query : ''}
361
409
  />
362
- </div>
363
- <div className="PrimaryKeyInput NodeCreationInput">
364
- <ErrorMessage name="primary_key" component="span" />
365
- <label htmlFor="primaryKey">Primary Key</label>
366
- <Field
367
- type="text"
368
- name="primary_key"
369
- id="primaryKey"
370
- placeholder="Comma-separated list of PKs"
371
- />
372
- </div>
373
- {tagsInput}
374
- <div className="NodeModeInput NodeCreationInput">
375
- <ErrorMessage name="mode" component="span" />
376
- <label htmlFor="Mode">Mode</label>
377
- <Field as="select" name="mode" id="Mode">
378
- <option value="draft">Draft</option>
379
- <option value="published">Published</option>
380
- </Field>
381
- </div>
410
+ )}
411
+ <br />
412
+ {nodeType === 'metric' || node.type === 'metric' ? (
413
+ <MetricMetadataFields />
414
+ ) : (
415
+ ''
416
+ )}
417
+ {nodeType !== 'metric' && node.type !== 'metric' ? (
418
+ action === Action.Edit ? (
419
+ selectPrimaryKey
420
+ ) : (
421
+ <ColumnsSelect
422
+ defaultValue={[]}
423
+ fieldName="primary_key"
424
+ label="Primary Key"
425
+ isMulti={true}
426
+ />
427
+ )
428
+ ) : action === Action.Edit ? (
429
+ selectRequiredDims
430
+ ) : (
431
+ <RequiredDimensionsSelect />
432
+ )}
433
+ {Object.entries(extensions).map(
434
+ ([key, ExtensionComponent]) => (
435
+ <div key={key} className="mt-4 border-t pt-4">
436
+ <ExtensionComponent
437
+ node={node}
438
+ action={action}
439
+ registerSubmitHandler={(
440
+ onSubmit,
441
+ { prepend } = {},
442
+ ) => {
443
+ if (!submitHandlers.includes(onSubmit)) {
444
+ if (prepend) {
445
+ submitHandlers.unshift(onSubmit);
446
+ } else {
447
+ submitHandlers.push(onSubmit);
448
+ }
449
+ }
450
+ }}
451
+ />
452
+ </div>
453
+ ),
454
+ )}
455
+ {action === Action.Edit ? selectTags : <TagsField />}
456
+ <NodeModeField />
457
+
382
458
  <button type="submit" disabled={isSubmitting}>
383
- {action === Action.Add ? 'Create' : 'Save'} {nodeType}
459
+ {isSubmitting ? (
460
+ <LoadingIcon />
461
+ ) : (
462
+ (action === Action.Add ? 'Create ' : 'Save ') +
463
+ (nodeType ? nodeType : '')
464
+ )}
384
465
  </button>
385
466
  </>
386
467
  )}