datajunction-ui 0.0.1-a46.dev3 → 0.0.1-a47
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/Makefile +5 -0
- package/package.json +2 -2
- package/src/app/components/NodeMaterializationDelete.jsx +80 -0
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +46 -51
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +33 -24
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +38 -31
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +179 -110
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -3
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +12 -5
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +276 -191
- package/src/app/services/DJService.js +26 -25
- package/src/app/services/__tests__/DJService.test.jsx +37 -21
- package/src/mocks/mockNodes.jsx +61 -7
- package/src/styles/index.css +113 -11
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import ClientCodePopover from './ClientCodePopover';
|
|
3
2
|
import TableIcon from '../../icons/TableIcon';
|
|
4
3
|
import AddMaterializationPopover from './AddMaterializationPopover';
|
|
5
4
|
import * as React from 'react';
|
|
6
5
|
import AddBackfillPopover from './AddBackfillPopover';
|
|
6
|
+
import { labelize } from '../../../utils/form';
|
|
7
|
+
import NodeMaterializationDelete from '../../components/NodeMaterializationDelete';
|
|
7
8
|
|
|
8
9
|
const cronstrue = require('cronstrue');
|
|
9
10
|
|
|
@@ -18,17 +19,6 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
18
19
|
};
|
|
19
20
|
fetchData().catch(console.error);
|
|
20
21
|
}, [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
|
-
// };
|
|
32
22
|
|
|
33
23
|
const partitionColumnsMap = node
|
|
34
24
|
? Object.fromEntries(
|
|
@@ -47,116 +37,195 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
47
37
|
|
|
48
38
|
const materializationRows = materializations => {
|
|
49
39
|
return materializations.map(materialization => (
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.filter(col => col.partition !== null)
|
|
62
|
-
.map(column => {
|
|
63
|
-
return (
|
|
64
|
-
<div className="partition__full" key={column.name}>
|
|
65
|
-
<div className="partition__header">{column.display_name}</div>
|
|
66
|
-
<div className="partition__body">
|
|
67
|
-
<code>{column.name}</code>
|
|
68
|
-
<span className="badge partition_value">
|
|
69
|
-
{column.partition.type_}
|
|
70
|
-
</span>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</td>
|
|
76
|
-
<td>
|
|
77
|
-
{materialization.output_tables.map(table => (
|
|
78
|
-
<div className={`table__full`} key={table}>
|
|
79
|
-
<div className="table__header">
|
|
80
|
-
<TableIcon />{' '}
|
|
81
|
-
<span className={`entity-info`}>
|
|
82
|
-
{table.split('.')[0] + '.' + table.split('.')[1]}
|
|
83
|
-
</span>
|
|
84
|
-
</div>
|
|
85
|
-
<div className={`table__body upstream_tables`}>
|
|
86
|
-
{table.split('.')[2]}
|
|
87
|
-
</div>
|
|
40
|
+
<>
|
|
41
|
+
<div className="tr">
|
|
42
|
+
<div key={materialization.name} style={{ fontSize: 'large' }}>
|
|
43
|
+
<div
|
|
44
|
+
className="text-start node_name td"
|
|
45
|
+
style={{ fontWeight: '600' }}
|
|
46
|
+
>
|
|
47
|
+
{materialization.job
|
|
48
|
+
?.replace('MaterializationJob', '')
|
|
49
|
+
.match(/[A-Z][a-z]+/g)
|
|
50
|
+
.join(' ')}
|
|
88
51
|
</div>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
52
|
+
<div className="td">
|
|
53
|
+
<NodeMaterializationDelete
|
|
54
|
+
nodeName={node.name}
|
|
55
|
+
materializationName={materialization.name}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="td">
|
|
59
|
+
<span className={`badge cron`}>{materialization.schedule}</span>
|
|
60
|
+
<div className={`cron-description`}>{cron(materialization)} </div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="td">
|
|
63
|
+
<span className={`badge strategy`}>
|
|
64
|
+
{labelize(materialization.strategy)}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div style={{ display: 'table-row' }}>
|
|
70
|
+
<div style={{ display: 'inline-flex' }}>
|
|
71
|
+
<ul className="backfills">
|
|
72
|
+
<li className="backfill">
|
|
73
|
+
<div className="backfills_header">Output Tables</div>{' '}
|
|
74
|
+
{materialization.output_tables.map(table => (
|
|
75
|
+
<div className={`table__full`} key={table}>
|
|
76
|
+
<div className="table__header">
|
|
77
|
+
<TableIcon />{' '}
|
|
78
|
+
<span className={`entity-info`}>
|
|
79
|
+
{table.split('.')[0] + '.' + table.split('.')[1]}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className={`table__body upstream_tables`}>
|
|
83
|
+
{table.split('.')[2]}
|
|
84
|
+
</div>
|
|
110
85
|
</div>
|
|
111
|
-
|
|
112
|
-
</
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
86
|
+
))}
|
|
87
|
+
</li>
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div style={{ display: 'inline-flex' }}>
|
|
92
|
+
<ul className="backfills">
|
|
93
|
+
<li>
|
|
94
|
+
<div className="backfills_header">Workflows</div>{' '}
|
|
95
|
+
<ul>
|
|
96
|
+
{materialization.urls.map((url, idx) => (
|
|
97
|
+
<li style={{ listStyle: 'none' }} key={idx}>
|
|
98
|
+
<div
|
|
99
|
+
className="partitionLink"
|
|
100
|
+
style={{ fontSize: 'revert' }}
|
|
101
|
+
>
|
|
102
|
+
<a
|
|
103
|
+
href={url}
|
|
104
|
+
key={`url-${idx}`}
|
|
105
|
+
className=""
|
|
106
|
+
target="blank"
|
|
107
|
+
>
|
|
108
|
+
{idx === 0 ? 'main' : 'backfill'}
|
|
109
|
+
</a>
|
|
110
|
+
</div>
|
|
111
|
+
</li>
|
|
112
|
+
))}
|
|
113
|
+
</ul>
|
|
114
|
+
</li>
|
|
115
|
+
</ul>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div style={{ display: 'inline-flex' }}>
|
|
119
|
+
<ul className="backfills">
|
|
120
|
+
<li className="backfill">
|
|
121
|
+
<details open>
|
|
122
|
+
<summary>
|
|
123
|
+
<span className="backfills_header">Backfills</span>{' '}
|
|
124
|
+
</summary>
|
|
125
|
+
{materialization.strategy === 'incremental_time' ? (
|
|
126
|
+
<ul>
|
|
127
|
+
<li>
|
|
128
|
+
<AddBackfillPopover
|
|
129
|
+
node={node}
|
|
130
|
+
materialization={materialization}
|
|
131
|
+
/>
|
|
132
|
+
</li>
|
|
133
|
+
{materialization.backfills.map(backfill => (
|
|
134
|
+
<li className="backfill">
|
|
135
|
+
<div className="partitionLink">
|
|
136
|
+
<a href={backfill.urls[0]}>
|
|
137
|
+
{backfill.spec.map(partition => {
|
|
138
|
+
const partitionBody =
|
|
139
|
+
'range' in partition &&
|
|
140
|
+
partition['range'] !== null ? (
|
|
141
|
+
<>
|
|
142
|
+
<span className="badge partition_value">
|
|
143
|
+
{partition.range[0]}
|
|
144
|
+
</span>
|
|
145
|
+
to
|
|
146
|
+
<span className="badge partition_value">
|
|
147
|
+
{partition.range[1]}
|
|
148
|
+
</span>
|
|
149
|
+
</>
|
|
150
|
+
) : (
|
|
151
|
+
<span className="badge partition_value">
|
|
152
|
+
{partition.values.join(', ')}
|
|
153
|
+
</span>
|
|
154
|
+
);
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<div>
|
|
158
|
+
{
|
|
159
|
+
partitionColumnsMap[
|
|
160
|
+
partition.column_name.replaceAll(
|
|
161
|
+
'_DOT_',
|
|
162
|
+
'.',
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
}{' '}
|
|
166
|
+
{partitionBody}
|
|
167
|
+
</div>
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</a>
|
|
172
|
+
</div>
|
|
173
|
+
</li>
|
|
174
|
+
))}
|
|
175
|
+
</ul>
|
|
176
|
+
) : (
|
|
177
|
+
<ul>
|
|
178
|
+
<li>N/A</li>
|
|
179
|
+
</ul>
|
|
180
|
+
)}
|
|
181
|
+
</details>
|
|
182
|
+
</li>
|
|
183
|
+
</ul>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="td">
|
|
186
|
+
<ul className="backfills">
|
|
187
|
+
<li className="backfill">
|
|
188
|
+
<div className="backfills_header">Partitions</div>{' '}
|
|
189
|
+
<ul>
|
|
190
|
+
{node.columns
|
|
191
|
+
.filter(col => col.partition !== null)
|
|
192
|
+
.map(column => {
|
|
193
|
+
return (
|
|
194
|
+
<li>
|
|
195
|
+
<div className="partitionLink">
|
|
196
|
+
{column.display_name}
|
|
197
|
+
<span className="badge partition_value">
|
|
198
|
+
{column.partition.type_}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
</li>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
</ul>
|
|
205
|
+
</li>
|
|
206
|
+
</ul>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</>
|
|
130
210
|
));
|
|
131
211
|
};
|
|
132
212
|
return (
|
|
133
213
|
<>
|
|
134
|
-
<div
|
|
214
|
+
<div
|
|
215
|
+
className="table-vertical"
|
|
216
|
+
role="table"
|
|
217
|
+
aria-label="Materializations"
|
|
218
|
+
>
|
|
135
219
|
<div>
|
|
136
220
|
<h2>Materializations</h2>
|
|
137
221
|
{node ? <AddMaterializationPopover node={node} /> : <></>}
|
|
138
222
|
{materializations.length > 0 ? (
|
|
139
|
-
<
|
|
223
|
+
<div
|
|
140
224
|
className="card-inner-table table"
|
|
141
225
|
aria-label="Materializations"
|
|
142
226
|
aria-hidden="false"
|
|
143
227
|
>
|
|
144
|
-
<
|
|
145
|
-
<tr>
|
|
146
|
-
<th className="text-start">Schedule</th>
|
|
147
|
-
<th>Job Type</th>
|
|
148
|
-
<th>Strategy</th>
|
|
149
|
-
<th>Partitions</th>
|
|
150
|
-
<th>Intended Output Tables</th>
|
|
151
|
-
{materializations[0].strategy === 'incremental_time' ? (
|
|
152
|
-
<th>Backfills</th>
|
|
153
|
-
) : (
|
|
154
|
-
<></>
|
|
155
|
-
)}
|
|
156
|
-
<th>URLs</th>
|
|
157
|
-
</tr>
|
|
158
|
-
</thead>
|
|
159
|
-
<tbody>
|
|
228
|
+
<div style={{ display: 'table' }}>
|
|
160
229
|
{materializationRows(
|
|
161
230
|
materializations.filter(
|
|
162
231
|
materialization =>
|
|
@@ -166,8 +235,8 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
166
235
|
),
|
|
167
236
|
),
|
|
168
237
|
)}
|
|
169
|
-
</
|
|
170
|
-
</
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
171
240
|
) : (
|
|
172
241
|
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
173
242
|
No materialization workflows configured for this node.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Field } from 'formik';
|
|
3
|
+
|
|
4
|
+
export default function PartitionValueForm({ col, materialization }) {
|
|
5
|
+
if (col.partition.type_ === 'temporal') {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<div
|
|
9
|
+
className="partition__full"
|
|
10
|
+
key={col.name}
|
|
11
|
+
style={{ width: '50%' }}
|
|
12
|
+
>
|
|
13
|
+
<div className="partition__header">{col.display_name}</div>
|
|
14
|
+
<div className="partition__body">
|
|
15
|
+
<span style={{ padding: '0.5rem' }}>From</span>{' '}
|
|
16
|
+
<Field
|
|
17
|
+
type="text"
|
|
18
|
+
name={`partitionValues.['${col.name}'].from`}
|
|
19
|
+
id={`${col.name}.from`}
|
|
20
|
+
placeholder="20230101"
|
|
21
|
+
default="20230101"
|
|
22
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
23
|
+
/>{' '}
|
|
24
|
+
<span style={{ padding: '0.5rem' }}>To</span>
|
|
25
|
+
<Field
|
|
26
|
+
type="text"
|
|
27
|
+
name={`partitionValues.['${col.name}'].to`}
|
|
28
|
+
id={`${col.name}.to`}
|
|
29
|
+
placeholder="20230102"
|
|
30
|
+
default="20230102"
|
|
31
|
+
style={{ width: '7rem' }}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<div
|
|
41
|
+
className="partition__full"
|
|
42
|
+
key={col.name}
|
|
43
|
+
style={{ width: '50%' }}
|
|
44
|
+
>
|
|
45
|
+
<div className="partition__header">{col.display_name}</div>
|
|
46
|
+
<div className="partition__body">
|
|
47
|
+
<Field
|
|
48
|
+
type="text"
|
|
49
|
+
name={`partitionValues.['${col.name}']`}
|
|
50
|
+
id={col.name}
|
|
51
|
+
placeholder=""
|
|
52
|
+
default=""
|
|
53
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, fireEvent, waitFor
|
|
3
|
-
import EditColumnPopover from '../EditColumnPopover';
|
|
2
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
4
3
|
import DJClientContext from '../../../providers/djclient';
|
|
5
4
|
import AddBackfillPopover from '../AddBackfillPopover';
|
|
6
5
|
import { mocks } from '../../../../mocks/mockNodes';
|
|
@@ -11,6 +10,17 @@ const mockDjClient = {
|
|
|
11
10
|
},
|
|
12
11
|
};
|
|
13
12
|
|
|
13
|
+
let reloadMock = jest.fn();
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
delete window.location;
|
|
17
|
+
window.location = { reload: reloadMock };
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
reloadMock.mockClear();
|
|
22
|
+
});
|
|
23
|
+
|
|
14
24
|
describe('<AddBackfillPopover />', () => {
|
|
15
25
|
it('renders correctly and handles form submission', async () => {
|
|
16
26
|
// Mock onSubmit function
|
|
@@ -25,7 +35,7 @@ describe('<AddBackfillPopover />', () => {
|
|
|
25
35
|
const { getByLabelText, getByText } = render(
|
|
26
36
|
<DJClientContext.Provider value={mockDjClient}>
|
|
27
37
|
<AddBackfillPopover
|
|
28
|
-
node={mocks.
|
|
38
|
+
node={mocks.mockTransformNode}
|
|
29
39
|
materialization={mocks.nodeMaterializations}
|
|
30
40
|
onSubmit={onSubmitMock}
|
|
31
41
|
/>
|
|
@@ -29,6 +29,7 @@ describe('<NodePage />', () => {
|
|
|
29
29
|
history: jest.fn(),
|
|
30
30
|
revisions: jest.fn(),
|
|
31
31
|
materializations: jest.fn(),
|
|
32
|
+
materializationInfo: jest.fn(),
|
|
32
33
|
sql: jest.fn(),
|
|
33
34
|
cube: jest.fn(),
|
|
34
35
|
compiledSql: jest.fn(),
|
|
@@ -604,13 +605,17 @@ describe('<NodePage />', () => {
|
|
|
604
605
|
|
|
605
606
|
it('renders the NodeMaterialization tab with materializations correctly', async () => {
|
|
606
607
|
const djClient = mockDJClient();
|
|
607
|
-
djClient.DataJunctionAPI.node.mockReturnValue(mocks.
|
|
608
|
-
djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
|
|
608
|
+
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
|
|
609
|
+
// djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
|
|
609
610
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
610
611
|
djClient.DataJunctionAPI.materializations.mockReturnValue(
|
|
611
612
|
mocks.nodeMaterializations,
|
|
612
613
|
);
|
|
613
614
|
|
|
615
|
+
djClient.DataJunctionAPI.materializationInfo.mockReturnValue(
|
|
616
|
+
mocks.materializationInfo,
|
|
617
|
+
);
|
|
618
|
+
|
|
614
619
|
const element = (
|
|
615
620
|
<DJClientContext.Provider value={djClient}>
|
|
616
621
|
<NodePage />
|
|
@@ -618,7 +623,9 @@ describe('<NodePage />', () => {
|
|
|
618
623
|
);
|
|
619
624
|
render(
|
|
620
625
|
<MemoryRouter
|
|
621
|
-
initialEntries={[
|
|
626
|
+
initialEntries={[
|
|
627
|
+
'/nodes/default.repair_order_transform/materializations',
|
|
628
|
+
]}
|
|
622
629
|
>
|
|
623
630
|
<Routes>
|
|
624
631
|
<Route path="nodes/:name/:tab" element={element} />
|
|
@@ -631,10 +638,10 @@ describe('<NodePage />', () => {
|
|
|
631
638
|
screen.getByRole('button', { name: 'Materializations' }),
|
|
632
639
|
);
|
|
633
640
|
expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
|
|
634
|
-
mocks.
|
|
641
|
+
mocks.mockTransformNode.name,
|
|
635
642
|
);
|
|
636
643
|
expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
|
|
637
|
-
mocks.
|
|
644
|
+
mocks.mockTransformNode.name,
|
|
638
645
|
);
|
|
639
646
|
|
|
640
647
|
expect(
|