datajunction-ui 0.0.144 → 0.0.145
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/CubeBuilderPage/CubePreviewPanel.jsx +173 -0
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +268 -97
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +273 -60
- package/src/app/pages/CubeBuilderPage/__tests__/CubePreviewPanel.test.jsx +108 -0
- package/src/app/pages/CubeBuilderPage/__tests__/DimensionsSelect.test.jsx +229 -0
- package/src/app/pages/CubeBuilderPage/__tests__/MetricsSelect.test.jsx +137 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +145 -37
- package/src/app/pages/CubeBuilderPage/index.jsx +367 -125
- package/src/app/pages/CubeBuilderPage/styles.css +489 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +12 -1
- package/src/app/services/DJService.js +69 -0
- package/src/app/services/__tests__/DJService.test.jsx +100 -0
|
@@ -1,21 +1,114 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
2
|
+
import Select from 'react-select';
|
|
2
3
|
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
3
4
|
import { DataJunctionAPI } from '../../services/DJService';
|
|
4
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
|
+
import { useCurrentUser } from '../../providers/UserProvider';
|
|
5
7
|
import 'react-querybuilder/dist/query-builder.scss';
|
|
6
8
|
import 'styles/styles.scss';
|
|
7
|
-
import
|
|
9
|
+
import './styles.css';
|
|
10
|
+
import '../QueryPlannerPage/styles.css';
|
|
11
|
+
import { ErrorMessage, FastField, Field, Form, Formik, useField } from 'formik';
|
|
8
12
|
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
9
|
-
import { useParams } from 'react-router-dom';
|
|
13
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
10
14
|
import { Action } from '../../components/forms/Action';
|
|
11
15
|
import NodeNameField from '../../components/forms/NodeNameField';
|
|
12
16
|
import { MetricsSelect } from './MetricsSelect';
|
|
13
17
|
import { DimensionsSelect } from './DimensionsSelect';
|
|
14
|
-
import {
|
|
15
|
-
|
|
18
|
+
import { CubePreviewPanel } from './CubePreviewPanel';
|
|
19
|
+
|
|
20
|
+
// Description textarea using FastField so typing here doesn't trigger
|
|
21
|
+
// re-renders of unrelated form sections (heavy ones like the SQL preview).
|
|
22
|
+
const DescriptionField = () => (
|
|
23
|
+
<FastField
|
|
24
|
+
as="textarea"
|
|
25
|
+
id="Description"
|
|
26
|
+
name="description"
|
|
27
|
+
placeholder="Describe your cube"
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Simple Tags select matching MetricsSelect styling
|
|
32
|
+
const CubeTagsSelect = ({ defaultValue }) => {
|
|
33
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
34
|
+
const [field, , helpers] = useField('tags');
|
|
35
|
+
const [options, setOptions] = useState([]);
|
|
36
|
+
const [selected, setSelected] = useState(defaultValue || []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
djClient.listTags().then(tags => {
|
|
40
|
+
setOptions(tags.map(t => ({ value: t.name, label: t.display_name })));
|
|
41
|
+
});
|
|
42
|
+
}, [djClient]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (defaultValue) setSelected(defaultValue);
|
|
46
|
+
}, [defaultValue]);
|
|
47
|
+
|
|
48
|
+
const handleChange = sel => {
|
|
49
|
+
setSelected(sel || []);
|
|
50
|
+
helpers.setValue((sel || []).map(s => s.value));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Select
|
|
55
|
+
value={selected}
|
|
56
|
+
options={options}
|
|
57
|
+
onChange={handleChange}
|
|
58
|
+
isMulti
|
|
59
|
+
placeholder="Select tags..."
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Simple Owners select matching MetricsSelect styling
|
|
65
|
+
const CubeOwnersSelect = ({ defaultValue, isEdit = false }) => {
|
|
66
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
67
|
+
const { currentUser } = useCurrentUser();
|
|
68
|
+
const [field, , helpers] = useField('owners');
|
|
69
|
+
const [options, setOptions] = useState([]);
|
|
70
|
+
const [selected, setSelected] = useState(defaultValue || []);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
djClient.users().then(users => {
|
|
74
|
+
setOptions(users.map(u => ({ value: u.username, label: u.username })));
|
|
75
|
+
});
|
|
76
|
+
}, [djClient]);
|
|
77
|
+
|
|
78
|
+
// Only auto-default to current user in Add mode; in Edit mode wait for defaultValue
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (defaultValue) {
|
|
81
|
+
setSelected(defaultValue);
|
|
82
|
+
helpers.setValue(defaultValue.map(d => d.value));
|
|
83
|
+
} else if (!isEdit && currentUser) {
|
|
84
|
+
const def = [
|
|
85
|
+
{ value: currentUser.username, label: currentUser.username },
|
|
86
|
+
];
|
|
87
|
+
setSelected(def);
|
|
88
|
+
helpers.setValue([currentUser.username]);
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [defaultValue, currentUser, isEdit]);
|
|
92
|
+
|
|
93
|
+
const handleChange = sel => {
|
|
94
|
+
setSelected(sel || []);
|
|
95
|
+
helpers.setValue((sel || []).map(s => s.value));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Select
|
|
100
|
+
value={selected}
|
|
101
|
+
options={options}
|
|
102
|
+
onChange={handleChange}
|
|
103
|
+
isMulti
|
|
104
|
+
placeholder="Select owners..."
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
16
108
|
|
|
17
109
|
export function CubeBuilderPage() {
|
|
18
110
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
111
|
+
const navigate = useNavigate();
|
|
19
112
|
|
|
20
113
|
let { nodeType, initialNamespace, name } = useParams();
|
|
21
114
|
const action = name !== undefined ? Action.Edit : Action.Add;
|
|
@@ -64,12 +157,8 @@ export function CubeBuilderPage() {
|
|
|
64
157
|
await djClient.tagsNode(values.name, values.tags);
|
|
65
158
|
}
|
|
66
159
|
setStatus({
|
|
67
|
-
success:
|
|
68
|
-
|
|
69
|
-
Successfully created {json.type} node{' '}
|
|
70
|
-
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
71
|
-
</>
|
|
72
|
-
),
|
|
160
|
+
success: true,
|
|
161
|
+
savedName: json.name,
|
|
73
162
|
});
|
|
74
163
|
} else {
|
|
75
164
|
setStatus({
|
|
@@ -95,12 +184,8 @@ export function CubeBuilderPage() {
|
|
|
95
184
|
);
|
|
96
185
|
if ((status === 200 || status === 201) && tagsResponse.status === 200) {
|
|
97
186
|
setStatus({
|
|
98
|
-
success:
|
|
99
|
-
|
|
100
|
-
Successfully updated {json.type} node{' '}
|
|
101
|
-
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
102
|
-
</>
|
|
103
|
-
),
|
|
187
|
+
success: true,
|
|
188
|
+
savedName: json.name,
|
|
104
189
|
});
|
|
105
190
|
} else {
|
|
106
191
|
setStatus({
|
|
@@ -122,61 +207,50 @@ export function CubeBuilderPage() {
|
|
|
122
207
|
'tags',
|
|
123
208
|
data.tags.map(tag => tag.name),
|
|
124
209
|
);
|
|
125
|
-
//
|
|
126
|
-
// field rather than just the values
|
|
210
|
+
// Store default values as arrays for the Select components
|
|
127
211
|
setSelectTags(
|
|
128
|
-
|
|
129
|
-
defaultValue={data.tags.map(t => {
|
|
130
|
-
return { value: t.name, label: t.displayName };
|
|
131
|
-
})}
|
|
132
|
-
/>,
|
|
212
|
+
data.tags.map(t => ({ value: t.name, label: t.displayName })),
|
|
133
213
|
);
|
|
134
214
|
if (data.owners) {
|
|
135
215
|
setSelectOwners(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
/>,
|
|
216
|
+
data.owners.map(owner => ({
|
|
217
|
+
value: owner.username,
|
|
218
|
+
label: owner.username,
|
|
219
|
+
})),
|
|
141
220
|
);
|
|
142
221
|
}
|
|
143
222
|
};
|
|
144
223
|
|
|
145
|
-
const staticFieldsInEdit = () => (
|
|
146
|
-
<>
|
|
147
|
-
<div className="NodeNameInput NodeCreationInput">
|
|
148
|
-
<label htmlFor="name">Name</label> {name}
|
|
149
|
-
</div>
|
|
150
|
-
<div className="NodeNameInput NodeCreationInput">
|
|
151
|
-
<label htmlFor="name">Type</label> cube
|
|
152
|
-
</div>
|
|
153
|
-
<div className="DisplayNameInput NodeCreationInput">
|
|
154
|
-
<ErrorMessage name="display_name" component="span" />
|
|
155
|
-
<label htmlFor="displayName">Display Name</label>
|
|
156
|
-
<Field
|
|
157
|
-
type="text"
|
|
158
|
-
name="display_name"
|
|
159
|
-
id="displayName"
|
|
160
|
-
placeholder="Human readable display name"
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
163
|
-
</>
|
|
164
|
-
);
|
|
165
|
-
|
|
166
224
|
// @ts-ignore
|
|
167
225
|
return (
|
|
168
226
|
<>
|
|
169
227
|
<div className="mid">
|
|
170
|
-
<NamespaceHeader
|
|
228
|
+
<NamespaceHeader
|
|
229
|
+
namespace={
|
|
230
|
+
initialNamespace
|
|
231
|
+
? initialNamespace
|
|
232
|
+
: name
|
|
233
|
+
? name.substring(0, name.lastIndexOf('.'))
|
|
234
|
+
: ''
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
171
237
|
<Formik
|
|
172
238
|
initialValues={initialValues}
|
|
173
239
|
validate={validator}
|
|
174
240
|
onSubmit={handleSubmit}
|
|
175
241
|
>
|
|
176
|
-
{function Render({
|
|
242
|
+
{function Render({
|
|
243
|
+
isSubmitting,
|
|
244
|
+
status,
|
|
245
|
+
setFieldValue,
|
|
246
|
+
values,
|
|
247
|
+
props,
|
|
248
|
+
}) {
|
|
177
249
|
const [node, setNode] = useState([]);
|
|
178
250
|
const [selectTags, setSelectTags] = useState(null);
|
|
179
251
|
const [selectOwners, setSelectOwners] = useState(null);
|
|
252
|
+
const [justSaved, setJustSaved] = useState(false);
|
|
253
|
+
const [saveError, setSaveError] = useState(null);
|
|
180
254
|
|
|
181
255
|
// Get cube
|
|
182
256
|
useEffect(() => {
|
|
@@ -195,86 +269,254 @@ export function CubeBuilderPage() {
|
|
|
195
269
|
fetchData().catch(console.error);
|
|
196
270
|
}, [setFieldValue]);
|
|
197
271
|
|
|
272
|
+
// Stable callbacks so the memoized child components don't re-render
|
|
273
|
+
// on every Formik state change (e.g. typing in display_name).
|
|
274
|
+
const setMetrics = useCallback(
|
|
275
|
+
v => setFieldValue('metrics', v),
|
|
276
|
+
[setFieldValue],
|
|
277
|
+
);
|
|
278
|
+
const setDimensions = useCallback(
|
|
279
|
+
v => setFieldValue('dimensions', v),
|
|
280
|
+
[setFieldValue],
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Briefly show "Saved" state on the button, then redirect to the cube page
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (status?.success) {
|
|
286
|
+
setJustSaved(true);
|
|
287
|
+
setSaveError(null);
|
|
288
|
+
const savedName = status.savedName;
|
|
289
|
+
const timer = setTimeout(() => {
|
|
290
|
+
if (savedName) {
|
|
291
|
+
navigate(`/nodes/${savedName}`);
|
|
292
|
+
}
|
|
293
|
+
}, 1200);
|
|
294
|
+
return () => clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
if (status?.failure) {
|
|
297
|
+
setSaveError(status.failure);
|
|
298
|
+
setJustSaved(false);
|
|
299
|
+
}
|
|
300
|
+
}, [status, navigate]);
|
|
301
|
+
|
|
198
302
|
return (
|
|
199
|
-
<Form>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
303
|
+
<Form className="cube-builder">
|
|
304
|
+
{/* Header */}
|
|
305
|
+
<div className="cube-builder-header">
|
|
306
|
+
<h2>
|
|
307
|
+
{action === Action.Edit ? 'Edit' : 'Create'}{' '}
|
|
308
|
+
<span className="node_type__cube node_type_creation_heading">
|
|
309
|
+
Cube
|
|
310
|
+
</span>
|
|
311
|
+
</h2>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Two-column layout */}
|
|
315
|
+
<div className="cube-builder-layout">
|
|
316
|
+
{/* Left: Main form */}
|
|
317
|
+
<div className="cube-builder-main">
|
|
318
|
+
{/* Details Section */}
|
|
319
|
+
<div className="cube-form-section">
|
|
320
|
+
<div className="cube-form-section-body">
|
|
321
|
+
{action === Action.Add ? (
|
|
322
|
+
<>
|
|
323
|
+
{/* Add mode uses NodeNameField for namespace/name */}
|
|
324
|
+
<NodeNameField />
|
|
325
|
+
|
|
326
|
+
{/* Row: Description | Mode */}
|
|
327
|
+
<div className="cube-field-row">
|
|
328
|
+
<div className="cube-field cube-field-grow">
|
|
329
|
+
<ErrorMessage
|
|
330
|
+
name="description"
|
|
331
|
+
component="span"
|
|
332
|
+
/>
|
|
333
|
+
<label
|
|
334
|
+
className="cube-field-label"
|
|
335
|
+
htmlFor="Description"
|
|
336
|
+
>
|
|
337
|
+
Description
|
|
338
|
+
</label>
|
|
339
|
+
<DescriptionField />
|
|
340
|
+
</div>
|
|
341
|
+
<div className="cube-field cube-field-small">
|
|
342
|
+
<label
|
|
343
|
+
className="cube-field-label"
|
|
344
|
+
htmlFor="Mode"
|
|
345
|
+
>
|
|
346
|
+
Mode
|
|
347
|
+
</label>
|
|
348
|
+
<Field as="select" name="mode" id="Mode">
|
|
349
|
+
<option value="draft">Draft</option>
|
|
350
|
+
<option value="published">Published</option>
|
|
351
|
+
</Field>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{/* Row: Tags | Owners */}
|
|
356
|
+
<div className="cube-field-row">
|
|
357
|
+
<div className="cube-field cube-field-half">
|
|
358
|
+
<label className="cube-field-label">Tags</label>
|
|
359
|
+
<CubeTagsSelect />
|
|
360
|
+
</div>
|
|
361
|
+
<div className="cube-field cube-field-half">
|
|
362
|
+
<label className="cube-field-label">
|
|
363
|
+
Owners
|
|
364
|
+
</label>
|
|
365
|
+
<CubeOwnersSelect />
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</>
|
|
236
369
|
) : (
|
|
237
|
-
|
|
370
|
+
<>
|
|
371
|
+
{/* Row 1: Name (full width) */}
|
|
372
|
+
<div className="cube-field">
|
|
373
|
+
<label className="cube-field-label">Name</label>
|
|
374
|
+
<div className="cube-field-static">{name}</div>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{/* Row 2: Display Name | Mode */}
|
|
378
|
+
<div className="cube-field-row">
|
|
379
|
+
<div className="cube-field cube-field-grow">
|
|
380
|
+
<ErrorMessage
|
|
381
|
+
name="display_name"
|
|
382
|
+
component="span"
|
|
383
|
+
/>
|
|
384
|
+
<label
|
|
385
|
+
className="cube-field-label"
|
|
386
|
+
htmlFor="displayName"
|
|
387
|
+
>
|
|
388
|
+
Display Name
|
|
389
|
+
</label>
|
|
390
|
+
<FastField
|
|
391
|
+
type="text"
|
|
392
|
+
name="display_name"
|
|
393
|
+
id="displayName"
|
|
394
|
+
placeholder="Human readable name"
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
<div className="cube-field cube-field-small">
|
|
398
|
+
<label
|
|
399
|
+
className="cube-field-label"
|
|
400
|
+
htmlFor="Mode"
|
|
401
|
+
>
|
|
402
|
+
Mode
|
|
403
|
+
</label>
|
|
404
|
+
<Field as="select" name="mode" id="Mode">
|
|
405
|
+
<option value="draft">Draft</option>
|
|
406
|
+
<option value="published">Published</option>
|
|
407
|
+
</Field>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
{/* Row 3: Description (full width) */}
|
|
412
|
+
<div className="cube-field">
|
|
413
|
+
<ErrorMessage
|
|
414
|
+
name="description"
|
|
415
|
+
component="span"
|
|
416
|
+
/>
|
|
417
|
+
<label
|
|
418
|
+
className="cube-field-label"
|
|
419
|
+
htmlFor="Description"
|
|
420
|
+
>
|
|
421
|
+
Description
|
|
422
|
+
</label>
|
|
423
|
+
<DescriptionField />
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
{/* Row 4: Tags | Owners */}
|
|
427
|
+
<div className="cube-field-row">
|
|
428
|
+
<div className="cube-field cube-field-half">
|
|
429
|
+
<label className="cube-field-label">Tags</label>
|
|
430
|
+
<CubeTagsSelect defaultValue={selectTags} />
|
|
431
|
+
</div>
|
|
432
|
+
<div className="cube-field cube-field-half">
|
|
433
|
+
<label className="cube-field-label">
|
|
434
|
+
Owners
|
|
435
|
+
</label>
|
|
436
|
+
<CubeOwnersSelect
|
|
437
|
+
defaultValue={selectOwners}
|
|
438
|
+
isEdit={true}
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</>
|
|
238
443
|
)}
|
|
239
|
-
</
|
|
444
|
+
</div>
|
|
240
445
|
</div>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<div className="
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
446
|
+
|
|
447
|
+
{/* Metrics Section */}
|
|
448
|
+
<div className="cube-form-section">
|
|
449
|
+
<div className="cube-form-section-header">
|
|
450
|
+
<h3>Metrics</h3>
|
|
451
|
+
</div>
|
|
452
|
+
<div className="cube-form-section-body">
|
|
453
|
+
<div data-testid="select-metrics">
|
|
454
|
+
{action === Action.Edit ? (
|
|
455
|
+
<MetricsSelect cube={node} onChange={setMetrics} />
|
|
456
|
+
) : (
|
|
457
|
+
<MetricsSelect onChange={setMetrics} />
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
{/* Dimensions Section */}
|
|
464
|
+
<div className="cube-form-section">
|
|
465
|
+
<div className="cube-form-section-header">
|
|
466
|
+
<h3>Dimensions</h3>
|
|
467
|
+
</div>
|
|
468
|
+
<div className="cube-form-section-body">
|
|
469
|
+
<div data-testid="select-dimensions">
|
|
470
|
+
{action === Action.Edit ? (
|
|
471
|
+
<DimensionsSelect
|
|
472
|
+
cube={node}
|
|
473
|
+
metrics={values.metrics}
|
|
474
|
+
onChange={setDimensions}
|
|
475
|
+
/>
|
|
476
|
+
) : (
|
|
477
|
+
<DimensionsSelect
|
|
478
|
+
metrics={values.metrics}
|
|
479
|
+
onChange={setDimensions}
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* Right: Sidebar */}
|
|
488
|
+
<div className="cube-builder-sidebar">
|
|
489
|
+
<CubePreviewPanel
|
|
490
|
+
metrics={values.metrics}
|
|
491
|
+
dimensions={values.dimensions}
|
|
492
|
+
/>
|
|
493
|
+
|
|
494
|
+
<div className="cube-settings">
|
|
495
|
+
{saveError && (
|
|
496
|
+
<div className="save-error-message">{saveError}</div>
|
|
497
|
+
)}
|
|
498
|
+
<button
|
|
499
|
+
type="submit"
|
|
500
|
+
disabled={isSubmitting || justSaved}
|
|
501
|
+
aria-label="CreateCube"
|
|
502
|
+
className={`save-cube-btn${
|
|
503
|
+
justSaved ? ' save-cube-btn--saved' : ''
|
|
504
|
+
}${isSubmitting ? ' save-cube-btn--loading' : ''}`}
|
|
505
|
+
>
|
|
506
|
+
{isSubmitting ? (
|
|
507
|
+
<>
|
|
508
|
+
<span className="save-spinner" aria-hidden="true" />
|
|
509
|
+
Saving...
|
|
510
|
+
</>
|
|
511
|
+
) : justSaved ? (
|
|
512
|
+
'✓ Saved'
|
|
513
|
+
) : action === Action.Add ? (
|
|
514
|
+
'Create Cube'
|
|
255
515
|
) : (
|
|
256
|
-
|
|
516
|
+
'Save'
|
|
257
517
|
)}
|
|
258
|
-
</
|
|
259
|
-
</div>
|
|
260
|
-
<div className="NodeModeInput NodeCreationInput">
|
|
261
|
-
<ErrorMessage name="mode" component="span" />
|
|
262
|
-
<label htmlFor="Mode">Mode</label>
|
|
263
|
-
<Field as="select" name="mode" id="Mode">
|
|
264
|
-
<option value="draft">Draft</option>
|
|
265
|
-
<option value="published">Published</option>
|
|
266
|
-
</Field>
|
|
518
|
+
</button>
|
|
267
519
|
</div>
|
|
268
|
-
{action === Action.Edit ? selectTags : <TagsField />}
|
|
269
|
-
{action === Action.Edit ? selectOwners : <OwnersField />}
|
|
270
|
-
<button
|
|
271
|
-
type="submit"
|
|
272
|
-
disabled={isSubmitting}
|
|
273
|
-
aria-label="CreateCube"
|
|
274
|
-
>
|
|
275
|
-
{action === Action.Add ? 'Create Cube' : 'Save'}{' '}
|
|
276
|
-
{nodeType}
|
|
277
|
-
</button>
|
|
278
520
|
</div>
|
|
279
521
|
</div>
|
|
280
522
|
</Form>
|