datajunction-ui 0.0.31 → 0.0.41
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 +3 -3
- package/src/app/components/ListGroupItem.jsx +2 -2
- package/src/app/components/QueryInfo.jsx +2 -1
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +2 -2
- package/src/app/components/djgraph/__tests__/Collapse.test.jsx +6 -3
- package/src/app/pages/AddEditNodePage/index.jsx +1 -1
- package/src/app/pages/AddEditTagPage/index.jsx +1 -1
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +55 -21
- package/src/app/pages/NodePage/NodeInfoTab.jsx +17 -6
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +5 -0
- package/src/app/pages/NodePage/NodePreAggregationsTab.jsx +656 -0
- package/src/app/pages/NodePage/NodeValidateTab.jsx +4 -2
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +58 -45
- package/src/app/pages/NodePage/__tests__/NodePreAggregationsTab.test.jsx +654 -0
- package/src/app/pages/NodePage/index.jsx +9 -1
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +19 -4
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +47 -9
- package/src/app/pages/SQLBuilderPage/index.jsx +2 -2
- package/src/app/services/DJService.js +26 -0
- package/src/styles/preaggregations.css +547 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo, useContext } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import { labelize } from '../../../utils/form';
|
|
4
|
+
import '../../../styles/preaggregations.css';
|
|
5
|
+
|
|
6
|
+
const cronstrue = require('cronstrue');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pre-aggregations tab for non-cube nodes (transform, metric, dimension).
|
|
10
|
+
* Shows pre-aggs grouped by staleness (current vs stale versions).
|
|
11
|
+
*/
|
|
12
|
+
export default function NodePreAggregationsTab({ node }) {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const [preaggs, setPreaggs] = useState([]);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [expandedIds, setExpandedIds] = useState(new Set());
|
|
18
|
+
const [expandedGrainIds, setExpandedGrainIds] = useState(new Set());
|
|
19
|
+
const [deactivating, setDeactivating] = useState(new Set());
|
|
20
|
+
|
|
21
|
+
const MAX_VISIBLE_GRAIN = 10;
|
|
22
|
+
|
|
23
|
+
// Fetch pre-aggregations for this node
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const fetchPreaggs = async () => {
|
|
26
|
+
if (!node?.name) return;
|
|
27
|
+
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await djClient.listPreaggs({
|
|
33
|
+
node_name: node.name,
|
|
34
|
+
include_stale: true,
|
|
35
|
+
});
|
|
36
|
+
if (result._error) {
|
|
37
|
+
setError(result.message);
|
|
38
|
+
} else {
|
|
39
|
+
setPreaggs(result.items || []);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(err.message || 'Failed to load pre-aggregations');
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fetchPreaggs();
|
|
49
|
+
}, [node?.name, djClient]);
|
|
50
|
+
|
|
51
|
+
// Group pre-aggs by staleness
|
|
52
|
+
const { currentPreaggs, stalePreaggs } = useMemo(() => {
|
|
53
|
+
const currentVersion = node?.version;
|
|
54
|
+
const current = [];
|
|
55
|
+
const stale = [];
|
|
56
|
+
|
|
57
|
+
preaggs.forEach(preagg => {
|
|
58
|
+
if (preagg.node_version === currentVersion) {
|
|
59
|
+
current.push(preagg);
|
|
60
|
+
} else {
|
|
61
|
+
stale.push(preagg);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { currentPreaggs: current, stalePreaggs: stale };
|
|
66
|
+
}, [preaggs, node?.version]);
|
|
67
|
+
|
|
68
|
+
// Auto-expand the first current pre-agg when data loads
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (currentPreaggs.length > 0 && expandedIds.size === 0) {
|
|
71
|
+
setExpandedIds(new Set([currentPreaggs[0].id]));
|
|
72
|
+
}
|
|
73
|
+
}, [currentPreaggs]);
|
|
74
|
+
|
|
75
|
+
// Toggle expanded state for a pre-agg row
|
|
76
|
+
const toggleExpanded = id => {
|
|
77
|
+
setExpandedIds(prev => {
|
|
78
|
+
const next = new Set(prev);
|
|
79
|
+
if (next.has(id)) {
|
|
80
|
+
next.delete(id);
|
|
81
|
+
} else {
|
|
82
|
+
next.add(id);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Deactivate a single pre-agg workflow
|
|
89
|
+
const handleDeactivate = async preaggId => {
|
|
90
|
+
if (
|
|
91
|
+
!window.confirm(
|
|
92
|
+
'Are you sure you want to deactivate this workflow? ' +
|
|
93
|
+
'The materialization will stop running.',
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setDeactivating(prev => new Set(prev).add(preaggId));
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const result = await djClient.deactivatePreaggWorkflow(preaggId);
|
|
103
|
+
if (result._error) {
|
|
104
|
+
alert(`Failed to deactivate: ${result.message}`);
|
|
105
|
+
} else {
|
|
106
|
+
// Refresh the list
|
|
107
|
+
const refreshed = await djClient.listPreaggs({
|
|
108
|
+
node_name: node.name,
|
|
109
|
+
include_stale: true,
|
|
110
|
+
});
|
|
111
|
+
if (!refreshed._error) {
|
|
112
|
+
setPreaggs(refreshed.items || []);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
alert(`Error: ${err.message}`);
|
|
117
|
+
} finally {
|
|
118
|
+
setDeactivating(prev => {
|
|
119
|
+
const next = new Set(prev);
|
|
120
|
+
next.delete(preaggId);
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Bulk deactivate all stale workflows
|
|
127
|
+
const handleDeactivateAllStale = async () => {
|
|
128
|
+
const activeStale = stalePreaggs.filter(
|
|
129
|
+
p => p.workflow_status === 'active',
|
|
130
|
+
);
|
|
131
|
+
if (activeStale.length === 0) {
|
|
132
|
+
alert('No active stale workflows to deactivate.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
!window.confirm(
|
|
138
|
+
`Are you sure you want to deactivate ${activeStale.length} stale workflow(s)? ` +
|
|
139
|
+
'These materializations are from older node versions and will stop running.',
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setDeactivating(prev => {
|
|
146
|
+
const next = new Set(prev);
|
|
147
|
+
activeStale.forEach(p => next.add(p.id));
|
|
148
|
+
return next;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = await djClient.bulkDeactivatePreaggWorkflows(
|
|
153
|
+
node.name,
|
|
154
|
+
true,
|
|
155
|
+
);
|
|
156
|
+
if (result._error) {
|
|
157
|
+
alert(`Failed to deactivate: ${result.message}`);
|
|
158
|
+
} else {
|
|
159
|
+
// Refresh the list
|
|
160
|
+
const refreshed = await djClient.listPreaggs({
|
|
161
|
+
node_name: node.name,
|
|
162
|
+
include_stale: true,
|
|
163
|
+
});
|
|
164
|
+
if (!refreshed._error) {
|
|
165
|
+
setPreaggs(refreshed.items || []);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
alert(`Error: ${err.message}`);
|
|
170
|
+
} finally {
|
|
171
|
+
setDeactivating(new Set());
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Format cron expression to human-readable
|
|
176
|
+
const formatSchedule = schedule => {
|
|
177
|
+
if (!schedule) return 'Not scheduled';
|
|
178
|
+
try {
|
|
179
|
+
return cronstrue.toString(schedule);
|
|
180
|
+
} catch {
|
|
181
|
+
return schedule;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Render a single pre-agg row
|
|
186
|
+
const renderPreaggRow = (preagg, isStale = false) => {
|
|
187
|
+
const isExpanded = expandedIds.has(preagg.id);
|
|
188
|
+
const isDeactivating = deactivating.has(preagg.id);
|
|
189
|
+
const hasActiveWorkflow = preagg.workflow_status === 'active';
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
key={preagg.id}
|
|
194
|
+
className={`preagg-row ${isStale ? 'preagg-row--stale' : ''}`}
|
|
195
|
+
>
|
|
196
|
+
{/* Collapsed header row */}
|
|
197
|
+
<div
|
|
198
|
+
className="preagg-row-header"
|
|
199
|
+
onClick={() => toggleExpanded(preagg.id)}
|
|
200
|
+
>
|
|
201
|
+
<span className="preagg-row-toggle">
|
|
202
|
+
{isExpanded ? '\u25BC' : '\u25B6'}
|
|
203
|
+
</span>
|
|
204
|
+
|
|
205
|
+
<div className="preagg-row-grain-chips">
|
|
206
|
+
{(() => {
|
|
207
|
+
const grainCols = preagg.grain_columns || [];
|
|
208
|
+
const maxVisible = MAX_VISIBLE_GRAIN;
|
|
209
|
+
const visibleCols = grainCols.slice(0, maxVisible);
|
|
210
|
+
const hiddenCount = grainCols.length - maxVisible;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{visibleCols.map((col, idx) => {
|
|
215
|
+
const parts = col.split('.');
|
|
216
|
+
const shortName = parts[parts.length - 1];
|
|
217
|
+
return (
|
|
218
|
+
<span key={idx} className="preagg-grain-chip">
|
|
219
|
+
{shortName}
|
|
220
|
+
</span>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
{hiddenCount > 0 && (
|
|
224
|
+
<span className="preagg-grain-chip preagg-grain-chip--more">
|
|
225
|
+
+{hiddenCount}
|
|
226
|
+
</span>
|
|
227
|
+
)}
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
})()}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<span className="preagg-row-measures">
|
|
234
|
+
{preagg.measures?.length || 0} measure
|
|
235
|
+
{(preagg.measures?.length || 0) !== 1 ? 's' : ''}
|
|
236
|
+
</span>
|
|
237
|
+
|
|
238
|
+
{preagg.related_metrics?.length > 0 && (
|
|
239
|
+
<span className="preagg-metric-count-badge">
|
|
240
|
+
{preagg.related_metrics.length} metric
|
|
241
|
+
{preagg.related_metrics.length !== 1 ? 's' : ''}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{hasActiveWorkflow ? (
|
|
246
|
+
<span className="preagg-status-badge preagg-status-badge--active">
|
|
247
|
+
Active
|
|
248
|
+
</span>
|
|
249
|
+
) : preagg.workflow_status === 'paused' ? (
|
|
250
|
+
<span className="preagg-status-badge preagg-status-badge--paused">
|
|
251
|
+
Paused
|
|
252
|
+
</span>
|
|
253
|
+
) : (
|
|
254
|
+
<span className="preagg-status-badge preagg-status-badge--pending">
|
|
255
|
+
Pending
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{preagg.schedule && (
|
|
260
|
+
<span className="preagg-row-schedule">
|
|
261
|
+
{formatSchedule(preagg.schedule).toLowerCase()}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{isStale && (
|
|
266
|
+
<span className="preagg-row-version">
|
|
267
|
+
was {preagg.node_version}
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Expanded details */}
|
|
273
|
+
{isExpanded && (
|
|
274
|
+
<div
|
|
275
|
+
className={`preagg-details ${
|
|
276
|
+
isStale ? 'preagg-details--stale' : ''
|
|
277
|
+
}`}
|
|
278
|
+
>
|
|
279
|
+
{isStale && (
|
|
280
|
+
<div className="preagg-stale-banner">
|
|
281
|
+
<span className="preagg-stale-banner-icon">⚠️</span>
|
|
282
|
+
<div>
|
|
283
|
+
<strong>Built for {preagg.node_version}</strong> — current is{' '}
|
|
284
|
+
{node.version}
|
|
285
|
+
<br />
|
|
286
|
+
<span className="preagg-stale-banner-text">
|
|
287
|
+
This workflow is still running but won't be used for
|
|
288
|
+
queries.
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
<div className="preagg-stack">
|
|
295
|
+
{/* Config + Grain side by side */}
|
|
296
|
+
<div className="preagg-two-column">
|
|
297
|
+
{/* Config */}
|
|
298
|
+
<div>
|
|
299
|
+
<div className="preagg-card-label">Config</div>
|
|
300
|
+
<div className="preagg-card">
|
|
301
|
+
{/* Table-style key-value pairs */}
|
|
302
|
+
<table className="preagg-config-table">
|
|
303
|
+
<tbody>
|
|
304
|
+
<tr>
|
|
305
|
+
<td className="preagg-config-key">Strategy</td>
|
|
306
|
+
<td className="preagg-config-value">
|
|
307
|
+
{preagg.strategy
|
|
308
|
+
? labelize(preagg.strategy)
|
|
309
|
+
: 'Not set'}
|
|
310
|
+
</td>
|
|
311
|
+
</tr>
|
|
312
|
+
<tr>
|
|
313
|
+
<td className="preagg-config-key">Schedule</td>
|
|
314
|
+
<td className="preagg-config-value">
|
|
315
|
+
{preagg.schedule ? (
|
|
316
|
+
<>
|
|
317
|
+
{formatSchedule(preagg.schedule)}
|
|
318
|
+
<span className="preagg-config-schedule-cron">
|
|
319
|
+
({preagg.schedule})
|
|
320
|
+
</span>
|
|
321
|
+
</>
|
|
322
|
+
) : (
|
|
323
|
+
'Not scheduled'
|
|
324
|
+
)}
|
|
325
|
+
</td>
|
|
326
|
+
</tr>
|
|
327
|
+
{preagg.lookback_window && (
|
|
328
|
+
<tr>
|
|
329
|
+
<td className="preagg-config-key">Lookback</td>
|
|
330
|
+
<td className="preagg-config-value">
|
|
331
|
+
{preagg.lookback_window}
|
|
332
|
+
</td>
|
|
333
|
+
</tr>
|
|
334
|
+
)}
|
|
335
|
+
{preagg.max_partition &&
|
|
336
|
+
preagg.max_partition.length > 0 && (
|
|
337
|
+
<tr>
|
|
338
|
+
<td className="preagg-config-key">
|
|
339
|
+
Max Partition
|
|
340
|
+
</td>
|
|
341
|
+
<td className="preagg-config-value">
|
|
342
|
+
<code>{preagg.max_partition.join(', ')}</code>
|
|
343
|
+
</td>
|
|
344
|
+
</tr>
|
|
345
|
+
)}
|
|
346
|
+
</tbody>
|
|
347
|
+
</table>
|
|
348
|
+
|
|
349
|
+
{/* Actions */}
|
|
350
|
+
<div className="preagg-actions">
|
|
351
|
+
{/* Workflow buttons - one per URL */}
|
|
352
|
+
{preagg.workflow_urls?.map((wf, idx) => {
|
|
353
|
+
const label = wf.label || 'Workflow';
|
|
354
|
+
const capitalizedLabel =
|
|
355
|
+
label.charAt(0).toUpperCase() + label.slice(1);
|
|
356
|
+
return (
|
|
357
|
+
<a
|
|
358
|
+
key={idx}
|
|
359
|
+
href={wf.url}
|
|
360
|
+
target="_blank"
|
|
361
|
+
rel="noopener noreferrer"
|
|
362
|
+
className="preagg-action-btn"
|
|
363
|
+
>
|
|
364
|
+
{capitalizedLabel}
|
|
365
|
+
</a>
|
|
366
|
+
);
|
|
367
|
+
})}
|
|
368
|
+
|
|
369
|
+
{hasActiveWorkflow && (
|
|
370
|
+
<button
|
|
371
|
+
className={`preagg-action-btn preagg-action-btn--danger`}
|
|
372
|
+
disabled={isDeactivating}
|
|
373
|
+
onClick={e => {
|
|
374
|
+
e.stopPropagation();
|
|
375
|
+
handleDeactivate(preagg.id);
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
{isDeactivating ? 'Deactivating...' : 'Deactivate'}
|
|
379
|
+
</button>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Grain */}
|
|
386
|
+
<div>
|
|
387
|
+
<div className="preagg-card-label">Grain</div>
|
|
388
|
+
<div className="preagg-card preagg-card--compact">
|
|
389
|
+
<div className="preagg-grain-list">
|
|
390
|
+
{(() => {
|
|
391
|
+
const grainCols = preagg.grain_columns || [];
|
|
392
|
+
const isGrainExpanded = expandedGrainIds.has(preagg.id);
|
|
393
|
+
const visibleCols = isGrainExpanded
|
|
394
|
+
? grainCols
|
|
395
|
+
: grainCols.slice(0, MAX_VISIBLE_GRAIN);
|
|
396
|
+
const hiddenCount =
|
|
397
|
+
grainCols.length - MAX_VISIBLE_GRAIN;
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
{visibleCols.map((col, idx) => {
|
|
402
|
+
const parts = col.split('.');
|
|
403
|
+
const nodeName = parts.slice(0, -1).join('.');
|
|
404
|
+
return (
|
|
405
|
+
<a
|
|
406
|
+
key={idx}
|
|
407
|
+
href={`/nodes/${nodeName}`}
|
|
408
|
+
title={`View ${nodeName}`}
|
|
409
|
+
className="preagg-grain-badge"
|
|
410
|
+
>
|
|
411
|
+
{col}
|
|
412
|
+
</a>
|
|
413
|
+
);
|
|
414
|
+
})}
|
|
415
|
+
{!isGrainExpanded && hiddenCount > 0 && (
|
|
416
|
+
<button
|
|
417
|
+
className="preagg-expand-btn"
|
|
418
|
+
onClick={e => {
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
setExpandedGrainIds(prev => {
|
|
421
|
+
const next = new Set(prev);
|
|
422
|
+
next.add(preagg.id);
|
|
423
|
+
return next;
|
|
424
|
+
});
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
+{hiddenCount} more
|
|
428
|
+
</button>
|
|
429
|
+
)}
|
|
430
|
+
{isGrainExpanded && hiddenCount > 0 && (
|
|
431
|
+
<button
|
|
432
|
+
className="preagg-expand-btn"
|
|
433
|
+
onClick={e => {
|
|
434
|
+
e.stopPropagation();
|
|
435
|
+
setExpandedGrainIds(prev => {
|
|
436
|
+
const next = new Set(prev);
|
|
437
|
+
next.delete(preagg.id);
|
|
438
|
+
return next;
|
|
439
|
+
});
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
Show less
|
|
443
|
+
</button>
|
|
444
|
+
)}
|
|
445
|
+
</>
|
|
446
|
+
);
|
|
447
|
+
})()}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Measures */}
|
|
454
|
+
<div>
|
|
455
|
+
<div className="preagg-card-label preagg-card-label--with-info">
|
|
456
|
+
Measures
|
|
457
|
+
<span
|
|
458
|
+
className="preagg-info-icon"
|
|
459
|
+
title="Pre-computed aggregations stored in this pre-aggregation. At query time, DJ uses these to avoid re-scanning raw data."
|
|
460
|
+
>
|
|
461
|
+
ⓘ
|
|
462
|
+
</span>
|
|
463
|
+
</div>
|
|
464
|
+
<div
|
|
465
|
+
className="preagg-card preagg-card--table"
|
|
466
|
+
style={{ border: '1px solid #e2e8f0' }}
|
|
467
|
+
>
|
|
468
|
+
<table className="preagg-measures-table">
|
|
469
|
+
<thead>
|
|
470
|
+
<tr>
|
|
471
|
+
<th>Name</th>
|
|
472
|
+
<th>
|
|
473
|
+
Aggregation
|
|
474
|
+
<span
|
|
475
|
+
className="preagg-info-icon"
|
|
476
|
+
title="Phase 1: How raw data is aggregated when building the pre-agg table"
|
|
477
|
+
>
|
|
478
|
+
ⓘ
|
|
479
|
+
</span>
|
|
480
|
+
</th>
|
|
481
|
+
<th>
|
|
482
|
+
Merge
|
|
483
|
+
<span
|
|
484
|
+
className="preagg-info-icon"
|
|
485
|
+
title="Phase 2: How pre-aggregated values are combined at query time"
|
|
486
|
+
>
|
|
487
|
+
ⓘ
|
|
488
|
+
</span>
|
|
489
|
+
</th>
|
|
490
|
+
<th>
|
|
491
|
+
Rule
|
|
492
|
+
<span
|
|
493
|
+
className="preagg-info-icon"
|
|
494
|
+
title="Additivity: FULL = can roll up across any dimension"
|
|
495
|
+
>
|
|
496
|
+
ⓘ
|
|
497
|
+
</span>
|
|
498
|
+
</th>
|
|
499
|
+
<th>
|
|
500
|
+
Used By
|
|
501
|
+
<span
|
|
502
|
+
className="preagg-info-icon"
|
|
503
|
+
title="Metrics that use this measure"
|
|
504
|
+
>
|
|
505
|
+
ⓘ
|
|
506
|
+
</span>
|
|
507
|
+
</th>
|
|
508
|
+
</tr>
|
|
509
|
+
</thead>
|
|
510
|
+
<tbody>
|
|
511
|
+
{preagg.measures?.map((measure, idx) => (
|
|
512
|
+
<tr key={idx}>
|
|
513
|
+
<td className="preagg-measure-name">
|
|
514
|
+
{measure.name}
|
|
515
|
+
</td>
|
|
516
|
+
<td>
|
|
517
|
+
<code className="preagg-agg-badge">
|
|
518
|
+
{measure.aggregation
|
|
519
|
+
? `${measure.aggregation}(${measure.expression})`
|
|
520
|
+
: measure.expression}
|
|
521
|
+
</code>
|
|
522
|
+
</td>
|
|
523
|
+
<td>
|
|
524
|
+
{measure.merge && (
|
|
525
|
+
<code className="preagg-merge-badge">
|
|
526
|
+
{measure.merge}
|
|
527
|
+
</code>
|
|
528
|
+
)}
|
|
529
|
+
</td>
|
|
530
|
+
<td>
|
|
531
|
+
{measure.rule && (
|
|
532
|
+
<span className="preagg-rule-badge">
|
|
533
|
+
{typeof measure.rule === 'object'
|
|
534
|
+
? measure.rule.type || ''
|
|
535
|
+
: measure.rule}
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
</td>
|
|
539
|
+
<td>
|
|
540
|
+
{measure.used_by_metrics?.length > 0 && (
|
|
541
|
+
<div className="preagg-metrics-list">
|
|
542
|
+
{measure.used_by_metrics.map((metric, mIdx) => (
|
|
543
|
+
<a
|
|
544
|
+
key={mIdx}
|
|
545
|
+
href={`/nodes/${metric.name}`}
|
|
546
|
+
title={metric.name}
|
|
547
|
+
className="preagg-metric-badge"
|
|
548
|
+
>
|
|
549
|
+
{metric.display_name ||
|
|
550
|
+
metric.name.split('.').pop()}
|
|
551
|
+
</a>
|
|
552
|
+
))}
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
</td>
|
|
556
|
+
</tr>
|
|
557
|
+
))}
|
|
558
|
+
</tbody>
|
|
559
|
+
</table>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
)}
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Loading state
|
|
570
|
+
if (loading) {
|
|
571
|
+
return <div className="preagg-loading">Loading pre-aggregations...</div>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Error state
|
|
575
|
+
if (error) {
|
|
576
|
+
return (
|
|
577
|
+
<div className="message alert preagg-error">
|
|
578
|
+
Error loading pre-aggregations: {error}
|
|
579
|
+
</div>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// No pre-aggs
|
|
584
|
+
if (preaggs.length === 0) {
|
|
585
|
+
return (
|
|
586
|
+
<div className="preagg-no-data">
|
|
587
|
+
<div className="message alert preagg-no-data-alert">
|
|
588
|
+
No pre-aggregations found for this node.
|
|
589
|
+
</div>
|
|
590
|
+
<p className="preagg-no-data-text">
|
|
591
|
+
Pre-aggregations are created when you use the{' '}
|
|
592
|
+
<a href="/query-planner">Query Planner</a> to plan materializations
|
|
593
|
+
for metrics derived from this node.
|
|
594
|
+
</p>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Calculate if there are active stale workflows
|
|
600
|
+
const activeStaleCount = stalePreaggs.filter(
|
|
601
|
+
p => p.workflow_status === 'active',
|
|
602
|
+
).length;
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
<div className="preagg-container">
|
|
606
|
+
{/* Current Version Section */}
|
|
607
|
+
<div className="preagg-section">
|
|
608
|
+
<div className="preagg-section-header">
|
|
609
|
+
<h3 className="preagg-section-title">
|
|
610
|
+
Current Pre-Aggregations ({node.version})
|
|
611
|
+
</h3>
|
|
612
|
+
<span className="preagg-section-count">
|
|
613
|
+
{currentPreaggs.length} pre-aggregation
|
|
614
|
+
{currentPreaggs.length !== 1 ? 's' : ''}
|
|
615
|
+
</span>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
{currentPreaggs.length > 0 ? (
|
|
619
|
+
currentPreaggs.map(preagg => renderPreaggRow(preagg, false))
|
|
620
|
+
) : (
|
|
621
|
+
<div className="preagg-empty">
|
|
622
|
+
No pre-aggregations for the current version.
|
|
623
|
+
</div>
|
|
624
|
+
)}
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Stale Section */}
|
|
628
|
+
{stalePreaggs.length > 0 && (
|
|
629
|
+
<div className="preagg-section">
|
|
630
|
+
<div className="preagg-section-header preagg-section-header--stale">
|
|
631
|
+
<div className="preagg-section-header-left">
|
|
632
|
+
<h3 className="preagg-section-title preagg-section-title--stale">
|
|
633
|
+
Stale Pre-Aggregations ({stalePreaggs.length})
|
|
634
|
+
</h3>
|
|
635
|
+
<span className="preagg-section-count preagg-section-count--stale">
|
|
636
|
+
{activeStaleCount} active workflow
|
|
637
|
+
{activeStaleCount !== 1 ? 's' : ''}
|
|
638
|
+
</span>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
{activeStaleCount > 0 && (
|
|
642
|
+
<button
|
|
643
|
+
className="preagg-action-btn preagg-action-btn--danger-fill"
|
|
644
|
+
onClick={handleDeactivateAllStale}
|
|
645
|
+
>
|
|
646
|
+
Deactivate All Stale
|
|
647
|
+
</button>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
{stalePreaggs.map(preagg => renderPreaggRow(preagg, true))}
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
@@ -342,8 +342,10 @@ export default function NodeValidateTab({ node, djClient }) {
|
|
|
342
342
|
.slice(0, 100)
|
|
343
343
|
.map((rowData, index) => (
|
|
344
344
|
<tr key={`data-row:${index}`}>
|
|
345
|
-
{rowData.map(rowValue => (
|
|
346
|
-
<td key={
|
|
345
|
+
{rowData.map((rowValue, colIndex) => (
|
|
346
|
+
<td key={`${index}-${colIndex}`}>
|
|
347
|
+
{rowValue}
|
|
348
|
+
</td>
|
|
347
349
|
))}
|
|
348
350
|
</tr>
|
|
349
351
|
))}
|