datajunction-ui 0.0.1-rc.24 → 0.0.1-rc.26

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 (44) hide show
  1. package/.env +1 -0
  2. package/package.json +3 -2
  3. package/src/app/components/Tab.jsx +0 -1
  4. package/src/app/constants.js +2 -2
  5. package/src/app/icons/LoadingIcon.jsx +14 -0
  6. package/src/app/index.tsx +11 -1
  7. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +28 -2
  8. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +44 -9
  9. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +1 -0
  10. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +0 -50
  11. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +2 -0
  12. package/src/app/pages/AddEditNodePage/index.jsx +60 -6
  13. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  14. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  15. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  16. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  17. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  18. package/src/app/pages/LoginPage/__tests__/index.test.jsx +34 -2
  19. package/src/app/pages/LoginPage/index.jsx +9 -82
  20. package/src/app/pages/NamespacePage/index.jsx +5 -0
  21. package/src/app/pages/NodePage/AddBackfillPopover.jsx +166 -0
  22. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +161 -0
  23. package/src/app/pages/NodePage/EditColumnPopover.jsx +1 -1
  24. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +0 -1
  25. package/src/app/pages/NodePage/NodeColumnTab.jsx +102 -25
  26. package/src/app/pages/NodePage/NodeInfoTab.jsx +33 -23
  27. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +158 -99
  28. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +153 -0
  29. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +47 -0
  30. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +47 -17
  31. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +101 -100
  32. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +1 -0
  33. package/src/app/pages/Root/index.tsx +1 -1
  34. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  35. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  36. package/src/app/pages/TagPage/index.jsx +79 -0
  37. package/src/app/services/DJService.js +166 -1
  38. package/src/app/services/__tests__/DJService.test.jsx +196 -1
  39. package/src/mocks/mockNodes.jsx +64 -31
  40. package/src/styles/dag.css +4 -0
  41. package/src/styles/index.css +89 -1
  42. package/src/styles/loading.css +34 -0
  43. package/src/styles/login.css +17 -3
  44. package/src/utils/form.jsx +2 -2
@@ -1,6 +1,9 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import ClientCodePopover from './ClientCodePopover';
3
3
  import TableIcon from '../../icons/TableIcon';
4
+ import AddMaterializationPopover from './AddMaterializationPopover';
5
+ import * as React from 'react';
6
+ import AddBackfillPopover from './AddBackfillPopover';
4
7
 
5
8
  const cronstrue = require('cronstrue');
6
9
 
@@ -15,18 +18,25 @@ export default function NodeMaterializationTab({ node, djClient }) {
15
18
  };
16
19
  fetchData().catch(console.error);
17
20
  }, [djClient, node]);
21
+ //
22
+ // const rangePartition = partition => {
23
+ // return (
24
+ // <div>
25
+ // <span className="badge partition_value">
26
+ // <span className="badge partition_value">{partition.range[0]}</span>to
27
+ // <span className="badge partition_value">{partition.range[1]}</span>
28
+ // </span>
29
+ // </div>
30
+ // );
31
+ // };
18
32
 
19
- const rangePartition = partition => {
20
- return (
21
- <div>
22
- <span className="badge partition_value">
23
- <span className="badge partition_value">{partition.range[0]}</span>to
24
- <span className="badge partition_value">{partition.range[1]}</span>
25
- </span>
26
- </div>
27
- );
28
- };
29
-
33
+ const partitionColumnsMap = node
34
+ ? Object.fromEntries(
35
+ node?.columns
36
+ .filter(col => col.partition !== null)
37
+ .map(col => [col.name, col.display_name]),
38
+ )
39
+ : {};
30
40
  const cron = materialization => {
31
41
  var parsedCron = '';
32
42
  try {
@@ -39,10 +49,6 @@ export default function NodeMaterializationTab({ node, djClient }) {
39
49
  return materializations.map(materialization => (
40
50
  <tr key={materialization.name}>
41
51
  <td className="text-start node_name">
42
- <a href={materialization.urls[0]}>{materialization.name}</a>
43
- <ClientCodePopover code={materialization.clientCode} />
44
- </td>
45
- <td>
46
52
  <span className={`badge cron`}>{materialization.schedule}</span>
47
53
  <div className={`cron-description`}>{cron(materialization)} </div>
48
54
  </td>
@@ -50,41 +56,24 @@ export default function NodeMaterializationTab({ node, djClient }) {
50
56
  {materialization.engine.name}
51
57
  <br />
52
58
  {materialization.engine.version}
59
+ <ClientCodePopover code={materialization.clientCode} />
53
60
  </td>
54
61
  <td>
55
- {materialization.config.partitions ? (
56
- materialization.config.partitions.map(partition =>
57
- partition.type_ === 'categorical' ? (
58
- <div className="partition__full" key={partition.name}>
59
- <div className="partition__header">{partition.name}</div>
62
+ {node.columns
63
+ .filter(col => col.partition !== null)
64
+ .map(column => {
65
+ return (
66
+ <div className="partition__full" key={column.name}>
67
+ <div className="partition__header">{column.display_name}</div>
60
68
  <div className="partition__body">
61
- {partition.values !== null && partition.values.length > 0
62
- ? partition.values.map(val => (
63
- <span
64
- className="badge partition_value"
65
- key={`partition-value-${val}`}
66
- >
67
- {val}
68
- </span>
69
- ))
70
- : null}
71
- {partition.range !== null && partition.range.length > 0
72
- ? rangePartition(partition)
73
- : null}
74
- {(partition.range === null && partition.values === null) ||
75
- (partition.range?.length === 0 &&
76
- partition.values?.length === 0) ? (
77
- <span className={`badge partition_value_highlight`}>
78
- ALL
79
- </span>
80
- ) : null}
69
+ <code>{column.name}</code>
70
+ <span className="badge partition_value">
71
+ {column.partition.type_}
72
+ </span>
81
73
  </div>
82
74
  </div>
83
- ) : null,
84
- )
85
- ) : (
86
- <br />
87
- )}
75
+ );
76
+ })}
88
77
  </td>
89
78
  <td>
90
79
  {materialization.output_tables.map(table => (
@@ -101,30 +90,26 @@ export default function NodeMaterializationTab({ node, djClient }) {
101
90
  </div>
102
91
  ))}
103
92
  </td>
104
- {/*<td>{Object.keys(materialization.config.spark).map(key => <li className={`list-group-item`}>{key}: {materialization.config.spark[key]}</li>)}</td>*/}
105
-
106
93
  <td>
107
- {materialization.config.partitions ? (
108
- materialization.config.partitions.map(partition =>
109
- partition.type_ === 'temporal' ? (
110
- <div className="partition__full" key={partition.name}>
111
- <div className="partition__header">{partition.name}</div>
112
- <div className="partition__body">
113
- {partition.values !== null && partition.values.length > 0
114
- ? partition.values.map(val => (
115
- <span className="badge partition_value">{val}</span>
116
- ))
117
- : null}
118
- {partition.range !== null && partition.range.length > 0
119
- ? rangePartition(partition)
120
- : null}
121
- </div>
94
+ {materialization.backfills.map(backfill => (
95
+ <a href={backfill.urls[0]} className="partitionLink">
96
+ <div className="partition__full" key={backfill.spec.column_name}>
97
+ <div className="partition__header">
98
+ {partitionColumnsMap[backfill.spec.column_name]}
122
99
  </div>
123
- ) : null,
124
- )
125
- ) : (
126
- <br />
127
- )}
100
+ <div className="partition__body">
101
+ <span className="badge partition_value">
102
+ {backfill.spec.range[0]}
103
+ </span>
104
+ to
105
+ <span className="badge partition_value">
106
+ {backfill.spec.range[1]}
107
+ </span>
108
+ </div>
109
+ </div>
110
+ </a>
111
+ ))}
112
+ <AddBackfillPopover node={node} materialization={materialization} />
128
113
  </td>
129
114
  <td>
130
115
  {materialization.urls.map((url, idx) => (
@@ -137,38 +122,112 @@ export default function NodeMaterializationTab({ node, djClient }) {
137
122
  ));
138
123
  };
139
124
  return (
140
- <div className="table-responsive">
141
- {materializations.length > 0 ? (
142
- <table
143
- className="card-inner-table table"
144
- aria-label="Materializations"
145
- aria-hidden="false"
146
- >
147
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
148
- <tr>
149
- <th className="text-start">Name</th>
150
- <th>Schedule</th>
151
- <th>Engine</th>
152
- <th>Partitions</th>
153
- <th>Output Tables</th>
154
- <th>Backfills</th>
155
- <th>URLs</th>
156
- </tr>
157
- </thead>
158
- <tbody>
159
- {materializationRows(
160
- materializations.filter(
161
- materialization =>
162
- !(materialization.name === 'default' && node.type === 'cube'),
163
- ),
164
- )}
165
- </tbody>
166
- </table>
167
- ) : (
168
- <div className="message alert" style={{ marginTop: '10px' }}>
169
- No materializations available for this node
125
+ <>
126
+ <div className="table-vertical">
127
+ <div>
128
+ <h2>Materializations</h2>
129
+ <AddMaterializationPopover node={node} />
130
+ {materializations.length > 0 ? (
131
+ <table
132
+ className="card-inner-table table"
133
+ aria-label="Materializations"
134
+ aria-hidden="false"
135
+ >
136
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
137
+ <tr>
138
+ <th className="text-start">Schedule</th>
139
+ <th>Engine</th>
140
+ <th>Partitions</th>
141
+ <th>Output Tables</th>
142
+ <th>Backfills</th>
143
+ <th>URLs</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ {materializationRows(
148
+ materializations.filter(
149
+ materialization =>
150
+ !(
151
+ materialization.name === 'default' &&
152
+ node.type === 'cube'
153
+ ),
154
+ ),
155
+ )}
156
+ </tbody>
157
+ </table>
158
+ ) : (
159
+ <div className="message alert" style={{ marginTop: '10px' }}>
160
+ No materialization workflows configured for this node.
161
+ </div>
162
+ )}
170
163
  </div>
171
- )}
172
- </div>
164
+ <div>
165
+ <h2>Materialized Datasets</h2>
166
+ {node && node.availability !== null ? (
167
+ <table
168
+ className="card-inner-table table"
169
+ aria-label="Availability"
170
+ aria-hidden="false"
171
+ >
172
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
173
+ <tr>
174
+ <th className="text-start">Catalog</th>
175
+ <th>Schema</th>
176
+ <th>Table</th>
177
+ <th>Valid Through</th>
178
+ <th>Partitions</th>
179
+ </tr>
180
+ </thead>
181
+ <tbody>
182
+ <tr>
183
+ <td>{node.availability.schema_}</td>
184
+ <td>
185
+ {
186
+ <div
187
+ className={`table__full`}
188
+ key={node.availability.table}
189
+ >
190
+ <div className="table__header">
191
+ <TableIcon />{' '}
192
+ <span className={`entity-info`}>
193
+ {node.availability.catalog +
194
+ '.' +
195
+ node.availability.schema_}
196
+ </span>
197
+ </div>
198
+ <div className={`table__body upstream_tables`}>
199
+ <a href={node.availability.url}>
200
+ {node.availability.table}
201
+ </a>
202
+ </div>
203
+ </div>
204
+ }
205
+ </td>
206
+ <td>{node.availability.valid_through_ts}</td>
207
+ <td>
208
+ <span
209
+ className={`badge partition_value`}
210
+ style={{ fontSize: '100%' }}
211
+ >
212
+ <span className={`badge partition_value_highlight`}>
213
+ {node.availability.min_temporal_partition}
214
+ </span>
215
+ to
216
+ <span className={`badge partition_value_highlight`}>
217
+ {node.availability.max_temporal_partition}
218
+ </span>
219
+ </span>
220
+ </td>
221
+ </tr>
222
+ </tbody>
223
+ </table>
224
+ ) : (
225
+ <div className="message alert" style={{ marginTop: '10px' }}>
226
+ No materialized datasets available for this node.
227
+ </div>
228
+ )}
229
+ </div>
230
+ </div>
231
+ </>
173
232
  );
174
233
  }
@@ -0,0 +1,153 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { Field, Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import EditIcon from '../../icons/EditIcon';
7
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
8
+
9
+ export default function PartitionColumnPopover({ column, node, onSubmit }) {
10
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
11
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
12
+ const ref = useRef(null);
13
+
14
+ useEffect(() => {
15
+ const handleClickOutside = event => {
16
+ if (ref.current && !ref.current.contains(event.target)) {
17
+ setPopoverAnchor(false);
18
+ }
19
+ };
20
+ document.addEventListener('click', handleClickOutside, true);
21
+ return () => {
22
+ document.removeEventListener('click', handleClickOutside, true);
23
+ };
24
+ }, [setPopoverAnchor]);
25
+
26
+ const savePartition = async (
27
+ { node, column, partition_type, format, granularity },
28
+ { setSubmitting, setStatus },
29
+ ) => {
30
+ setSubmitting(false);
31
+ const response = await djClient.setPartition(
32
+ node,
33
+ column,
34
+ partition_type,
35
+ format,
36
+ granularity,
37
+ );
38
+ if (response.status === 200 || response.status === 201) {
39
+ setStatus({ success: 'Saved!' });
40
+ } else {
41
+ setStatus({
42
+ failure: `${response.json.message}`,
43
+ });
44
+ }
45
+ onSubmit();
46
+ // window.location.reload();
47
+ };
48
+
49
+ return (
50
+ <>
51
+ <button
52
+ className="edit_button"
53
+ aria-label="PartitionColumn"
54
+ tabIndex="0"
55
+ onClick={() => {
56
+ setPopoverAnchor(!popoverAnchor);
57
+ }}
58
+ >
59
+ <EditIcon />
60
+ </button>
61
+ <div
62
+ className="popover"
63
+ role="dialog"
64
+ aria-label="client-code"
65
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
66
+ ref={ref}
67
+ >
68
+ <Formik
69
+ initialValues={{
70
+ column: column.name,
71
+ node: node.name,
72
+ partition_type: '',
73
+ format: 'yyyyMMdd',
74
+ granularity: 'day',
75
+ }}
76
+ onSubmit={savePartition}
77
+ >
78
+ {function Render({ values, isSubmitting, status, setFieldValue }) {
79
+ return (
80
+ <Form>
81
+ {displayMessageAfterSubmit(status)}
82
+ <span data-testid="edit-partition">
83
+ <label htmlFor="react-select-3-input">Partition Type</label>
84
+ <Field
85
+ as="select"
86
+ name="partition_type"
87
+ id="partitionType"
88
+ placeholder="Partition Type"
89
+ >
90
+ <option value=""></option>
91
+ <option value="temporal">Temporal</option>
92
+ <option value="categorical">Categorical</option>
93
+ </Field>
94
+ </span>
95
+ <input
96
+ hidden={true}
97
+ name="column"
98
+ value={column.name}
99
+ readOnly={true}
100
+ />
101
+ <input
102
+ hidden={true}
103
+ name="node"
104
+ value={node.name}
105
+ readOnly={true}
106
+ />
107
+ <br />
108
+ <br />
109
+ {values.partition_type === 'temporal' ? (
110
+ <>
111
+ <label htmlFor="react-select-3-input">
112
+ Partition Format
113
+ </label>
114
+ <Field
115
+ type="text"
116
+ name="format"
117
+ id="partitionFormat"
118
+ placeholder="Optional temporal partition format (ex: yyyyMMdd)"
119
+ />
120
+ <br />
121
+ <br />
122
+ <label htmlFor="react-select-3-input">
123
+ Partition Granularity
124
+ </label>
125
+ <Field
126
+ as="select"
127
+ name="granularity"
128
+ id="partitionGranularity"
129
+ placeholder="Granularity"
130
+ >
131
+ <option value="day">Day</option>
132
+ <option value="hour">Hour</option>
133
+ </Field>
134
+ </>
135
+ ) : (
136
+ ''
137
+ )}
138
+ <button
139
+ className="add_node"
140
+ type="submit"
141
+ aria-label="SaveEditColumn"
142
+ aria-hidden="false"
143
+ >
144
+ Save
145
+ </button>
146
+ </Form>
147
+ );
148
+ }}
149
+ </Formik>
150
+ </div>
151
+ </>
152
+ );
153
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
+ import EditColumnPopover from '../EditColumnPopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+ import AddBackfillPopover from '../AddBackfillPopover';
6
+ import { mocks } from '../../../../mocks/mockNodes';
7
+
8
+ const mockDjClient = {
9
+ DataJunctionAPI: {
10
+ runBackfill: jest.fn(),
11
+ },
12
+ };
13
+
14
+ describe('<AddBackfillPopover />', () => {
15
+ it('renders correctly and handles form submission', async () => {
16
+ // Mock onSubmit function
17
+ const onSubmitMock = jest.fn();
18
+
19
+ mockDjClient.DataJunctionAPI.runBackfill.mockReturnValue({
20
+ status: 201,
21
+ json: { message: '' },
22
+ });
23
+
24
+ // Render the component
25
+ const { getByLabelText, getByText } = render(
26
+ <DJClientContext.Provider value={mockDjClient}>
27
+ <AddBackfillPopover
28
+ node={mocks.mockMetricNode}
29
+ materialization={mocks.nodeMaterializations}
30
+ onSubmit={onSubmitMock}
31
+ />
32
+ </DJClientContext.Provider>,
33
+ );
34
+
35
+ // Open the popover
36
+ fireEvent.click(getByLabelText('AddBackfill'));
37
+
38
+ fireEvent.click(getByText('Save'));
39
+ getByText('Save').click();
40
+
41
+ // Expect setAttributes to be called
42
+ await waitFor(() => {
43
+ expect(mockDjClient.DataJunctionAPI.runBackfill).toHaveBeenCalled();
44
+ expect(getByText('Saved!')).toBeInTheDocument();
45
+ });
46
+ });
47
+ });
@@ -36,6 +36,7 @@ describe('<NodePage />', () => {
36
36
  nodesWithDimension: jest.fn(),
37
37
  attributes: jest.fn(),
38
38
  dimensions: jest.fn(),
39
+ setPartition: jest.fn(),
39
40
  },
40
41
  };
41
42
  };
@@ -70,6 +71,7 @@ describe('<NodePage />', () => {
70
71
  {
71
72
  name: 'default_DOT_avg_repair_price',
72
73
  type: 'double',
74
+ display_name: 'Default DOT avg repair price',
73
75
  attributes: [],
74
76
  dimension: null,
75
77
  },
@@ -82,7 +84,7 @@ describe('<NodePage />', () => {
82
84
  },
83
85
  ],
84
86
  created_at: '2023-08-21T16:48:56.932162+00:00',
85
- tags: [],
87
+ tags: [{ name: 'purpose', display_name: 'Purpose' }],
86
88
  primary_key: [],
87
89
  createNodeClientCode:
88
90
  'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)',
@@ -303,13 +305,12 @@ describe('<NodePage />', () => {
303
305
  'v1.0',
304
306
  );
305
307
 
306
- // expect(screen.getByRole('dialog', { name: 'Table' })).not.toBeInTheDocument();
307
308
  expect(
308
309
  screen.getByRole('dialog', { name: 'NodeStatus' }),
309
310
  ).toBeInTheDocument();
310
311
 
311
312
  expect(screen.getByRole('dialog', { name: 'Tags' })).toHaveTextContent(
312
- '',
313
+ 'Purpose',
313
314
  );
314
315
 
315
316
  expect(
@@ -396,6 +397,11 @@ describe('<NodePage />', () => {
396
397
  djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
397
398
  djClient.DataJunctionAPI.attributes.mockReturnValue(mocks.attributes);
398
399
  djClient.DataJunctionAPI.dimensions.mockReturnValue(mocks.dimensions);
400
+ djClient.DataJunctionAPI.setPartition.mockReturnValue({
401
+ status: 200,
402
+ json: { message: '' },
403
+ });
404
+
399
405
  const element = (
400
406
  <DJClientContext.Provider value={djClient}>
401
407
  <NodePage />
@@ -416,6 +422,9 @@ describe('<NodePage />', () => {
416
422
  expect(
417
423
  screen.getByRole('columnheader', { name: 'ColumnName' }),
418
424
  ).toHaveTextContent('default_DOT_avg_repair_price');
425
+ expect(
426
+ screen.getByRole('columnheader', { name: 'ColumnDisplayName' }),
427
+ ).toHaveTextContent('Default DOT avg repair price');
419
428
  expect(
420
429
  screen.getByRole('columnheader', { name: 'ColumnType' }),
421
430
  ).toHaveTextContent('double');
@@ -439,6 +448,19 @@ describe('<NodePage />', () => {
439
448
  expect(
440
449
  screen.getByRole('button', { name: 'SaveLinkDimension' }),
441
450
  ).toBeInTheDocument();
451
+
452
+ // check that the set column partition popover can be clicked
453
+ const partitionColumnPopover = screen.getByRole('button', {
454
+ name: 'PartitionColumn',
455
+ });
456
+ expect(partitionColumnPopover).toBeInTheDocument();
457
+ fireEvent.click(partitionColumnPopover);
458
+ const savePartition = screen.getByRole('button', {
459
+ name: 'SaveEditColumn',
460
+ });
461
+ expect(savePartition).toBeInTheDocument();
462
+ fireEvent.click(savePartition);
463
+ expect(screen.getByText('Saved!'));
442
464
  });
443
465
  });
444
466
  // check compiled SQL on nodeInfo page
@@ -573,7 +595,10 @@ describe('<NodePage />', () => {
573
595
  expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
574
596
  mocks.mockMetricNode.name,
575
597
  );
576
- screen.getByText('No materializations available for this node');
598
+ screen.getByText(
599
+ 'No materialization workflows configured for this node.',
600
+ );
601
+ screen.getByText('No materialized datasets available for this node.');
577
602
  });
578
603
  });
579
604
 
@@ -598,20 +623,25 @@ describe('<NodePage />', () => {
598
623
  </Routes>
599
624
  </MemoryRouter>,
600
625
  );
601
- await waitFor(() => {
602
- fireEvent.click(screen.getByRole('button', { name: 'Materializations' }));
603
- expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
604
- mocks.mockMetricNode.name,
605
- );
606
- expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
607
- mocks.mockMetricNode.name,
608
- );
626
+ await waitFor(
627
+ () => {
628
+ fireEvent.click(
629
+ screen.getByRole('button', { name: 'Materializations' }),
630
+ );
631
+ expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
632
+ mocks.mockMetricNode.name,
633
+ );
634
+ expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
635
+ mocks.mockMetricNode.name,
636
+ );
609
637
 
610
- expect(
611
- screen.getByRole('table', { name: 'Materializations' }),
612
- ).toMatchSnapshot();
613
- });
614
- });
638
+ expect(
639
+ screen.getByRole('table', { name: 'Materializations' }),
640
+ ).toMatchSnapshot();
641
+ },
642
+ { timeout: 3000 },
643
+ );
644
+ }, 60000);
615
645
 
616
646
  it('renders the NodeSQL tab', async () => {
617
647
  const djClient = mockDJClient();