datajunction-ui 0.0.1-a112 → 0.0.1-a113.dev0
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/runit.sh +30 -0
- package/runit2.sh +30 -0
- package/src/app/components/NodeMaterializationDelete.jsx +11 -1
- package/src/app/components/Search.jsx +1 -1
- package/src/app/icons/WrenchIcon.jsx +36 -0
- package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
- package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
- package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +46 -45
- package/src/app/pages/NamespacePage/index.jsx +8 -26
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +17 -9
- package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
- package/src/app/pages/NodePage/LinkComplexDimensionPopover.jsx +139 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +346 -91
- package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +43 -1
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +23 -10
- package/src/app/services/DJService.js +59 -19
- package/src/app/services/__tests__/DJService.test.jsx +55 -1
- package/src/styles/index.css +9 -0
|
@@ -1,25 +1,100 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
2
2
|
import TableIcon from '../../icons/TableIcon';
|
|
3
3
|
import AddMaterializationPopover from './AddMaterializationPopover';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import AddBackfillPopover from './AddBackfillPopover';
|
|
6
6
|
import { labelize } from '../../../utils/form';
|
|
7
7
|
import NodeMaterializationDelete from '../../components/NodeMaterializationDelete';
|
|
8
|
+
import Tab from '../../components/Tab';
|
|
9
|
+
import NodeRevisionMaterializationTab from './NodeRevisionMaterializationTab';
|
|
10
|
+
import AvailabilityStateBlock from './AvailabilityStateBlock';
|
|
8
11
|
|
|
9
12
|
const cronstrue = require('cronstrue');
|
|
10
13
|
|
|
11
14
|
export default function NodeMaterializationTab({ node, djClient }) {
|
|
12
|
-
const [
|
|
15
|
+
const [rawMaterializations, setRawMaterializations] = useState([]);
|
|
16
|
+
const [selectedRevisionTab, setSelectedRevisionTab] = useState(null);
|
|
17
|
+
const [showInactive, setShowInactive] = useState(false);
|
|
18
|
+
const [availabilityStates, setAvailabilityStates] = useState([]);
|
|
19
|
+
const [availabilityStatesByRevision, setAvailabilityStatesByRevision] =
|
|
20
|
+
useState({});
|
|
21
|
+
const [isRebuilding, setIsRebuilding] = useState(() => {
|
|
22
|
+
// Check if we're in the middle of a rebuild operation
|
|
23
|
+
return localStorage.getItem(`rebuilding-${node?.name}`) === 'true';
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const filteredMaterializations = useMemo(() => {
|
|
27
|
+
return showInactive
|
|
28
|
+
? rawMaterializations
|
|
29
|
+
: rawMaterializations.filter(mat => !mat.deactivated_at);
|
|
30
|
+
}, [rawMaterializations, showInactive]);
|
|
31
|
+
|
|
32
|
+
const materializationsByRevision = useMemo(() => {
|
|
33
|
+
return filteredMaterializations.reduce((acc, mat) => {
|
|
34
|
+
// Extract version from materialization config
|
|
35
|
+
const matVersion = mat.config?.cube?.version || node?.version;
|
|
36
|
+
|
|
37
|
+
if (!acc[matVersion]) {
|
|
38
|
+
acc[matVersion] = [];
|
|
39
|
+
}
|
|
40
|
+
acc[matVersion].push(mat);
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
}, [filteredMaterializations, node?.version]);
|
|
44
|
+
|
|
13
45
|
useEffect(() => {
|
|
14
46
|
const fetchData = async () => {
|
|
15
47
|
if (node) {
|
|
16
48
|
const data = await djClient.materializations(node.name);
|
|
17
|
-
|
|
49
|
+
|
|
50
|
+
// Store raw data
|
|
51
|
+
setRawMaterializations(data);
|
|
52
|
+
|
|
53
|
+
// Fetch availability states
|
|
54
|
+
const availabilityData = await djClient.availabilityStates(node.name);
|
|
55
|
+
setAvailabilityStates(availabilityData);
|
|
56
|
+
|
|
57
|
+
// Group availability states by version
|
|
58
|
+
const availabilityGrouped = availabilityData.reduce((acc, avail) => {
|
|
59
|
+
const version = avail.node_version || node.version;
|
|
60
|
+
if (!acc[version]) {
|
|
61
|
+
acc[version] = [];
|
|
62
|
+
}
|
|
63
|
+
acc[version].push(avail);
|
|
64
|
+
return acc;
|
|
65
|
+
}, {});
|
|
66
|
+
|
|
67
|
+
setAvailabilityStatesByRevision(availabilityGrouped);
|
|
68
|
+
|
|
69
|
+
// Clear rebuilding state once data is loaded after a page reload
|
|
70
|
+
if (localStorage.getItem(`rebuilding-${node.name}`) === 'true') {
|
|
71
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
72
|
+
setIsRebuilding(false);
|
|
73
|
+
}
|
|
18
74
|
}
|
|
19
75
|
};
|
|
20
76
|
fetchData().catch(console.error);
|
|
21
77
|
}, [djClient, node]);
|
|
22
78
|
|
|
79
|
+
// Separate useEffect to set default selected tab
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (
|
|
82
|
+
!selectedRevisionTab &&
|
|
83
|
+
Object.keys(materializationsByRevision).length > 0
|
|
84
|
+
) {
|
|
85
|
+
// First try to find current node version
|
|
86
|
+
if (materializationsByRevision[node?.version]) {
|
|
87
|
+
setSelectedRevisionTab(node.version);
|
|
88
|
+
} else {
|
|
89
|
+
// Otherwise, select the most recent version (sort by version string)
|
|
90
|
+
const sortedVersions = Object.keys(materializationsByRevision).sort(
|
|
91
|
+
(a, b) => b.localeCompare(a),
|
|
92
|
+
);
|
|
93
|
+
setSelectedRevisionTab(sortedVersions[0]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [materializationsByRevision, selectedRevisionTab, node?.version]);
|
|
97
|
+
|
|
23
98
|
const partitionColumnsMap = node
|
|
24
99
|
? Object.fromEntries(
|
|
25
100
|
node?.columns
|
|
@@ -35,9 +110,198 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
35
110
|
return parsedCron;
|
|
36
111
|
};
|
|
37
112
|
|
|
113
|
+
const onClickRevisionTab = revisionId => () => {
|
|
114
|
+
setSelectedRevisionTab(revisionId);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const buildRevisionTabs = () => {
|
|
118
|
+
const versions = Object.keys(materializationsByRevision);
|
|
119
|
+
|
|
120
|
+
// Check if there are any materializations at all (including inactive ones)
|
|
121
|
+
const hasAnyMaterializations = rawMaterializations.length > 0;
|
|
122
|
+
|
|
123
|
+
// Determine which versions have only inactive materializations
|
|
124
|
+
const versionHasOnlyInactive = {};
|
|
125
|
+
rawMaterializations.forEach(mat => {
|
|
126
|
+
const matVersion = mat.config?.cube?.version || node.version;
|
|
127
|
+
if (!versionHasOnlyInactive[matVersion]) {
|
|
128
|
+
versionHasOnlyInactive[matVersion] = {
|
|
129
|
+
hasActive: false,
|
|
130
|
+
hasInactive: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (mat.deactivated_at) {
|
|
134
|
+
versionHasOnlyInactive[matVersion].hasInactive = true;
|
|
135
|
+
} else {
|
|
136
|
+
versionHasOnlyInactive[matVersion].hasActive = true;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// If no active versions but there are inactive materializations, show checkbox and button
|
|
141
|
+
if (versions.length === 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
style={{
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
justifyContent: 'flex-end',
|
|
148
|
+
marginBottom: '20px',
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
|
152
|
+
{hasAnyMaterializations && (
|
|
153
|
+
<label
|
|
154
|
+
style={{
|
|
155
|
+
display: 'flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
gap: '5px',
|
|
158
|
+
fontSize: '14px',
|
|
159
|
+
color: '#333',
|
|
160
|
+
padding: '4px 8px',
|
|
161
|
+
borderRadius: '12px',
|
|
162
|
+
backgroundColor: '#f5f5f5',
|
|
163
|
+
border: '1px solid #ddd',
|
|
164
|
+
}}
|
|
165
|
+
title="Shows inactive materializations for the latest cube."
|
|
166
|
+
>
|
|
167
|
+
<input
|
|
168
|
+
type="checkbox"
|
|
169
|
+
checked={showInactive}
|
|
170
|
+
onChange={e => setShowInactive(e.target.checked)}
|
|
171
|
+
/>
|
|
172
|
+
Show Inactive
|
|
173
|
+
</label>
|
|
174
|
+
)}
|
|
175
|
+
{node && <AddMaterializationPopover node={node} />}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sort versions: current version first, then by version string (most recent first)
|
|
182
|
+
const sortedVersions = versions.sort((a, b) => {
|
|
183
|
+
// Current node version always comes first
|
|
184
|
+
if (a === node?.version) return -1;
|
|
185
|
+
if (b === node?.version) return 1;
|
|
186
|
+
|
|
187
|
+
// Then sort by version string (descending)
|
|
188
|
+
return b.localeCompare(a);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Check if latest version has active materializations
|
|
192
|
+
const hasLatestVersionMaterialization =
|
|
193
|
+
materializationsByRevision[node?.version] &&
|
|
194
|
+
materializationsByRevision[node?.version].length > 0;
|
|
195
|
+
|
|
196
|
+
// Refresh latest materialization function
|
|
197
|
+
const refreshLatestMaterialization = async () => {
|
|
198
|
+
if (
|
|
199
|
+
!window.confirm(
|
|
200
|
+
'This will create a new version of the cube and build new materialization workflows. The previous version of the cube and its materialization will be accessible using a specific version label. Would you like to continue?',
|
|
201
|
+
)
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Set loading state in both React state and localStorage
|
|
207
|
+
setIsRebuilding(true);
|
|
208
|
+
localStorage.setItem(`rebuilding-${node.name}`, 'true');
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const { status, json } = await djClient.refreshLatestMaterialization(
|
|
212
|
+
node.name,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (status === 200 || status === 201) {
|
|
216
|
+
// Keep the loading state during page reload
|
|
217
|
+
window.location.reload(); // Reload to show the updated materialization
|
|
218
|
+
} else {
|
|
219
|
+
alert(`Failed to rebuild materialization: ${json.message}`);
|
|
220
|
+
// Clear loading state on error
|
|
221
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
222
|
+
setIsRebuilding(false);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
alert(`Error rebuilding materialization: ${error.message}`);
|
|
226
|
+
// Clear loading state on error
|
|
227
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
228
|
+
setIsRebuilding(false);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
style={{
|
|
235
|
+
display: 'flex',
|
|
236
|
+
alignItems: 'center',
|
|
237
|
+
justifyContent: 'space-between',
|
|
238
|
+
marginBottom: '20px',
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div className="align-items-center row">
|
|
242
|
+
{sortedVersions.map(version => (
|
|
243
|
+
<NodeRevisionMaterializationTab
|
|
244
|
+
key={version}
|
|
245
|
+
version={version}
|
|
246
|
+
node={node}
|
|
247
|
+
selectedRevisionTab={selectedRevisionTab}
|
|
248
|
+
onClickRevisionTab={onClickRevisionTab}
|
|
249
|
+
showInactive={showInactive}
|
|
250
|
+
versionHasOnlyInactive={versionHasOnlyInactive}
|
|
251
|
+
/>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
|
255
|
+
<label
|
|
256
|
+
style={{
|
|
257
|
+
display: 'flex',
|
|
258
|
+
alignItems: 'center',
|
|
259
|
+
gap: '5px',
|
|
260
|
+
fontSize: '14px',
|
|
261
|
+
color: '#333',
|
|
262
|
+
padding: '4px 8px',
|
|
263
|
+
borderRadius: '12px',
|
|
264
|
+
backgroundColor: '#f5f5f5',
|
|
265
|
+
border: '1px solid #ddd',
|
|
266
|
+
}}
|
|
267
|
+
title="Shows inactive materializations for the latest cube."
|
|
268
|
+
>
|
|
269
|
+
<input
|
|
270
|
+
type="checkbox"
|
|
271
|
+
checked={showInactive}
|
|
272
|
+
onChange={e => setShowInactive(e.target.checked)}
|
|
273
|
+
/>
|
|
274
|
+
Show Inactive
|
|
275
|
+
</label>
|
|
276
|
+
{node &&
|
|
277
|
+
(hasLatestVersionMaterialization ? (
|
|
278
|
+
<button
|
|
279
|
+
className="edit_button"
|
|
280
|
+
aria-label="RefreshLatestMaterialization"
|
|
281
|
+
tabIndex="0"
|
|
282
|
+
onClick={refreshLatestMaterialization}
|
|
283
|
+
disabled={isRebuilding}
|
|
284
|
+
title="Create a new version of the cube and re-create its materialization workflows."
|
|
285
|
+
style={{
|
|
286
|
+
opacity: isRebuilding ? 0.7 : 1,
|
|
287
|
+
cursor: isRebuilding ? 'not-allowed' : 'pointer',
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<span className="add_node">
|
|
291
|
+
Rebuild (latest) Materialization
|
|
292
|
+
</span>
|
|
293
|
+
</button>
|
|
294
|
+
) : (
|
|
295
|
+
<AddMaterializationPopover node={node} />
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
38
302
|
const materializationRows = materializations => {
|
|
39
|
-
return materializations.map(materialization => (
|
|
40
|
-
|
|
303
|
+
return materializations.map((materialization, index) => (
|
|
304
|
+
<div key={`${materialization.name}-${index}`}>
|
|
41
305
|
<div className="tr">
|
|
42
306
|
<div key={materialization.name} style={{ fontSize: 'large' }}>
|
|
43
307
|
<div
|
|
@@ -53,6 +317,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
53
317
|
<NodeMaterializationDelete
|
|
54
318
|
nodeName={node.name}
|
|
55
319
|
materializationName={materialization.name}
|
|
320
|
+
nodeVersion={selectedRevisionTab}
|
|
56
321
|
/>
|
|
57
322
|
</div>
|
|
58
323
|
<div className="td">
|
|
@@ -191,7 +456,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
191
456
|
.filter(col => col.partition !== null)
|
|
192
457
|
.map(column => {
|
|
193
458
|
return (
|
|
194
|
-
<li>
|
|
459
|
+
<li key={column.name}>
|
|
195
460
|
<div className="partitionLink">
|
|
196
461
|
{column.display_name}
|
|
197
462
|
<span className="badge partition_value">
|
|
@@ -206,20 +471,86 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
206
471
|
</ul>
|
|
207
472
|
</div>
|
|
208
473
|
</div>
|
|
209
|
-
|
|
474
|
+
</div>
|
|
210
475
|
));
|
|
211
476
|
};
|
|
477
|
+
const currentRevisionMaterializations = selectedRevisionTab
|
|
478
|
+
? materializationsByRevision[selectedRevisionTab] || []
|
|
479
|
+
: filteredMaterializations;
|
|
480
|
+
|
|
481
|
+
const currentRevisionAvailability = selectedRevisionTab
|
|
482
|
+
? availabilityStatesByRevision[selectedRevisionTab] || []
|
|
483
|
+
: availabilityStates;
|
|
484
|
+
|
|
485
|
+
const renderMaterializedDatasets = availabilityStates => {
|
|
486
|
+
if (!availabilityStates || availabilityStates.length === 0) {
|
|
487
|
+
return (
|
|
488
|
+
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
489
|
+
No materialized datasets available for this revision.
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return availabilityStates.map((availability, index) => (
|
|
495
|
+
<AvailabilityStateBlock
|
|
496
|
+
key={`availability-${index}`}
|
|
497
|
+
availability={availability}
|
|
498
|
+
/>
|
|
499
|
+
));
|
|
500
|
+
};
|
|
501
|
+
|
|
212
502
|
return (
|
|
213
503
|
<>
|
|
214
504
|
<div
|
|
215
505
|
className="table-vertical"
|
|
216
506
|
role="table"
|
|
217
507
|
aria-label="Materializations"
|
|
508
|
+
style={{ position: 'relative' }}
|
|
218
509
|
>
|
|
510
|
+
{/* Loading overlay */}
|
|
511
|
+
{isRebuilding && (
|
|
512
|
+
<div
|
|
513
|
+
style={{
|
|
514
|
+
position: 'absolute',
|
|
515
|
+
top: 0,
|
|
516
|
+
left: 0,
|
|
517
|
+
right: 0,
|
|
518
|
+
bottom: 0,
|
|
519
|
+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
520
|
+
display: 'flex',
|
|
521
|
+
flexDirection: 'column',
|
|
522
|
+
justifyContent: 'center',
|
|
523
|
+
alignItems: 'center',
|
|
524
|
+
zIndex: 1000,
|
|
525
|
+
minHeight: '200px',
|
|
526
|
+
}}
|
|
527
|
+
>
|
|
528
|
+
<div
|
|
529
|
+
style={{
|
|
530
|
+
width: '40px',
|
|
531
|
+
height: '40px',
|
|
532
|
+
border: '4px solid #f3f3f3',
|
|
533
|
+
borderTop: '4px solid #3498db',
|
|
534
|
+
borderRadius: '50%',
|
|
535
|
+
animation: 'spin 1s linear infinite',
|
|
536
|
+
marginBottom: '16px',
|
|
537
|
+
}}
|
|
538
|
+
/>
|
|
539
|
+
<div
|
|
540
|
+
style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}
|
|
541
|
+
>
|
|
542
|
+
Rebuilding materialization...
|
|
543
|
+
<br />
|
|
544
|
+
<small style={{ fontSize: '14px' }}>
|
|
545
|
+
This may take a few moments
|
|
546
|
+
</small>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
|
|
219
551
|
<div>
|
|
220
|
-
|
|
221
|
-
{
|
|
222
|
-
{materializations.length > 0 ? (
|
|
552
|
+
{buildRevisionTabs()}
|
|
553
|
+
{currentRevisionMaterializations.length > 0 ? (
|
|
223
554
|
<div
|
|
224
555
|
className="card-inner-table table"
|
|
225
556
|
aria-label="Materializations"
|
|
@@ -227,7 +558,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
227
558
|
>
|
|
228
559
|
<div style={{ display: 'table' }}>
|
|
229
560
|
{materializationRows(
|
|
230
|
-
|
|
561
|
+
currentRevisionMaterializations.filter(
|
|
231
562
|
materialization =>
|
|
232
563
|
!(
|
|
233
564
|
materialization.name === 'default' &&
|
|
@@ -239,88 +570,12 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
239
570
|
</div>
|
|
240
571
|
) : (
|
|
241
572
|
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
242
|
-
No materialization workflows configured for this
|
|
573
|
+
No materialization workflows configured for this revision.
|
|
243
574
|
</div>
|
|
244
575
|
)}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{node && node.availability !== null ? (
|
|
249
|
-
<table
|
|
250
|
-
className="card-inner-table table"
|
|
251
|
-
aria-label="Availability"
|
|
252
|
-
aria-hidden="false"
|
|
253
|
-
>
|
|
254
|
-
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
255
|
-
<tr>
|
|
256
|
-
<th className="text-start">Output Dataset</th>
|
|
257
|
-
<th>Valid Through</th>
|
|
258
|
-
<th>Partitions</th>
|
|
259
|
-
<th>Links</th>
|
|
260
|
-
</tr>
|
|
261
|
-
</thead>
|
|
262
|
-
<tbody>
|
|
263
|
-
<tr>
|
|
264
|
-
<td>
|
|
265
|
-
{
|
|
266
|
-
<div
|
|
267
|
-
className={`table__full`}
|
|
268
|
-
key={node.availability.table}
|
|
269
|
-
>
|
|
270
|
-
<div className="table__header">
|
|
271
|
-
<TableIcon />{' '}
|
|
272
|
-
<span className={`entity-info`}>
|
|
273
|
-
{node.availability.catalog +
|
|
274
|
-
'.' +
|
|
275
|
-
node.availability.schema_}
|
|
276
|
-
</span>
|
|
277
|
-
</div>
|
|
278
|
-
<div className={`table__body upstream_tables`}>
|
|
279
|
-
<a href={node.availability.url}>
|
|
280
|
-
{node.availability.table}
|
|
281
|
-
</a>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
}
|
|
285
|
-
</td>
|
|
286
|
-
<td>
|
|
287
|
-
{new Date(node.availability.valid_through_ts).toISOString()}
|
|
288
|
-
</td>
|
|
289
|
-
<td>
|
|
290
|
-
<span
|
|
291
|
-
className={`badge partition_value`}
|
|
292
|
-
style={{ fontSize: '100%' }}
|
|
293
|
-
>
|
|
294
|
-
<span className={`badge partition_value_highlight`}>
|
|
295
|
-
{node.availability.min_temporal_partition}
|
|
296
|
-
</span>
|
|
297
|
-
to
|
|
298
|
-
<span className={`badge partition_value_highlight`}>
|
|
299
|
-
{node.availability.max_temporal_partition}
|
|
300
|
-
</span>
|
|
301
|
-
</span>
|
|
302
|
-
</td>
|
|
303
|
-
<td>
|
|
304
|
-
{node.availability.links !== null ? (
|
|
305
|
-
Object.entries(node.availability.links).map(
|
|
306
|
-
([key, value]) => (
|
|
307
|
-
<div key={key}>
|
|
308
|
-
<a href={value} target="_blank" rel="noreferrer">
|
|
309
|
-
{key}
|
|
310
|
-
</a>
|
|
311
|
-
</div>
|
|
312
|
-
),
|
|
313
|
-
)
|
|
314
|
-
) : (
|
|
315
|
-
<></>
|
|
316
|
-
)}
|
|
317
|
-
</td>
|
|
318
|
-
</tr>
|
|
319
|
-
</tbody>
|
|
320
|
-
</table>
|
|
321
|
-
) : (
|
|
322
|
-
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
323
|
-
No materialized datasets available for this node.
|
|
576
|
+
{Object.keys(materializationsByRevision).length > 0 && (
|
|
577
|
+
<div style={{ marginTop: '30px' }}>
|
|
578
|
+
{renderMaterializedDatasets(currentRevisionAvailability)}
|
|
324
579
|
</div>
|
|
325
580
|
)}
|
|
326
581
|
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Tab from '../../components/Tab';
|
|
2
|
+
|
|
3
|
+
export default function NodeRevisionMaterializationTab({
|
|
4
|
+
version,
|
|
5
|
+
node,
|
|
6
|
+
selectedRevisionTab,
|
|
7
|
+
onClickRevisionTab,
|
|
8
|
+
showInactive,
|
|
9
|
+
versionHasOnlyInactive,
|
|
10
|
+
}) {
|
|
11
|
+
const isCurrentVersion = version === node?.version;
|
|
12
|
+
const tabName = isCurrentVersion ? `${version} (latest)` : version;
|
|
13
|
+
const versionInfo = versionHasOnlyInactive[version];
|
|
14
|
+
const isOnlyInactive =
|
|
15
|
+
versionInfo && !versionInfo.hasActive && versionInfo.hasInactive;
|
|
16
|
+
|
|
17
|
+
// For inactive-only versions, render with oval styling
|
|
18
|
+
if (isOnlyInactive && showInactive) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
key={version}
|
|
22
|
+
className={selectedRevisionTab === version ? 'col active' : 'col'}
|
|
23
|
+
>
|
|
24
|
+
<div className="header-tabs nav-overflow nav nav-tabs">
|
|
25
|
+
<div className="nav-item">
|
|
26
|
+
<button
|
|
27
|
+
id={version}
|
|
28
|
+
className="nav-link"
|
|
29
|
+
tabIndex="0"
|
|
30
|
+
onClick={onClickRevisionTab(version)}
|
|
31
|
+
aria-label={tabName}
|
|
32
|
+
aria-hidden="false"
|
|
33
|
+
style={{
|
|
34
|
+
padding: '4px 8px',
|
|
35
|
+
borderRadius: '12px',
|
|
36
|
+
backgroundColor: '#f5f5f5',
|
|
37
|
+
border: '1px solid #ddd',
|
|
38
|
+
margin: '0 2px',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{tabName}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Tab
|
|
51
|
+
key={version}
|
|
52
|
+
id={version}
|
|
53
|
+
name={tabName}
|
|
54
|
+
onClick={onClickRevisionTab(version)}
|
|
55
|
+
selectedTab={selectedRevisionTab}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -6,12 +6,19 @@ describe('<NodeMaterializationTab />', () => {
|
|
|
6
6
|
const mockDjClient = {
|
|
7
7
|
node: jest.fn(),
|
|
8
8
|
materializations: jest.fn(),
|
|
9
|
+
availabilityStates: jest.fn(),
|
|
10
|
+
materializationInfo: jest.fn(),
|
|
11
|
+
refreshLatestMaterialization: jest.fn(),
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
const mockMaterializations = [
|
|
12
15
|
{
|
|
13
16
|
name: 'mat_one',
|
|
14
|
-
config: {
|
|
17
|
+
config: {
|
|
18
|
+
cube: {
|
|
19
|
+
version: 'v1.0',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
15
22
|
schedule: '@daily',
|
|
16
23
|
job: 'SparkSqlMaterializationJob',
|
|
17
24
|
backfills: [
|
|
@@ -29,6 +36,27 @@ describe('<NodeMaterializationTab />', () => {
|
|
|
29
36
|
strategy: 'full',
|
|
30
37
|
output_tables: ['table1'],
|
|
31
38
|
urls: ['https://example.com/'],
|
|
39
|
+
deactivated_at: null,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const mockAvailabilityStates = [
|
|
44
|
+
{
|
|
45
|
+
id: 1,
|
|
46
|
+
catalog: 'default',
|
|
47
|
+
schema_: 'foo',
|
|
48
|
+
table: 'bar',
|
|
49
|
+
valid_through_ts: 1729667463,
|
|
50
|
+
url: 'https://www.table.com',
|
|
51
|
+
links: { dashboard: 'https://www.foobar.com/dashboard' },
|
|
52
|
+
categorical_partitions: [],
|
|
53
|
+
temporal_partitions: [],
|
|
54
|
+
min_temporal_partition: ['2022', '01', '01'],
|
|
55
|
+
max_temporal_partition: ['2023', '01', '25'],
|
|
56
|
+
partitions: [],
|
|
57
|
+
updated_at: '2023-08-21T16:48:52.880498+00:00',
|
|
58
|
+
node_revision_id: 1,
|
|
59
|
+
node_version: 'v1.0',
|
|
32
60
|
},
|
|
33
61
|
];
|
|
34
62
|
|
|
@@ -134,13 +162,27 @@ describe('<NodeMaterializationTab />', () => {
|
|
|
134
162
|
|
|
135
163
|
beforeEach(() => {
|
|
136
164
|
mockDjClient.materializations.mockReset();
|
|
165
|
+
mockDjClient.availabilityStates.mockReset();
|
|
166
|
+
mockDjClient.materializationInfo.mockReset();
|
|
137
167
|
});
|
|
138
168
|
|
|
139
169
|
it('renders NodeMaterializationTab tab correctly', async () => {
|
|
140
170
|
mockDjClient.materializations.mockReturnValue(mockMaterializations);
|
|
171
|
+
mockDjClient.availabilityStates.mockReturnValue(mockAvailabilityStates);
|
|
172
|
+
mockDjClient.materializationInfo.mockReturnValue({
|
|
173
|
+
job_types: [],
|
|
174
|
+
strategies: [],
|
|
175
|
+
});
|
|
141
176
|
|
|
142
177
|
render(<NodeMaterializationTab node={mockNode} djClient={mockDjClient} />);
|
|
143
178
|
await waitFor(() => {
|
|
179
|
+
// Check that the version tab is rendered
|
|
180
|
+
expect(screen.getByText('v1.0 (latest)')).toBeInTheDocument();
|
|
181
|
+
|
|
182
|
+
// Check that the materialization is rendered
|
|
183
|
+
expect(screen.getByText('Spark Sql')).toBeInTheDocument();
|
|
184
|
+
|
|
185
|
+
// Check that the dashboard link is rendered in the availability section
|
|
144
186
|
const link = screen.getByText('dashboard').closest('a');
|
|
145
187
|
expect(link).toHaveAttribute('href', `https://www.foobar.com/dashboard`);
|
|
146
188
|
});
|