datajunction-ui 0.0.31 → 0.0.34
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/TODO.md +265 -0
- package/package.json +1 -1
- 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,654 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import NodePreAggregationsTab from '../NodePreAggregationsTab';
|
|
5
|
+
import DJClientContext from '../../../providers/djclient';
|
|
6
|
+
|
|
7
|
+
// Mock the CSS import
|
|
8
|
+
jest.mock('../../../../styles/preaggregations.css', () => ({}));
|
|
9
|
+
|
|
10
|
+
// Mock cronstrue - it's imported via require() in the component
|
|
11
|
+
jest.mock('cronstrue', () => ({
|
|
12
|
+
toString: cron => {
|
|
13
|
+
if (cron === '0 0 * * *') return 'At 12:00 AM';
|
|
14
|
+
if (cron === '0 * * * *') return 'Every hour';
|
|
15
|
+
return cron || 'Not scheduled';
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Mock labelize from utils/form
|
|
20
|
+
jest.mock('../../../../utils/form', () => ({
|
|
21
|
+
labelize: str => {
|
|
22
|
+
if (!str) return '';
|
|
23
|
+
// Convert snake_case/SCREAMING_SNAKE to Title Case
|
|
24
|
+
return str
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/_/g, ' ')
|
|
27
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const mockNode = {
|
|
32
|
+
name: 'default.orders_fact',
|
|
33
|
+
version: 'v1.0',
|
|
34
|
+
type: 'transform',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockPreaggs = {
|
|
38
|
+
items: [
|
|
39
|
+
{
|
|
40
|
+
id: 1,
|
|
41
|
+
node_revision_id: 1,
|
|
42
|
+
node_name: 'default.orders_fact',
|
|
43
|
+
node_version: 'v1.0',
|
|
44
|
+
grain_columns: [
|
|
45
|
+
'default.date_dim.date_id',
|
|
46
|
+
'default.customer_dim.customer_id',
|
|
47
|
+
],
|
|
48
|
+
measures: [
|
|
49
|
+
{
|
|
50
|
+
name: 'total_revenue',
|
|
51
|
+
expression: 'revenue',
|
|
52
|
+
aggregation: 'SUM',
|
|
53
|
+
merge: 'SUM',
|
|
54
|
+
rule: { type: 'full' },
|
|
55
|
+
used_by_metrics: [
|
|
56
|
+
{ name: 'default.revenue_metric', display_name: 'Revenue' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'order_count',
|
|
61
|
+
expression: '*',
|
|
62
|
+
aggregation: 'COUNT',
|
|
63
|
+
merge: 'SUM',
|
|
64
|
+
rule: { type: 'full' },
|
|
65
|
+
used_by_metrics: [
|
|
66
|
+
{ name: 'default.order_count_metric', display_name: 'Order Count' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
sql: 'SELECT date_id, customer_id, SUM(revenue), COUNT(*) FROM orders GROUP BY 1, 2',
|
|
71
|
+
grain_group_hash: 'abc123',
|
|
72
|
+
strategy: 'full',
|
|
73
|
+
schedule: '0 0 * * *',
|
|
74
|
+
lookback_window: null,
|
|
75
|
+
workflow_urls: [
|
|
76
|
+
{ label: 'scheduled', url: 'http://scheduler/workflow/123.main' },
|
|
77
|
+
],
|
|
78
|
+
workflow_status: 'active',
|
|
79
|
+
status: 'active',
|
|
80
|
+
materialized_table_ref: 'analytics.preaggs.orders_fact_abc123',
|
|
81
|
+
max_partition: ['2024', '01', '15'],
|
|
82
|
+
related_metrics: ['default.revenue_metric', 'default.order_count_metric'],
|
|
83
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
84
|
+
updated_at: '2024-01-15T00:00:00Z',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 2,
|
|
88
|
+
node_revision_id: 1,
|
|
89
|
+
node_name: 'default.orders_fact',
|
|
90
|
+
node_version: 'v1.0',
|
|
91
|
+
grain_columns: ['default.product_dim.category'],
|
|
92
|
+
measures: [
|
|
93
|
+
{
|
|
94
|
+
name: 'total_quantity',
|
|
95
|
+
expression: 'quantity',
|
|
96
|
+
aggregation: 'SUM',
|
|
97
|
+
merge: 'SUM',
|
|
98
|
+
rule: { type: 'full' },
|
|
99
|
+
used_by_metrics: null,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
sql: 'SELECT category, SUM(quantity) FROM orders GROUP BY 1',
|
|
103
|
+
grain_group_hash: 'def456',
|
|
104
|
+
strategy: 'incremental_time',
|
|
105
|
+
schedule: '0 * * * *',
|
|
106
|
+
lookback_window: '3 days',
|
|
107
|
+
workflow_urls: null,
|
|
108
|
+
workflow_status: null,
|
|
109
|
+
status: 'pending',
|
|
110
|
+
materialized_table_ref: null,
|
|
111
|
+
max_partition: null,
|
|
112
|
+
related_metrics: null,
|
|
113
|
+
created_at: '2024-01-10T00:00:00Z',
|
|
114
|
+
updated_at: null,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
total: 2,
|
|
118
|
+
limit: 50,
|
|
119
|
+
offset: 0,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const mockPreaggsWithStale = {
|
|
123
|
+
items: [
|
|
124
|
+
...mockPreaggs.items,
|
|
125
|
+
{
|
|
126
|
+
id: 3,
|
|
127
|
+
node_revision_id: 0,
|
|
128
|
+
node_name: 'default.orders_fact',
|
|
129
|
+
node_version: 'v0.9', // Stale version
|
|
130
|
+
grain_columns: ['default.date_dim.date_id'],
|
|
131
|
+
measures: [
|
|
132
|
+
{
|
|
133
|
+
name: 'old_revenue',
|
|
134
|
+
expression: 'revenue',
|
|
135
|
+
aggregation: 'SUM',
|
|
136
|
+
merge: 'SUM',
|
|
137
|
+
rule: { type: 'full' },
|
|
138
|
+
used_by_metrics: null,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
sql: 'SELECT date_id, SUM(revenue) FROM orders GROUP BY 1',
|
|
142
|
+
grain_group_hash: 'old123',
|
|
143
|
+
strategy: 'full',
|
|
144
|
+
schedule: '0 0 * * *',
|
|
145
|
+
workflow_urls: [
|
|
146
|
+
{ label: 'scheduled', url: 'http://scheduler/workflow/old.main' },
|
|
147
|
+
],
|
|
148
|
+
workflow_status: 'active',
|
|
149
|
+
status: 'active',
|
|
150
|
+
materialized_table_ref: 'analytics.preaggs.orders_fact_old',
|
|
151
|
+
max_partition: ['2024', '01', '10'],
|
|
152
|
+
related_metrics: null,
|
|
153
|
+
created_at: '2023-12-01T00:00:00Z',
|
|
154
|
+
updated_at: '2024-01-10T00:00:00Z',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
total: 3,
|
|
158
|
+
limit: 50,
|
|
159
|
+
offset: 0,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const createMockDjClient = (preaggs = mockPreaggs) => ({
|
|
163
|
+
DataJunctionAPI: {
|
|
164
|
+
listPreaggs: jest.fn().mockResolvedValue(preaggs),
|
|
165
|
+
deactivatePreaggWorkflow: jest.fn().mockResolvedValue({ status: 'none' }),
|
|
166
|
+
bulkDeactivatePreaggWorkflows: jest.fn().mockResolvedValue({
|
|
167
|
+
deactivated_count: 1,
|
|
168
|
+
deactivated: [{ id: 3 }],
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const renderWithContext = (component, djClient) => {
|
|
174
|
+
return render(
|
|
175
|
+
<MemoryRouter>
|
|
176
|
+
<DJClientContext.Provider value={djClient}>
|
|
177
|
+
{component}
|
|
178
|
+
</DJClientContext.Provider>
|
|
179
|
+
</MemoryRouter>,
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
describe('<NodePreAggregationsTab />', () => {
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
jest.clearAllMocks();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('Loading and Empty States', () => {
|
|
189
|
+
it('shows loading state initially', () => {
|
|
190
|
+
const djClient = createMockDjClient();
|
|
191
|
+
// Make the promise never resolve to keep loading state
|
|
192
|
+
djClient.DataJunctionAPI.listPreaggs.mockReturnValue(
|
|
193
|
+
new Promise(() => {}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
197
|
+
|
|
198
|
+
expect(
|
|
199
|
+
screen.getByText('Loading pre-aggregations...'),
|
|
200
|
+
).toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('shows empty state when no pre-aggregations exist', async () => {
|
|
204
|
+
const djClient = createMockDjClient({
|
|
205
|
+
items: [],
|
|
206
|
+
total: 0,
|
|
207
|
+
limit: 50,
|
|
208
|
+
offset: 0,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
212
|
+
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(
|
|
215
|
+
screen.getByText('No pre-aggregations found for this node.'),
|
|
216
|
+
).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('shows error state when API fails', async () => {
|
|
221
|
+
const djClient = createMockDjClient();
|
|
222
|
+
djClient.DataJunctionAPI.listPreaggs.mockResolvedValue({
|
|
223
|
+
_error: true,
|
|
224
|
+
message: 'Failed to fetch',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(
|
|
231
|
+
screen.getByText(/Error loading pre-aggregations/),
|
|
232
|
+
).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('Section Headers', () => {
|
|
238
|
+
it('renders "Current Pre-Aggregations" section header with version', async () => {
|
|
239
|
+
const djClient = createMockDjClient();
|
|
240
|
+
|
|
241
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
242
|
+
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(
|
|
245
|
+
screen.getByText('Current Pre-Aggregations (v1.0)'),
|
|
246
|
+
).toBeInTheDocument();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('renders "Stale Pre-Aggregations" section when stale preaggs exist', async () => {
|
|
251
|
+
const djClient = createMockDjClient(mockPreaggsWithStale);
|
|
252
|
+
|
|
253
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
254
|
+
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(
|
|
257
|
+
screen.getByText('Stale Pre-Aggregations (1)'),
|
|
258
|
+
).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('does not render stale section when no stale preaggs exist', async () => {
|
|
263
|
+
const djClient = createMockDjClient();
|
|
264
|
+
|
|
265
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(
|
|
269
|
+
screen.getByText('Current Pre-Aggregations (v1.0)'),
|
|
270
|
+
).toBeInTheDocument();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
screen.queryByText(/Stale Pre-Aggregations/),
|
|
275
|
+
).not.toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('Pre-agg Row Header', () => {
|
|
280
|
+
it('renders grain columns as chips', async () => {
|
|
281
|
+
const djClient = createMockDjClient();
|
|
282
|
+
|
|
283
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
284
|
+
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
// Should show short names from grain columns
|
|
287
|
+
expect(screen.getByText('date_id')).toBeInTheDocument();
|
|
288
|
+
expect(screen.getByText('customer_id')).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('renders measure count', async () => {
|
|
293
|
+
const djClient = createMockDjClient();
|
|
294
|
+
|
|
295
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
296
|
+
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
expect(screen.getByText('2 measures')).toBeInTheDocument();
|
|
299
|
+
expect(screen.getByText('1 measure')).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('renders metric count badge when related metrics exist', async () => {
|
|
304
|
+
const djClient = createMockDjClient();
|
|
305
|
+
|
|
306
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(screen.getByText('2 metrics')).toBeInTheDocument();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('renders status badges', async () => {
|
|
314
|
+
const djClient = createMockDjClient();
|
|
315
|
+
|
|
316
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
317
|
+
|
|
318
|
+
await waitFor(() => {
|
|
319
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
320
|
+
expect(screen.getByText('Pending')).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('renders schedule in human-readable format', async () => {
|
|
325
|
+
const djClient = createMockDjClient();
|
|
326
|
+
|
|
327
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
328
|
+
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
// Schedule appears in both header and config section, so use getAllByText
|
|
331
|
+
const scheduleElements = screen.getAllByText(/at 12:00 am/i);
|
|
332
|
+
expect(scheduleElements.length).toBeGreaterThan(0);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('Expanded Details', () => {
|
|
338
|
+
it('expands first pre-agg by default', async () => {
|
|
339
|
+
const djClient = createMockDjClient();
|
|
340
|
+
|
|
341
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
// Config section should be visible for the first expanded preagg
|
|
345
|
+
expect(screen.getByText('Config')).toBeInTheDocument();
|
|
346
|
+
expect(screen.getByText('Grain')).toBeInTheDocument();
|
|
347
|
+
expect(screen.getByText('Measures')).toBeInTheDocument();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('shows Config section with strategy and schedule', async () => {
|
|
352
|
+
const djClient = createMockDjClient();
|
|
353
|
+
|
|
354
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
355
|
+
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(screen.getByText('Strategy')).toBeInTheDocument();
|
|
358
|
+
expect(screen.getByText('Full')).toBeInTheDocument();
|
|
359
|
+
expect(screen.getByText('Schedule')).toBeInTheDocument();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('shows Grain section with dimension links', async () => {
|
|
364
|
+
const djClient = createMockDjClient();
|
|
365
|
+
|
|
366
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
367
|
+
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
// Full grain column names should appear in expanded section
|
|
370
|
+
const grainBadges = screen.getAllByText(
|
|
371
|
+
/default\.(date_dim|customer_dim)\./,
|
|
372
|
+
);
|
|
373
|
+
expect(grainBadges.length).toBeGreaterThan(0);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('shows Measures table with aggregation and merge info', async () => {
|
|
378
|
+
const djClient = createMockDjClient();
|
|
379
|
+
|
|
380
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
381
|
+
|
|
382
|
+
await waitFor(() => {
|
|
383
|
+
expect(screen.getByText('total_revenue')).toBeInTheDocument();
|
|
384
|
+
expect(screen.getByText('SUM(revenue)')).toBeInTheDocument();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('shows metrics that use each measure', async () => {
|
|
389
|
+
const djClient = createMockDjClient();
|
|
390
|
+
|
|
391
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
392
|
+
|
|
393
|
+
await waitFor(() => {
|
|
394
|
+
// Display names should appear
|
|
395
|
+
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
|
396
|
+
expect(screen.getByText('Order Count')).toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('toggles expansion when clicking row header', async () => {
|
|
401
|
+
// Use single preagg to simplify test
|
|
402
|
+
const singlePreagg = {
|
|
403
|
+
items: [mockPreaggs.items[0]],
|
|
404
|
+
total: 1,
|
|
405
|
+
limit: 50,
|
|
406
|
+
offset: 0,
|
|
407
|
+
};
|
|
408
|
+
const djClient = createMockDjClient(singlePreagg);
|
|
409
|
+
|
|
410
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
411
|
+
|
|
412
|
+
// Wait for initial render with expanded state
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
expect(screen.getByText('Config')).toBeInTheDocument();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Find and click the row header to collapse it
|
|
418
|
+
const measureText = screen.getByText('2 measures');
|
|
419
|
+
fireEvent.click(measureText.closest('.preagg-row-header'));
|
|
420
|
+
|
|
421
|
+
// After collapse, Config should no longer be visible
|
|
422
|
+
await waitFor(
|
|
423
|
+
() => {
|
|
424
|
+
expect(screen.queryByText('Config')).not.toBeInTheDocument();
|
|
425
|
+
},
|
|
426
|
+
{ timeout: 2000 },
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('Workflow Actions', () => {
|
|
432
|
+
it('renders workflow button with capitalized label', async () => {
|
|
433
|
+
const djClient = createMockDjClient();
|
|
434
|
+
|
|
435
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
436
|
+
|
|
437
|
+
await waitFor(() => {
|
|
438
|
+
const scheduledBtn = screen.getByText('Scheduled');
|
|
439
|
+
expect(scheduledBtn).toBeInTheDocument();
|
|
440
|
+
expect(scheduledBtn.closest('a')).toHaveAttribute(
|
|
441
|
+
'href',
|
|
442
|
+
'http://scheduler/workflow/123.main',
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('renders deactivate button for active workflows', async () => {
|
|
448
|
+
const djClient = createMockDjClient();
|
|
449
|
+
|
|
450
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
451
|
+
|
|
452
|
+
await waitFor(() => {
|
|
453
|
+
expect(screen.getByText('Deactivate')).toBeInTheDocument();
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('calls deactivatePreaggWorkflow when deactivate is clicked', async () => {
|
|
458
|
+
const djClient = createMockDjClient();
|
|
459
|
+
window.confirm = jest.fn(() => true);
|
|
460
|
+
|
|
461
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
462
|
+
|
|
463
|
+
await waitFor(
|
|
464
|
+
() => {
|
|
465
|
+
expect(screen.getByText('Deactivate')).toBeInTheDocument();
|
|
466
|
+
},
|
|
467
|
+
{ timeout: 2000 },
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
fireEvent.click(screen.getByText('Deactivate'));
|
|
471
|
+
|
|
472
|
+
await waitFor(
|
|
473
|
+
() => {
|
|
474
|
+
expect(
|
|
475
|
+
djClient.DataJunctionAPI.deactivatePreaggWorkflow,
|
|
476
|
+
).toHaveBeenCalledWith(1);
|
|
477
|
+
},
|
|
478
|
+
{ timeout: 2000 },
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('Stale Pre-aggregations', () => {
|
|
484
|
+
it('shows stale version warning in row header', async () => {
|
|
485
|
+
const djClient = createMockDjClient(mockPreaggsWithStale);
|
|
486
|
+
|
|
487
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
488
|
+
|
|
489
|
+
await waitFor(() => {
|
|
490
|
+
expect(screen.getByText('was v0.9')).toBeInTheDocument();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('shows "Deactivate All Stale" button when active stale workflows exist', async () => {
|
|
495
|
+
const djClient = createMockDjClient(mockPreaggsWithStale);
|
|
496
|
+
|
|
497
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
498
|
+
|
|
499
|
+
await waitFor(() => {
|
|
500
|
+
expect(screen.getByText('Deactivate All Stale')).toBeInTheDocument();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('calls bulkDeactivatePreaggWorkflows when "Deactivate All Stale" is clicked', async () => {
|
|
505
|
+
const djClient = createMockDjClient(mockPreaggsWithStale);
|
|
506
|
+
window.confirm = jest.fn(() => true);
|
|
507
|
+
|
|
508
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
509
|
+
|
|
510
|
+
await waitFor(
|
|
511
|
+
() => {
|
|
512
|
+
expect(screen.getByText('Deactivate All Stale')).toBeInTheDocument();
|
|
513
|
+
},
|
|
514
|
+
{ timeout: 2000 },
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
fireEvent.click(screen.getByText('Deactivate All Stale'));
|
|
518
|
+
|
|
519
|
+
await waitFor(
|
|
520
|
+
() => {
|
|
521
|
+
expect(
|
|
522
|
+
djClient.DataJunctionAPI.bulkDeactivatePreaggWorkflows,
|
|
523
|
+
).toHaveBeenCalledWith('default.orders_fact', true);
|
|
524
|
+
},
|
|
525
|
+
{ timeout: 2000 },
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe('Grain Truncation', () => {
|
|
531
|
+
it('shows "+N more" button when grain has more than MAX_VISIBLE_GRAIN columns', async () => {
|
|
532
|
+
const manyGrainPreaggs = {
|
|
533
|
+
items: [
|
|
534
|
+
{
|
|
535
|
+
...mockPreaggs.items[0],
|
|
536
|
+
grain_columns: [
|
|
537
|
+
'default.dim1.col1',
|
|
538
|
+
'default.dim2.col2',
|
|
539
|
+
'default.dim3.col3',
|
|
540
|
+
'default.dim4.col4',
|
|
541
|
+
'default.dim5.col5',
|
|
542
|
+
'default.dim6.col6',
|
|
543
|
+
'default.dim7.col7',
|
|
544
|
+
'default.dim8.col8',
|
|
545
|
+
'default.dim9.col9',
|
|
546
|
+
'default.dim10.col10',
|
|
547
|
+
'default.dim11.col11',
|
|
548
|
+
'default.dim12.col12',
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
total: 1,
|
|
553
|
+
limit: 50,
|
|
554
|
+
offset: 0,
|
|
555
|
+
};
|
|
556
|
+
const djClient = createMockDjClient(manyGrainPreaggs);
|
|
557
|
+
|
|
558
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
559
|
+
|
|
560
|
+
await waitFor(() => {
|
|
561
|
+
// Should show "+N more" since there are 12 columns and MAX_VISIBLE_GRAIN is 10
|
|
562
|
+
expect(screen.getByText('+2 more')).toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('shows "Show less" after expanding grain list', async () => {
|
|
567
|
+
const manyGrainPreaggs = {
|
|
568
|
+
items: [
|
|
569
|
+
{
|
|
570
|
+
...mockPreaggs.items[0],
|
|
571
|
+
grain_columns: [
|
|
572
|
+
'default.dim1.col1',
|
|
573
|
+
'default.dim2.col2',
|
|
574
|
+
'default.dim3.col3',
|
|
575
|
+
'default.dim4.col4',
|
|
576
|
+
'default.dim5.col5',
|
|
577
|
+
'default.dim6.col6',
|
|
578
|
+
'default.dim7.col7',
|
|
579
|
+
'default.dim8.col8',
|
|
580
|
+
'default.dim9.col9',
|
|
581
|
+
'default.dim10.col10',
|
|
582
|
+
'default.dim11.col11',
|
|
583
|
+
'default.dim12.col12',
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
total: 1,
|
|
588
|
+
limit: 50,
|
|
589
|
+
offset: 0,
|
|
590
|
+
};
|
|
591
|
+
const djClient = createMockDjClient(manyGrainPreaggs);
|
|
592
|
+
|
|
593
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
594
|
+
|
|
595
|
+
await waitFor(
|
|
596
|
+
() => {
|
|
597
|
+
expect(screen.getByText('+2 more')).toBeInTheDocument();
|
|
598
|
+
},
|
|
599
|
+
{ timeout: 2000 },
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Click to expand
|
|
603
|
+
fireEvent.click(screen.getByText('+2 more'));
|
|
604
|
+
|
|
605
|
+
await waitFor(
|
|
606
|
+
() => {
|
|
607
|
+
expect(screen.getByText('Show less')).toBeInTheDocument();
|
|
608
|
+
// All columns should now be visible
|
|
609
|
+
expect(screen.getByText('default.dim12.col12')).toBeInTheDocument();
|
|
610
|
+
},
|
|
611
|
+
{ timeout: 2000 },
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
describe('API Integration', () => {
|
|
617
|
+
it('calls listPreaggs with include_stale=true', async () => {
|
|
618
|
+
const djClient = createMockDjClient();
|
|
619
|
+
|
|
620
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
621
|
+
|
|
622
|
+
await waitFor(() => {
|
|
623
|
+
expect(djClient.DataJunctionAPI.listPreaggs).toHaveBeenCalledWith({
|
|
624
|
+
node_name: 'default.orders_fact',
|
|
625
|
+
include_stale: true,
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('refreshes list after deactivate action', async () => {
|
|
631
|
+
const djClient = createMockDjClient();
|
|
632
|
+
window.confirm = jest.fn(() => true);
|
|
633
|
+
|
|
634
|
+
renderWithContext(<NodePreAggregationsTab node={mockNode} />, djClient);
|
|
635
|
+
|
|
636
|
+
await waitFor(
|
|
637
|
+
() => {
|
|
638
|
+
expect(screen.getByText('Deactivate')).toBeInTheDocument();
|
|
639
|
+
},
|
|
640
|
+
{ timeout: 2000 },
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
fireEvent.click(screen.getByText('Deactivate'));
|
|
644
|
+
|
|
645
|
+
await waitFor(
|
|
646
|
+
() => {
|
|
647
|
+
// Should be called twice: initial load + refresh after deactivate
|
|
648
|
+
expect(djClient.DataJunctionAPI.listPreaggs).toHaveBeenCalledTimes(2);
|
|
649
|
+
},
|
|
650
|
+
{ timeout: 2000 },
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
});
|
|
@@ -11,6 +11,7 @@ import NotebookDownload from './NotebookDownload';
|
|
|
11
11
|
import DJClientContext from '../../providers/djclient';
|
|
12
12
|
import NodeValidateTab from './NodeValidateTab';
|
|
13
13
|
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
14
|
+
import NodePreAggregationsTab from './NodePreAggregationsTab';
|
|
14
15
|
import ClientCodePopover from './ClientCodePopover';
|
|
15
16
|
import WatchButton from './WatchNodeButton';
|
|
16
17
|
import NodesWithDimension from './NodesWithDimension';
|
|
@@ -131,7 +132,14 @@ export function NodePage() {
|
|
|
131
132
|
tabToDisplay = <NodeValidateTab node={node} djClient={djClient} />;
|
|
132
133
|
break;
|
|
133
134
|
case 'materializations':
|
|
134
|
-
|
|
135
|
+
// Cube nodes use cube-specific materialization tab
|
|
136
|
+
// Other nodes (transform, metric, dimension) use pre-aggregations tab
|
|
137
|
+
tabToDisplay =
|
|
138
|
+
node?.type === 'cube' ? (
|
|
139
|
+
<NodeMaterializationTab node={node} djClient={djClient} />
|
|
140
|
+
) : (
|
|
141
|
+
<NodePreAggregationsTab node={node} />
|
|
142
|
+
);
|
|
135
143
|
break;
|
|
136
144
|
case 'linked':
|
|
137
145
|
tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
|