datajunction-ui 0.0.1-a35.dev0 → 0.0.1-a37

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 (25) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/forms/Action.jsx +8 -0
  3. package/src/app/components/forms/NodeNameField.jsx +64 -0
  4. package/src/app/components/forms/NodeTagsInput.jsx +61 -0
  5. package/src/app/index.tsx +18 -0
  6. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +1 -1
  7. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +2 -5
  8. package/src/app/pages/AddEditNodePage/index.jsx +16 -7
  9. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
  10. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  11. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +79 -0
  12. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
  13. package/src/app/pages/CubeBuilderPage/index.jsx +254 -0
  14. package/src/app/pages/NamespacePage/index.jsx +5 -0
  15. package/src/app/pages/NodePage/AddBackfillPopover.jsx +13 -6
  16. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +47 -24
  17. package/src/app/pages/NodePage/NodeInfoTab.jsx +6 -1
  18. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +44 -32
  19. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +0 -1
  20. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +84 -0
  21. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +78 -174
  22. package/src/app/services/DJService.js +66 -12
  23. package/src/app/services/__tests__/DJService.test.jsx +72 -7
  24. package/src/styles/index.css +4 -0
  25. package/src/styles/node-creation.scss +12 -0
@@ -0,0 +1,405 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
+ import DJClientContext from '../../../providers/djclient';
3
+ import { CubeBuilderPage } from '../index';
4
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
5
+ import React from 'react';
6
+
7
+ const mockDjClient = {
8
+ metrics: jest.fn(),
9
+ commonDimensions: jest.fn(),
10
+ createCube: jest.fn(),
11
+ namespaces: jest.fn(),
12
+ cube: jest.fn(),
13
+ node: jest.fn(),
14
+ listTags: jest.fn(),
15
+ tagsNode: jest.fn(),
16
+ patchCube: jest.fn(),
17
+ };
18
+
19
+ const mockMetrics = [
20
+ 'default.num_repair_orders',
21
+ 'default.avg_repair_price',
22
+ 'default.total_repair_cost',
23
+ ];
24
+
25
+ const mockCube = {
26
+ node_revision_id: 102,
27
+ node_id: 33,
28
+ type: 'cube',
29
+ name: 'default.repair_orders_cube',
30
+ display_name: 'Default: Repair Orders Cube',
31
+ version: 'v4.0',
32
+ description: 'Repairs cube',
33
+ availability: null,
34
+ cube_elements: [
35
+ {
36
+ name: 'default_DOT_total_repair_cost',
37
+ display_name: 'Total Repair Cost',
38
+ node_name: 'default.total_repair_cost',
39
+ type: 'metric',
40
+ partition: null,
41
+ },
42
+ {
43
+ name: 'default_DOT_num_repair_orders',
44
+ display_name: 'Num Repair Orders',
45
+ node_name: 'default.num_repair_orders',
46
+ type: 'metric',
47
+ partition: null,
48
+ },
49
+ {
50
+ name: 'country',
51
+ display_name: 'Country',
52
+ node_name: 'default.hard_hat',
53
+ type: 'dimension',
54
+ partition: null,
55
+ },
56
+ {
57
+ name: 'state',
58
+ display_name: 'State',
59
+ node_name: 'default.hard_hat',
60
+ type: 'dimension',
61
+ partition: null,
62
+ },
63
+ ],
64
+ query: '',
65
+ columns: [
66
+ {
67
+ name: 'default.total_repair_cost',
68
+ display_name: 'Total Repair Cost',
69
+ type: 'double',
70
+ attributes: [],
71
+ dimension: null,
72
+ partition: null,
73
+ },
74
+ {
75
+ name: 'default.num_repair_orders',
76
+ display_name: 'Num Repair Orders',
77
+ type: 'bigint',
78
+ attributes: [],
79
+ dimension: null,
80
+ partition: null,
81
+ },
82
+ {
83
+ name: 'default.hard_hat.country',
84
+ display_name: 'Country',
85
+ type: 'string',
86
+ attributes: [],
87
+ dimension: null,
88
+ partition: null,
89
+ },
90
+ {
91
+ name: 'default.hard_hat.state',
92
+ display_name: 'State',
93
+ type: 'string',
94
+ attributes: [],
95
+ dimension: null,
96
+ partition: null,
97
+ },
98
+ ],
99
+ updated_at: '2023-12-03T06:51:09.598532+00:00',
100
+ materializations: [],
101
+ };
102
+
103
+ const mockCommonDimensions = [
104
+ {
105
+ name: 'default.date_dim.dateint',
106
+ type: 'timestamp',
107
+ node_name: 'default.date_dim',
108
+ node_display_name: 'Date',
109
+ is_primary_key: false,
110
+ path: [
111
+ 'default.repair_order_details.repair_order_id',
112
+ 'default.repair_order.hard_hat_id',
113
+ 'default.hard_hat.birth_date',
114
+ ],
115
+ },
116
+ {
117
+ name: 'default.date_dim.dateint',
118
+ type: 'timestamp',
119
+ node_name: 'default.date_dim',
120
+ node_display_name: 'Date',
121
+ is_primary_key: true,
122
+ path: [
123
+ 'default.repair_order_details.repair_order_id',
124
+ 'default.repair_order.hard_hat_id',
125
+ 'default.hard_hat.hire_date',
126
+ ],
127
+ },
128
+ {
129
+ name: 'default.date_dim.day',
130
+ type: 'int',
131
+ node_name: 'default.date_dim',
132
+ node_display_name: 'Date',
133
+ is_primary_key: false,
134
+ path: [
135
+ 'default.repair_order_details.repair_order_id',
136
+ 'default.repair_order.hard_hat_id',
137
+ 'default.hard_hat.birth_date',
138
+ ],
139
+ },
140
+ {
141
+ name: 'default.date_dim.day',
142
+ type: 'int',
143
+ node_name: 'default.date_dim',
144
+ node_display_name: 'Date',
145
+ is_primary_key: false,
146
+ path: [
147
+ 'default.repair_order_details.repair_order_id',
148
+ 'default.repair_order.hard_hat_id',
149
+ 'default.hard_hat.hire_date',
150
+ ],
151
+ },
152
+ {
153
+ name: 'default.date_dim.month',
154
+ type: 'int',
155
+ node_name: 'default.date_dim',
156
+ node_display_name: 'Date',
157
+ is_primary_key: false,
158
+ path: [
159
+ 'default.repair_order_details.repair_order_id',
160
+ 'default.repair_order.hard_hat_id',
161
+ 'default.hard_hat.birth_date',
162
+ ],
163
+ },
164
+ {
165
+ name: 'default.date_dim.month',
166
+ type: 'int',
167
+ node_name: 'default.date_dim',
168
+ node_display_name: 'Date',
169
+ is_primary_key: false,
170
+ path: [
171
+ 'default.repair_order_details.repair_order_id',
172
+ 'default.repair_order.hard_hat_id',
173
+ 'default.hard_hat.hire_date',
174
+ ],
175
+ },
176
+ {
177
+ name: 'default.date_dim.year',
178
+ type: 'int',
179
+ node_name: 'default.date_dim',
180
+ node_display_name: 'Date',
181
+ is_primary_key: false,
182
+ path: [
183
+ 'default.repair_order_details.repair_order_id',
184
+ 'default.repair_order.hard_hat_id',
185
+ 'default.hard_hat.birth_date',
186
+ ],
187
+ },
188
+ {
189
+ name: 'default.date_dim.year',
190
+ type: 'int',
191
+ node_name: 'default.date_dim',
192
+ node_display_name: 'Date',
193
+ is_primary_key: false,
194
+ path: [
195
+ 'default.repair_order_details.repair_order_id',
196
+ 'default.repair_order.hard_hat_id',
197
+ 'default.hard_hat.hire_date',
198
+ ],
199
+ },
200
+ ];
201
+
202
+ describe('CubeBuilderPage', () => {
203
+ beforeEach(() => {
204
+ mockDjClient.metrics.mockResolvedValue(mockMetrics);
205
+ mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
206
+ mockDjClient.createCube.mockResolvedValue({ status: 201, json: {} });
207
+ mockDjClient.namespaces.mockResolvedValue(['default']);
208
+ mockDjClient.cube.mockResolvedValue(mockCube);
209
+ mockDjClient.node.mockResolvedValue(mockCube);
210
+ mockDjClient.listTags.mockResolvedValue([]);
211
+ mockDjClient.tagsNode.mockResolvedValue([]);
212
+ mockDjClient.patchCube.mockResolvedValue({ status: 201, json: {} });
213
+
214
+ window.scrollTo = jest.fn();
215
+ });
216
+
217
+ afterEach(() => {
218
+ jest.clearAllMocks();
219
+ });
220
+
221
+ it('renders without crashing', () => {
222
+ render(
223
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
224
+ <CubeBuilderPage />
225
+ </DJClientContext.Provider>,
226
+ );
227
+ expect(screen.getByText('Cube')).toBeInTheDocument();
228
+ });
229
+
230
+ it('renders the Metrics section', () => {
231
+ render(
232
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
233
+ <CubeBuilderPage />
234
+ </DJClientContext.Provider>,
235
+ );
236
+ expect(screen.getByText('Metrics *')).toBeInTheDocument();
237
+ });
238
+
239
+ it('renders the Dimensions section', () => {
240
+ render(
241
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
242
+ <CubeBuilderPage />
243
+ </DJClientContext.Provider>,
244
+ );
245
+ expect(screen.getByText('Dimensions *')).toBeInTheDocument();
246
+ });
247
+
248
+ it('creates a new cube', async () => {
249
+ render(
250
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
251
+ <CubeBuilderPage />
252
+ </DJClientContext.Provider>,
253
+ );
254
+
255
+ await waitFor(() => {
256
+ expect(mockDjClient.metrics).toHaveBeenCalled();
257
+ });
258
+
259
+ const selectMetrics = screen.getAllByTestId('select-metrics')[0];
260
+ expect(selectMetrics).toBeDefined();
261
+ expect(selectMetrics).not.toBeNull();
262
+ expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument();
263
+
264
+ fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' });
265
+ for (const metric of mockMetrics) {
266
+ await waitFor(() => {
267
+ expect(screen.getByText(metric)).toBeInTheDocument();
268
+ fireEvent.click(screen.getByText(metric));
269
+ });
270
+ }
271
+ fireEvent.click(screen.getAllByText('Dimensions *')[0]);
272
+
273
+ expect(mockDjClient.commonDimensions).toHaveBeenCalled();
274
+
275
+ const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
276
+ expect(selectDimensions).toBeDefined();
277
+ expect(selectDimensions).not.toBeNull();
278
+ expect(
279
+ screen.getByText(
280
+ 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
281
+ ),
282
+ ).toBeInTheDocument();
283
+
284
+ const selectDimensionsDate = screen.getAllByTestId(
285
+ 'dimensions-default.date_dim',
286
+ )[0];
287
+
288
+ fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
289
+ fireEvent.click(screen.getByText('Day'));
290
+ fireEvent.click(screen.getByText('Month'));
291
+ fireEvent.click(screen.getByText('Year'));
292
+ fireEvent.click(screen.getByText('Dateint'));
293
+
294
+ // Save
295
+ const createCube = screen.getAllByRole('button', {
296
+ name: 'CreateCube',
297
+ })[0];
298
+ expect(createCube).toBeInTheDocument();
299
+
300
+ await waitFor(() => {
301
+ fireEvent.click(createCube);
302
+ });
303
+ await waitFor(() => {
304
+ expect(mockDjClient.createCube).toHaveBeenCalledWith(
305
+ '',
306
+ '',
307
+ '',
308
+ 'draft',
309
+ [
310
+ 'default.num_repair_orders',
311
+ 'default.avg_repair_price',
312
+ 'default.total_repair_cost',
313
+ ],
314
+ [
315
+ 'default.date_dim.day',
316
+ 'default.date_dim.month',
317
+ 'default.date_dim.year',
318
+ 'default.date_dim.dateint',
319
+ ],
320
+ [],
321
+ );
322
+ });
323
+ });
324
+
325
+ const renderEditNode = element => {
326
+ return render(
327
+ <MemoryRouter
328
+ initialEntries={['/nodes/default.repair_orders_cube/edit-cube']}
329
+ >
330
+ <Routes>
331
+ <Route path="nodes/:name/edit-cube" element={element} />
332
+ </Routes>
333
+ </MemoryRouter>,
334
+ );
335
+ };
336
+
337
+ it('updates an existing cube', async () => {
338
+ renderEditNode(
339
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
340
+ <CubeBuilderPage />
341
+ </DJClientContext.Provider>,
342
+ );
343
+ expect(screen.getAllByText('Edit')[0]).toBeInTheDocument();
344
+ await waitFor(() => {
345
+ expect(mockDjClient.cube).toHaveBeenCalled();
346
+ });
347
+ await waitFor(() => {
348
+ expect(mockDjClient.metrics).toHaveBeenCalled();
349
+ });
350
+
351
+ const selectMetrics = screen.getAllByTestId('select-metrics')[0];
352
+ expect(selectMetrics).toBeDefined();
353
+ expect(selectMetrics).not.toBeNull();
354
+ expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
355
+
356
+ fireEvent.click(screen.getAllByText('Dimensions *')[0]);
357
+
358
+ expect(mockDjClient.commonDimensions).toHaveBeenCalled();
359
+
360
+ const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
361
+ expect(selectDimensions).toBeDefined();
362
+ expect(selectDimensions).not.toBeNull();
363
+ expect(
364
+ screen.getByText(
365
+ 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
366
+ ),
367
+ ).toBeInTheDocument();
368
+
369
+ const selectDimensionsDate = screen.getAllByTestId(
370
+ 'dimensions-default.date_dim',
371
+ )[0];
372
+
373
+ fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
374
+ fireEvent.click(screen.getByText('Day'));
375
+ fireEvent.click(screen.getByText('Month'));
376
+ fireEvent.click(screen.getByText('Year'));
377
+ fireEvent.click(screen.getByText('Dateint'));
378
+
379
+ // Save
380
+ const createCube = screen.getAllByRole('button', {
381
+ name: 'CreateCube',
382
+ })[0];
383
+ expect(createCube).toBeInTheDocument();
384
+
385
+ await waitFor(() => {
386
+ fireEvent.click(createCube);
387
+ });
388
+ await waitFor(() => {
389
+ expect(mockDjClient.patchCube).toHaveBeenCalledWith(
390
+ 'default.repair_orders_cube',
391
+ 'Default: Repair Orders Cube',
392
+ 'Repairs cube',
393
+ 'draft',
394
+ ['default.total_repair_cost', 'default.num_repair_orders'],
395
+ [
396
+ 'default.date_dim.day',
397
+ 'default.date_dim.month',
398
+ 'default.date_dim.year',
399
+ 'default.date_dim.dateint',
400
+ ],
401
+ [],
402
+ );
403
+ });
404
+ });
405
+ });
@@ -0,0 +1,254 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
2
+ import NamespaceHeader from '../../components/NamespaceHeader';
3
+ import { DataJunctionAPI } from '../../services/DJService';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import 'react-querybuilder/dist/query-builder.scss';
6
+ import 'styles/styles.scss';
7
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
8
+ import { displayMessageAfterSubmit } from '../../../utils/form';
9
+ import { useParams } from 'react-router-dom';
10
+ import { Action } from '../../components/forms/Action';
11
+ import NodeNameField from '../../components/forms/NodeNameField';
12
+ import NodeTagsInput from '../../components/forms/NodeTagsInput';
13
+ import { MetricsSelect } from './MetricsSelect';
14
+ import { DimensionsSelect } from './DimensionsSelect';
15
+
16
+ export function CubeBuilderPage() {
17
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
18
+
19
+ let { nodeType, initialNamespace, name } = useParams();
20
+ const action = name !== undefined ? Action.Edit : Action.Add;
21
+ const validator = ruleType => !!ruleType.value;
22
+
23
+ const initialValues = {
24
+ name: action === Action.Edit ? name : '',
25
+ namespace: action === Action.Add ? initialNamespace : '',
26
+ display_name: '',
27
+ description: '',
28
+ mode: 'draft',
29
+ metrics: [],
30
+ dimensions: [],
31
+ filters: [],
32
+ };
33
+
34
+ const handleSubmit = (values, { setSubmitting, setStatus }) => {
35
+ if (action === Action.Add) {
36
+ setTimeout(() => {
37
+ createNode(values, setStatus);
38
+ setSubmitting(false);
39
+ }, 400);
40
+ } else {
41
+ setTimeout(() => {
42
+ patchNode(values, setStatus);
43
+ setSubmitting(false);
44
+ }, 400);
45
+ }
46
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
47
+ };
48
+
49
+ const createNode = async (values, setStatus) => {
50
+ const { status, json } = await djClient.createCube(
51
+ values.name,
52
+ values.display_name,
53
+ values.description,
54
+ values.mode,
55
+ values.metrics,
56
+ values.dimensions,
57
+ values.filters || [],
58
+ );
59
+ if (status === 200 || status === 201) {
60
+ if (values.tags) {
61
+ await djClient.tagsNode(values.name, values.tags);
62
+ }
63
+ setStatus({
64
+ success: (
65
+ <>
66
+ Successfully created {json.type} node{' '}
67
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
68
+ </>
69
+ ),
70
+ });
71
+ } else {
72
+ setStatus({
73
+ failure: `${json.message}`,
74
+ });
75
+ }
76
+ };
77
+
78
+ const patchNode = async (values, setStatus) => {
79
+ const { status, json } = await djClient.patchCube(
80
+ values.name,
81
+ values.display_name,
82
+ values.description,
83
+ values.mode,
84
+ values.metrics,
85
+ values.dimensions,
86
+ values.filters || [],
87
+ );
88
+ const tagsResponse = await djClient.tagsNode(
89
+ values.name,
90
+ (values.tags || []).map(tag => tag),
91
+ );
92
+ if ((status === 200 || status === 201) && tagsResponse.status === 200) {
93
+ setStatus({
94
+ success: (
95
+ <>
96
+ Successfully updated {json.type} node{' '}
97
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
98
+ </>
99
+ ),
100
+ });
101
+ } else {
102
+ setStatus({
103
+ failure: `${json.message}`,
104
+ });
105
+ }
106
+ };
107
+
108
+ const updateFieldsWithNodeData = (data, setFieldValue) => {
109
+ setFieldValue('display_name', data.display_name || '', false);
110
+ setFieldValue('description', data.description || '', false);
111
+ setFieldValue('mode', data.mode || 'draft', false);
112
+ };
113
+
114
+ const staticFieldsInEdit = () => (
115
+ <>
116
+ <div className="NodeNameInput NodeCreationInput">
117
+ <label htmlFor="name">Name</label> {name}
118
+ </div>
119
+ <div className="NodeNameInput NodeCreationInput">
120
+ <label htmlFor="name">Type</label> cube
121
+ </div>
122
+ <div className="DisplayNameInput NodeCreationInput">
123
+ <ErrorMessage name="display_name" component="span" />
124
+ <label htmlFor="displayName">Display Name</label>
125
+ <Field
126
+ type="text"
127
+ name="display_name"
128
+ id="displayName"
129
+ placeholder="Human readable display name"
130
+ />
131
+ </div>
132
+ </>
133
+ );
134
+
135
+ // @ts-ignore
136
+ return (
137
+ <>
138
+ <div className="mid">
139
+ <NamespaceHeader namespace="" />
140
+ <Formik
141
+ initialValues={initialValues}
142
+ validate={validator}
143
+ onSubmit={handleSubmit}
144
+ >
145
+ {function Render({ isSubmitting, status, setFieldValue, props }) {
146
+ const [node, setNode] = useState([]);
147
+
148
+ // Get cube
149
+ useEffect(() => {
150
+ const fetchData = async () => {
151
+ if (name) {
152
+ const node = await djClient.node(name);
153
+ const cube = await djClient.cube(name);
154
+ cube.tags = node.tags;
155
+ setNode(cube);
156
+ updateFieldsWithNodeData(cube, setFieldValue);
157
+ }
158
+ };
159
+ fetchData().catch(console.error);
160
+ }, [djClient, djClient.metrics, name]);
161
+
162
+ return (
163
+ <Form>
164
+ <div className="card">
165
+ <div className="card-header">
166
+ <h2>
167
+ {action === Action.Edit ? 'Edit' : 'Create'}{' '}
168
+ <span
169
+ className={`node_type__cube node_type_creation_heading`}
170
+ >
171
+ Cube
172
+ </span>
173
+ </h2>
174
+ {displayMessageAfterSubmit(status)}
175
+ {action === Action.Add ? (
176
+ <NodeNameField />
177
+ ) : (
178
+ staticFieldsInEdit(node)
179
+ )}
180
+ <div className="DescriptionInput NodeCreationInput">
181
+ <ErrorMessage name="description" component="span" />
182
+ <label htmlFor="Description">Description</label>
183
+ <Field
184
+ type="textarea"
185
+ as="textarea"
186
+ name="description"
187
+ id="Description"
188
+ placeholder="Describe your node"
189
+ />
190
+ </div>
191
+ <div className="CubeCreationInput">
192
+ <label htmlFor="react-select-3-input">Metrics *</label>
193
+ <p>Select metrics to include in the cube.</p>
194
+ <span
195
+ data-testid="select-metrics"
196
+ style={{ marginTop: '15px' }}
197
+ >
198
+ {action === Action.Edit ? (
199
+ <MetricsSelect cube={node} />
200
+ ) : (
201
+ <MetricsSelect />
202
+ )}
203
+ </span>
204
+ </div>
205
+ <br />
206
+ <br />
207
+ <div className="CubeCreationInput">
208
+ <label htmlFor="react-select-3-input">Dimensions *</label>
209
+ <p>
210
+ Select dimensions to include in the cube. As metrics are
211
+ selected above, the list of available dimensions will be
212
+ filtered to those shared by the selected metrics. If the
213
+ dimensions list is empty, no shared dimensions were
214
+ discovered.
215
+ </p>
216
+ <span data-testid="select-dimensions">
217
+ {action === Action.Edit ? (
218
+ <DimensionsSelect cube={node} />
219
+ ) : (
220
+ <DimensionsSelect />
221
+ )}
222
+ </span>
223
+ </div>
224
+ <div className="NodeModeInput NodeCreationInput">
225
+ <ErrorMessage name="mode" component="span" />
226
+ <label htmlFor="Mode">Mode</label>
227
+ <Field as="select" name="mode" id="Mode">
228
+ <option value="draft">Draft</option>
229
+ <option value="published">Published</option>
230
+ </Field>
231
+ </div>
232
+ <NodeTagsInput action={action} node={node} />
233
+ <button
234
+ type="submit"
235
+ disabled={isSubmitting}
236
+ aria-label="CreateCube"
237
+ >
238
+ {action === Action.Add ? 'Create Cube' : 'Save'}{' '}
239
+ {nodeType}
240
+ </button>
241
+ </div>
242
+ </div>
243
+ </Form>
244
+ );
245
+ }}
246
+ </Formik>
247
+ </div>
248
+ </>
249
+ );
250
+ }
251
+
252
+ CubeBuilderPage.defaultProps = {
253
+ djClient: DataJunctionAPI,
254
+ };
@@ -150,6 +150,11 @@ export function NamespacePage() {
150
150
  Tag
151
151
  </div>
152
152
  </a>
153
+ <a href={`/create/cube/${namespace}`}>
154
+ <div className="node_type__cube node_type_creation_heading">
155
+ Cube
156
+ </div>
157
+ </a>
153
158
  </div>
154
159
  </div>
155
160
  </span>
@@ -97,13 +97,20 @@ export default function AddBackfillPopover({
97
97
  {displayMessageAfterSubmit(status)}
98
98
  <h2>Run Backfill</h2>
99
99
  <span data-testid="edit-partition">
100
- <label htmlFor="engine" style={{ paddingBottom: '1rem' }}>
101
- Engine
100
+ <label
101
+ htmlFor="materializationName"
102
+ style={{ paddingBottom: '1rem' }}
103
+ >
104
+ Materialization Name
102
105
  </label>
103
- <Field as="select" name="engine" id="engine" disabled={true}>
104
- <option value={materialization?.engine?.name}>
105
- {materialization?.engine?.name}{' '}
106
- {materialization?.engine?.version}
106
+ <Field
107
+ as="select"
108
+ name="materializationName"
109
+ id="materializationName"
110
+ disabled={true}
111
+ >
112
+ <option value={materialization?.name}>
113
+ {materialization?.name}{' '}
107
114
  </option>
108
115
  </Field>
109
116
  </span>