datajunction-ui 0.0.11 → 0.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,61 +1,144 @@
1
1
  /**
2
2
  * Custom metadata field component for nodes
3
3
  */
4
- import { ErrorMessage, Field } from 'formik';
5
- import { useState } from 'react';
4
+ import { ErrorMessage, Field, useFormikContext } from 'formik';
5
+ import CodeMirror from '@uiw/react-codemirror';
6
+ import { langs } from '@uiw/codemirror-extensions-langs';
7
+ import { useState, useEffect } from 'react';
6
8
 
7
- export const CustomMetadataField = ({ initialValue = {} }) => {
8
- const [jsonString, setJsonString] = useState(
9
- JSON.stringify(initialValue, null, 2),
10
- );
11
- const [error, setError] = useState('');
9
+ export const CustomMetadataField = ({ value }) => {
10
+ const formik = useFormikContext();
11
+ const jsonExt = langs.json();
12
+ const [hasError, setHasError] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if (!value || value === '') {
16
+ setHasError(false);
17
+ return;
18
+ }
12
19
 
13
- const handleChange = (e, setFieldValue) => {
14
- const value = e.target.value;
15
- setJsonString(value);
20
+ const stringValue =
21
+ typeof value === 'string' ? value : JSON.stringify(value, null, 2);
16
22
 
17
23
  try {
18
- if (value.trim() === '') {
19
- setFieldValue('custom_metadata', null);
20
- setError('');
21
- } else {
22
- const parsed = JSON.parse(value);
23
- setFieldValue('custom_metadata', parsed);
24
- setError('');
25
- }
24
+ JSON.parse(stringValue);
25
+ setHasError(false);
26
26
  } catch (err) {
27
- setError('Invalid JSON format');
27
+ setHasError(true);
28
+ }
29
+ }, [value]);
30
+
31
+ const formatValue = value => {
32
+ if (value === null || value === undefined) {
33
+ return '';
34
+ }
35
+ if (typeof value === 'string') {
36
+ return value;
37
+ }
38
+ return JSON.stringify(value, null, 2);
39
+ };
40
+
41
+ const updateFormik = val => {
42
+ formik.setFieldValue('custom_metadata', val);
43
+ formik.setFieldTouched('custom_metadata', true);
44
+
45
+ if (!val || val.trim() === '') {
46
+ setHasError(false);
47
+ } else {
48
+ try {
49
+ JSON.parse(val);
50
+ setHasError(false);
51
+ } catch (err) {
52
+ setHasError(true);
53
+ }
28
54
  }
29
55
  };
30
56
 
31
57
  return (
32
- <div className="NodeCreationInput" style={{ marginTop: '20px' }}>
33
- <ErrorMessage name="custom_metadata" component="span" />
34
- <label htmlFor="CustomMetadata">Custom Metadata (JSON)</label>
35
- <Field name="custom_metadata">
36
- {({ field, form }) => (
37
- <div>
38
- <textarea
39
- id="CustomMetadata"
40
- value={jsonString}
41
- onChange={e => handleChange(e, form.setFieldValue)}
42
- style={{
43
- width: '100%',
44
- minHeight: '100px',
45
- fontFamily: 'monospace',
46
- fontSize: '12px',
47
- padding: '8px',
48
- border: error ? '1px solid red' : '1px solid #ccc',
49
- borderRadius: '4px',
50
- }}
51
- placeholder='{"key": "value"}'
52
- />
53
- {error && (
54
- <span style={{ color: 'red', fontSize: '12px' }}>{error}</span>
55
- )}
56
- </div>
57
- )}
58
- </Field>
58
+ <div className="QueryInput NodeCreationInput">
59
+ <details>
60
+ <summary style={{ cursor: 'pointer' }}>
61
+ <label
62
+ style={{
63
+ paddingLeft: '3px',
64
+ display: 'inline-block',
65
+ pointerEvents: 'none',
66
+ }}
67
+ >
68
+ Custom Metadata (JSON)
69
+ </label>
70
+ </summary>
71
+ <ErrorMessage name="custom_metadata" component="span" />
72
+ <Field
73
+ type="textarea"
74
+ style={{ display: 'none' }}
75
+ as="textarea"
76
+ name="custom_metadata"
77
+ id="CustomMetadata"
78
+ validate={value => {
79
+ if (!value || value.trim() === '') {
80
+ return undefined;
81
+ }
82
+ try {
83
+ const parsed = JSON.parse(value);
84
+
85
+ if (
86
+ typeof parsed === 'object' &&
87
+ parsed !== null &&
88
+ !Array.isArray(parsed)
89
+ ) {
90
+ const keys = Object.keys(parsed);
91
+ const originalKeyMatches = value.match(/"([^"]+)"\s*:/g);
92
+ if (
93
+ originalKeyMatches &&
94
+ originalKeyMatches.length > keys.length
95
+ ) {
96
+ return 'Duplicate keys detected';
97
+ }
98
+ }
99
+
100
+ return undefined;
101
+ } catch (err) {
102
+ return 'Invalid JSON format';
103
+ }
104
+ }}
105
+ />
106
+ <div
107
+ role="button"
108
+ tabIndex={0}
109
+ className={`relative flex ${
110
+ hasError ? 'bg-red-900/20' : 'bg-[#282a36]'
111
+ }`}
112
+ style={{
113
+ border: hasError ? '2px solid #ef4444' : 'none',
114
+ borderRadius: '4px',
115
+ boxShadow: hasError ? '0 0 0 1px rgba(239, 68, 68, 0.3)' : 'none',
116
+ }}
117
+ >
118
+ <CodeMirror
119
+ id={'custom_metadata'}
120
+ name={'custom_metadata'}
121
+ extensions={[jsonExt]}
122
+ value={formatValue(value)}
123
+ placeholder={'{\n "key": "value"\n}'}
124
+ options={{
125
+ theme: 'default',
126
+ lineNumbers: true,
127
+ }}
128
+ width="100%"
129
+ height="200px"
130
+ style={{
131
+ margin: '0 0 23px 0',
132
+ flex: 1,
133
+ fontSize: '150%',
134
+ textAlign: 'left',
135
+ }}
136
+ onChange={(value, viewUpdate) => {
137
+ updateFormik(value);
138
+ }}
139
+ />
140
+ </div>
141
+ </details>
59
142
  </div>
60
143
  );
61
144
  };
@@ -203,7 +203,7 @@ describe('AddEditNodePage submission succeeded', () => {
203
203
  '',
204
204
  undefined,
205
205
  ['dj'],
206
- '', // custom_metadata is set to '' when null in the form
206
+ null,
207
207
  );
208
208
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
209
209
  expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
@@ -109,6 +109,17 @@ export function AddEditNodePage({ extensions = {} }) {
109
109
  return primaryKey.map(columnName => columnName.trim());
110
110
  };
111
111
 
112
+ const parseCustomMetadata = customMetadata => {
113
+ if (!customMetadata || customMetadata.trim() === '') {
114
+ return null;
115
+ }
116
+ try {
117
+ return JSON.parse(customMetadata);
118
+ } catch (err) {
119
+ return null;
120
+ }
121
+ };
122
+
112
123
  const createNode = async (values, setStatus) => {
113
124
  const { status, json } = await djClient.createNode(
114
125
  nodeType,
@@ -124,7 +135,7 @@ export function AddEditNodePage({ extensions = {} }) {
124
135
  values.metric_direction,
125
136
  values.metric_unit,
126
137
  values.required_dimensions,
127
- values.custom_metadata,
138
+ parseCustomMetadata(values.custom_metadata),
128
139
  );
129
140
  if (status === 200 || status === 201) {
130
141
  if (values.tags) {
@@ -160,7 +171,7 @@ export function AddEditNodePage({ extensions = {} }) {
160
171
  values.significant_digits,
161
172
  values.required_dimensions,
162
173
  values.owners,
163
- values.custom_metadata,
174
+ parseCustomMetadata(values.custom_metadata),
164
175
  );
165
176
  const tagsResponse = await djClient.tagsNode(
166
177
  values.name,
@@ -283,6 +294,9 @@ export function AddEditNodePage({ extensions = {} }) {
283
294
  field,
284
295
  data[field].map(owner => owner.username),
285
296
  );
297
+ } else if (field === 'custom_metadata') {
298
+ const value = data[field] ? JSON.stringify(data[field], null, 2) : '';
299
+ setFieldValue(field, value, false);
286
300
  } else {
287
301
  setFieldValue(field, data[field] || '', false);
288
302
  }
@@ -352,6 +366,8 @@ export function AddEditNodePage({ extensions = {} }) {
352
366
  <Formik
353
367
  initialValues={initialValues}
354
368
  validate={validator}
369
+ validateOnChange={true}
370
+ validateOnBlur={true}
355
371
  onSubmit={async (values, { setSubmitting, setStatus }) => {
356
372
  try {
357
373
  for (const handler of submitHandlers) {
@@ -364,7 +380,16 @@ export function AddEditNodePage({ extensions = {} }) {
364
380
  }
365
381
  }}
366
382
  >
367
- {function Render({ isSubmitting, status, setFieldValue }) {
383
+ {function Render(formikProps) {
384
+ const {
385
+ isSubmitting,
386
+ status,
387
+ setFieldValue,
388
+ errors,
389
+ touched,
390
+ isValid,
391
+ dirty,
392
+ } = formikProps;
368
393
  const [node, setNode] = useState([]);
369
394
  const [selectPrimaryKey, setSelectPrimaryKey] = useState(null);
370
395
  const [selectRequiredDims, setSelectRequiredDims] =
@@ -449,9 +474,6 @@ export function AddEditNodePage({ extensions = {} }) {
449
474
  ) : (
450
475
  ''
451
476
  )}
452
- <CustomMetadataField
453
- initialValue={node.custom_metadata || {}}
454
- />
455
477
  {nodeType !== 'metric' && node.type !== 'metric' ? (
456
478
  action === Action.Edit ? (
457
479
  selectPrimaryKey
@@ -468,6 +490,11 @@ export function AddEditNodePage({ extensions = {} }) {
468
490
  ) : (
469
491
  <RequiredDimensionsSelect />
470
492
  )}
493
+ <CustomMetadataField
494
+ value={
495
+ node.custom_metadata ? node.custom_metadata : ''
496
+ }
497
+ />
471
498
  {Object.entries(extensions).map(
472
499
  ([key, ExtensionComponent]) => (
473
500
  <div key={key} className="mt-4 border-t pt-4">
@@ -493,7 +520,10 @@ export function AddEditNodePage({ extensions = {} }) {
493
520
  {action === Action.Edit ? selectTags : <TagsField />}
494
521
  <NodeModeField />
495
522
 
496
- <button type="submit" disabled={isSubmitting}>
523
+ <button
524
+ type="submit"
525
+ disabled={isSubmitting || !isValid}
526
+ >
497
527
  {isSubmitting ? (
498
528
  <LoadingIcon />
499
529
  ) : (