datajunction-ui 0.0.26-alpha.0 → 0.0.27-alpha.0
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 +2 -2
- package/src/app/components/Search.jsx +41 -33
- package/src/app/components/__tests__/Search.test.jsx +46 -11
- package/src/app/index.tsx +1 -1
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
- package/src/app/pages/AddEditNodePage/index.jsx +61 -17
- package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
- package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
- package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
- package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
- package/src/app/services/DJService.js +492 -3
- package/src/app/services/__tests__/DJService.test.jsx +582 -0
- package/src/mocks/mockNodes.jsx +36 -0
- package/webpack.config.js +27 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
act,
|
|
7
|
+
} from '@testing-library/react';
|
|
2
8
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
9
|
import {
|
|
4
10
|
QueryOverviewPanel,
|
|
@@ -18,6 +24,13 @@ jest.mock('react-syntax-highlighter/src/styles/hljs', () => ({
|
|
|
18
24
|
atomOneLight: {},
|
|
19
25
|
}));
|
|
20
26
|
|
|
27
|
+
// Mock clipboard API
|
|
28
|
+
Object.assign(navigator, {
|
|
29
|
+
clipboard: {
|
|
30
|
+
writeText: jest.fn(),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
21
34
|
const mockMeasuresResult = {
|
|
22
35
|
grain_groups: [
|
|
23
36
|
{
|
|
@@ -121,7 +134,7 @@ describe('QueryOverviewPanel', () => {
|
|
|
121
134
|
describe('Header', () => {
|
|
122
135
|
it('renders the overview header', () => {
|
|
123
136
|
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
|
|
124
|
-
expect(screen.getByText('
|
|
137
|
+
expect(screen.getByText('Query Plan')).toBeInTheDocument();
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
it('shows metric and dimension counts', () => {
|
|
@@ -147,10 +160,11 @@ describe('QueryOverviewPanel', () => {
|
|
|
147
160
|
expect(screen.getByText('stock')).toBeInTheDocument();
|
|
148
161
|
});
|
|
149
162
|
|
|
150
|
-
it('shows
|
|
163
|
+
it('shows status badge for each pre-agg', () => {
|
|
164
|
+
// The updated UI shows status badges instead of aggregability badges
|
|
151
165
|
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
|
|
152
|
-
|
|
153
|
-
expect(screen.
|
|
166
|
+
// All pre-aggs without materialization config show "Not Set" status
|
|
167
|
+
expect(screen.getAllByText('○ Not Set').length).toBe(2);
|
|
154
168
|
});
|
|
155
169
|
|
|
156
170
|
it('displays grain columns', () => {
|
|
@@ -161,7 +175,8 @@ describe('QueryOverviewPanel', () => {
|
|
|
161
175
|
|
|
162
176
|
it('shows materialization status', () => {
|
|
163
177
|
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
|
|
164
|
-
|
|
178
|
+
// Status shows "Not Set" when no materialization is configured
|
|
179
|
+
expect(screen.getAllByText('○ Not Set').length).toBe(2);
|
|
165
180
|
});
|
|
166
181
|
});
|
|
167
182
|
|
|
@@ -223,9 +238,10 @@ describe('QueryOverviewPanel', () => {
|
|
|
223
238
|
expect(screen.getByText('Generated SQL')).toBeInTheDocument();
|
|
224
239
|
});
|
|
225
240
|
|
|
226
|
-
it('shows
|
|
241
|
+
it('shows SQL view toggle with Optimized and Raw options', () => {
|
|
227
242
|
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
|
|
228
|
-
expect(screen.getByText('
|
|
243
|
+
expect(screen.getByText('Optimized')).toBeInTheDocument();
|
|
244
|
+
expect(screen.getByText('Raw')).toBeInTheDocument();
|
|
229
245
|
});
|
|
230
246
|
|
|
231
247
|
it('renders SQL in syntax highlighter', () => {
|
|
@@ -234,405 +250,2747 @@ describe('QueryOverviewPanel', () => {
|
|
|
234
250
|
expect(screen.getByText(mockMetricsResult.sql)).toBeInTheDocument();
|
|
235
251
|
});
|
|
236
252
|
|
|
237
|
-
it('
|
|
238
|
-
const mockClipboard = { writeText: jest.fn() };
|
|
239
|
-
Object.assign(navigator, { clipboard: mockClipboard });
|
|
240
|
-
|
|
253
|
+
it('defaults to Optimized view', () => {
|
|
241
254
|
renderWithRouter(<QueryOverviewPanel {...defaultProps} />);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
expect(mockClipboard.writeText).toHaveBeenCalledWith(
|
|
245
|
-
mockMetricsResult.sql,
|
|
246
|
-
);
|
|
255
|
+
const optimizedBtn = screen.getByText('Optimized');
|
|
256
|
+
expect(optimizedBtn).toHaveClass('active');
|
|
247
257
|
});
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe('PreAggDetailsPanel', () => {
|
|
252
|
-
const mockPreAgg = {
|
|
253
|
-
parent_name: 'default.repair_orders',
|
|
254
|
-
aggregability: 'FULL',
|
|
255
|
-
grain: ['date_id', 'customer_id'],
|
|
256
|
-
components: [
|
|
257
|
-
{
|
|
258
|
-
name: 'sum_revenue',
|
|
259
|
-
expression: 'SUM(revenue)',
|
|
260
|
-
aggregation: 'SUM',
|
|
261
|
-
merge: 'SUM',
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: 'count_orders',
|
|
265
|
-
expression: 'COUNT(*)',
|
|
266
|
-
aggregation: 'COUNT',
|
|
267
|
-
merge: 'SUM',
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const mockMetricFormulas = [
|
|
274
|
-
{
|
|
275
|
-
name: 'default.total_revenue',
|
|
276
|
-
short_name: 'total_revenue',
|
|
277
|
-
combiner: 'SUM(sum_revenue)',
|
|
278
|
-
is_derived: false,
|
|
279
|
-
components: ['sum_revenue'],
|
|
280
|
-
},
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
const onClose = jest.fn();
|
|
284
|
-
|
|
285
|
-
beforeEach(() => {
|
|
286
|
-
jest.clearAllMocks();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('returns null when no preAgg provided', () => {
|
|
290
|
-
const { container } = render(
|
|
291
|
-
<PreAggDetailsPanel preAgg={null} onClose={onClose} />,
|
|
292
|
-
);
|
|
293
|
-
expect(container.firstChild).toBeNull();
|
|
294
|
-
});
|
|
295
258
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
metricFormulas={mockMetricFormulas}
|
|
301
|
-
onClose={onClose}
|
|
302
|
-
/>,
|
|
303
|
-
);
|
|
304
|
-
expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
|
|
305
|
-
});
|
|
259
|
+
it('fetches and displays raw SQL when Raw tab is clicked', async () => {
|
|
260
|
+
const mockRawSql =
|
|
261
|
+
'SELECT * FROM raw_table WHERE date_id = 123 GROUP BY 1';
|
|
262
|
+
const onFetchRawSql = jest.fn().mockResolvedValue(mockRawSql);
|
|
306
263
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
preAgg={mockPreAgg}
|
|
311
|
-
metricFormulas={mockMetricFormulas}
|
|
312
|
-
onClose={onClose}
|
|
313
|
-
/>,
|
|
314
|
-
);
|
|
315
|
-
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
316
|
-
expect(screen.getByText('default.repair_orders')).toBeInTheDocument();
|
|
317
|
-
});
|
|
264
|
+
renderWithRouter(
|
|
265
|
+
<QueryOverviewPanel {...defaultProps} onFetchRawSql={onFetchRawSql} />,
|
|
266
|
+
);
|
|
318
267
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
onClose={onClose}
|
|
325
|
-
/>,
|
|
326
|
-
);
|
|
327
|
-
expect(screen.getByTitle('Close panel')).toBeInTheDocument();
|
|
328
|
-
});
|
|
268
|
+
// Click Raw tab
|
|
269
|
+
const rawBtn = screen.getByText('Raw');
|
|
270
|
+
await act(async () => {
|
|
271
|
+
fireEvent.click(rawBtn);
|
|
272
|
+
});
|
|
329
273
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
metricFormulas={mockMetricFormulas}
|
|
335
|
-
onClose={onClose}
|
|
336
|
-
/>,
|
|
337
|
-
);
|
|
338
|
-
fireEvent.click(screen.getByTitle('Close panel'));
|
|
339
|
-
expect(onClose).toHaveBeenCalled();
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(onFetchRawSql).toHaveBeenCalled();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
340
278
|
});
|
|
341
279
|
|
|
342
|
-
describe('
|
|
343
|
-
it('
|
|
344
|
-
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
onClose={onClose}
|
|
280
|
+
describe('Materialization CTA', () => {
|
|
281
|
+
it('shows Configure button when not materialized', () => {
|
|
282
|
+
renderWithRouter(
|
|
283
|
+
<QueryOverviewPanel
|
|
284
|
+
{...defaultProps}
|
|
285
|
+
onPlanMaterialization={jest.fn()}
|
|
349
286
|
/>,
|
|
350
287
|
);
|
|
351
|
-
expect(screen.getByText('
|
|
288
|
+
expect(screen.getByText('Configure')).toBeInTheDocument();
|
|
352
289
|
});
|
|
353
290
|
|
|
354
|
-
it('shows
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
onClose={onClose}
|
|
291
|
+
it('shows CTA content with Ready to materialize text', () => {
|
|
292
|
+
renderWithRouter(
|
|
293
|
+
<QueryOverviewPanel
|
|
294
|
+
{...defaultProps}
|
|
295
|
+
onPlanMaterialization={jest.fn()}
|
|
360
296
|
/>,
|
|
361
297
|
);
|
|
362
|
-
expect(screen.getByText('
|
|
363
|
-
expect(screen.getByText('customer_id')).toBeInTheDocument();
|
|
298
|
+
expect(screen.getByText('Ready to materialize?')).toBeInTheDocument();
|
|
364
299
|
});
|
|
365
300
|
|
|
366
|
-
it('
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
301
|
+
it('opens configuration form when Configure button is clicked', async () => {
|
|
302
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
303
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
304
|
+
temporalPartitions: [],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
renderWithRouter(
|
|
308
|
+
<QueryOverviewPanel
|
|
309
|
+
{...defaultProps}
|
|
310
|
+
onPlanMaterialization={jest.fn()}
|
|
311
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
373
312
|
/>,
|
|
374
313
|
);
|
|
375
|
-
|
|
314
|
+
|
|
315
|
+
const configureBtn = screen.getByText('Configure');
|
|
316
|
+
await act(async () => {
|
|
317
|
+
fireEvent.click(configureBtn);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Should show configuration form
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(
|
|
323
|
+
screen.getByText('Configure Materialization'),
|
|
324
|
+
).toBeInTheDocument();
|
|
325
|
+
});
|
|
376
326
|
});
|
|
377
327
|
});
|
|
378
328
|
|
|
379
|
-
describe('
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
329
|
+
describe('Materialization Configuration Form', () => {
|
|
330
|
+
const setupConfigForm = async () => {
|
|
331
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
332
|
+
columns: [
|
|
333
|
+
{ name: 'date_id', type: 'int' },
|
|
334
|
+
{ name: 'customer_id', type: 'int' },
|
|
335
|
+
],
|
|
336
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const onPlanMaterialization = jest.fn().mockResolvedValue({});
|
|
340
|
+
|
|
341
|
+
renderWithRouter(
|
|
342
|
+
<QueryOverviewPanel
|
|
343
|
+
{...defaultProps}
|
|
344
|
+
onPlanMaterialization={onPlanMaterialization}
|
|
345
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
386
346
|
/>,
|
|
387
347
|
);
|
|
388
|
-
|
|
348
|
+
|
|
349
|
+
const configureBtn = screen.getByText('Configure');
|
|
350
|
+
await act(async () => {
|
|
351
|
+
fireEvent.click(configureBtn);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await waitFor(() => {
|
|
355
|
+
expect(
|
|
356
|
+
screen.getByText('Configure Materialization'),
|
|
357
|
+
).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return { onPlanMaterialization, onFetchNodePartitions };
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
it('shows strategy options (Full and Incremental)', async () => {
|
|
364
|
+
await setupConfigForm();
|
|
365
|
+
|
|
366
|
+
expect(screen.getByText('Strategy')).toBeInTheDocument();
|
|
367
|
+
expect(screen.getByText('Full')).toBeInTheDocument();
|
|
368
|
+
expect(screen.getByText('Incremental')).toBeInTheDocument();
|
|
389
369
|
});
|
|
390
370
|
|
|
391
|
-
it('
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
371
|
+
it('allows switching strategy to Full', async () => {
|
|
372
|
+
await setupConfigForm();
|
|
373
|
+
|
|
374
|
+
const fullRadio = screen.getByLabelText('Full');
|
|
375
|
+
fireEvent.click(fullRadio);
|
|
376
|
+
|
|
377
|
+
expect(fullRadio).toBeChecked();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('shows close button on configuration form', async () => {
|
|
381
|
+
await setupConfigForm();
|
|
382
|
+
|
|
383
|
+
// Close button is the × in the header
|
|
384
|
+
const closeButtons = screen.getAllByText('×');
|
|
385
|
+
expect(closeButtons.length).toBeGreaterThan(0);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('closes configuration form when close button is clicked', async () => {
|
|
389
|
+
await setupConfigForm();
|
|
390
|
+
|
|
391
|
+
// Find and click the close button
|
|
392
|
+
const closeButton = screen.getByRole('button', {
|
|
393
|
+
name: /×/,
|
|
394
|
+
});
|
|
395
|
+
fireEvent.click(closeButton);
|
|
396
|
+
|
|
397
|
+
// Configuration form should be closed
|
|
398
|
+
await waitFor(() => {
|
|
399
|
+
expect(
|
|
400
|
+
screen.queryByText('Configure Materialization'),
|
|
401
|
+
).not.toBeInTheDocument();
|
|
402
|
+
});
|
|
400
403
|
});
|
|
401
404
|
});
|
|
402
405
|
|
|
403
|
-
describe('
|
|
404
|
-
it('
|
|
405
|
-
|
|
406
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
describe('Error Display', () => {
|
|
407
|
+
it('shows materialization error when present', () => {
|
|
408
|
+
renderWithRouter(
|
|
409
|
+
<QueryOverviewPanel
|
|
410
|
+
{...defaultProps}
|
|
411
|
+
onPlanMaterialization={jest.fn()}
|
|
412
|
+
materializationError="Failed to plan materialization"
|
|
413
|
+
onClearError={jest.fn()}
|
|
410
414
|
/>,
|
|
411
415
|
);
|
|
412
|
-
|
|
416
|
+
|
|
417
|
+
expect(
|
|
418
|
+
screen.getByText('Failed to plan materialization'),
|
|
419
|
+
).toBeInTheDocument();
|
|
413
420
|
});
|
|
414
421
|
|
|
415
|
-
it('
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
422
|
+
it('calls onClearError when dismiss button is clicked', () => {
|
|
423
|
+
const onClearError = jest.fn();
|
|
424
|
+
|
|
425
|
+
renderWithRouter(
|
|
426
|
+
<QueryOverviewPanel
|
|
427
|
+
{...defaultProps}
|
|
428
|
+
onPlanMaterialization={jest.fn()}
|
|
429
|
+
materializationError="Failed to plan materialization"
|
|
430
|
+
onClearError={onClearError}
|
|
421
431
|
/>,
|
|
422
432
|
);
|
|
423
|
-
|
|
424
|
-
|
|
433
|
+
|
|
434
|
+
// Find dismiss button (aria-label="Dismiss error")
|
|
435
|
+
const dismissBtn = screen.getByLabelText('Dismiss error');
|
|
436
|
+
fireEvent.click(dismissBtn);
|
|
437
|
+
|
|
438
|
+
expect(onClearError).toHaveBeenCalled();
|
|
425
439
|
});
|
|
440
|
+
});
|
|
426
441
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
442
|
+
describe('Workflow URLs Display', () => {
|
|
443
|
+
it('displays workflow URLs when provided', () => {
|
|
444
|
+
renderWithRouter(
|
|
445
|
+
<QueryOverviewPanel
|
|
446
|
+
{...defaultProps}
|
|
447
|
+
onPlanMaterialization={jest.fn()}
|
|
448
|
+
workflowUrls={['http://workflow.example.com/job/123']}
|
|
449
|
+
onClearWorkflowUrls={jest.fn()}
|
|
433
450
|
/>,
|
|
434
451
|
);
|
|
435
|
-
|
|
436
|
-
|
|
452
|
+
|
|
453
|
+
// The workflow URL or a related indicator should be shown
|
|
454
|
+
expect(screen.getByText(/workflow/i)).toBeInTheDocument();
|
|
437
455
|
});
|
|
456
|
+
});
|
|
438
457
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
458
|
+
describe('Planned Pre-aggregations', () => {
|
|
459
|
+
it('shows status badges for planned pre-aggs', () => {
|
|
460
|
+
const plannedPreaggs = {
|
|
461
|
+
'default.repair_orders|customer_id,date_id': {
|
|
462
|
+
id: 1,
|
|
463
|
+
node_name: 'default.repair_orders',
|
|
464
|
+
grain_columns: ['date_id', 'customer_id'],
|
|
465
|
+
workflow_status: 'active',
|
|
466
|
+
workflow_urls: ['http://example.com'],
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
renderWithRouter(
|
|
471
|
+
<QueryOverviewPanel
|
|
472
|
+
{...defaultProps}
|
|
473
|
+
plannedPreaggs={plannedPreaggs}
|
|
474
|
+
onPlanMaterialization={jest.fn()}
|
|
445
475
|
/>,
|
|
446
476
|
);
|
|
447
|
-
|
|
448
|
-
|
|
477
|
+
|
|
478
|
+
// Should show some indicator of planned state
|
|
479
|
+
// The exact text depends on the status logic
|
|
480
|
+
expect(
|
|
481
|
+
screen.queryByText('Not Set') || screen.queryByText('Workflow Active'),
|
|
482
|
+
).toBeTruthy();
|
|
449
483
|
});
|
|
450
484
|
});
|
|
451
485
|
|
|
452
|
-
describe('
|
|
453
|
-
it('
|
|
454
|
-
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
486
|
+
describe('Loaded Cube Display', () => {
|
|
487
|
+
it('shows cube name banner when loadedCubeName is provided', () => {
|
|
488
|
+
renderWithRouter(
|
|
489
|
+
<QueryOverviewPanel
|
|
490
|
+
{...defaultProps}
|
|
491
|
+
loadedCubeName="default.test_cube"
|
|
492
|
+
onPlanMaterialization={jest.fn()}
|
|
459
493
|
/>,
|
|
460
494
|
);
|
|
461
|
-
|
|
495
|
+
|
|
496
|
+
// Should show some indication of loaded cube
|
|
497
|
+
// The exact text depends on the implementation
|
|
498
|
+
expect(screen.getByText(/Query Plan/)).toBeInTheDocument();
|
|
462
499
|
});
|
|
500
|
+
});
|
|
463
501
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
502
|
+
describe('Partition Setup Form', () => {
|
|
503
|
+
const setupPartitionTest = async (partitionResults = {}) => {
|
|
504
|
+
const mockNodePartitions = {
|
|
505
|
+
'default.repair_orders': {
|
|
506
|
+
columns: [
|
|
507
|
+
{ name: 'date_id', type: 'int' },
|
|
508
|
+
{ name: 'customer_id', type: 'int' },
|
|
509
|
+
{ name: 'dateint', type: 'int' },
|
|
510
|
+
],
|
|
511
|
+
temporalPartitions: [],
|
|
512
|
+
...partitionResults,
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const onFetchNodePartitions = jest.fn().mockImplementation(nodeName =>
|
|
517
|
+
Promise.resolve(
|
|
518
|
+
mockNodePartitions[nodeName] || {
|
|
519
|
+
columns: [],
|
|
520
|
+
temporalPartitions: [],
|
|
521
|
+
},
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const onSetPartition = jest.fn().mockResolvedValue({ status: 200 });
|
|
526
|
+
const onPlanMaterialization = jest.fn().mockResolvedValue({});
|
|
527
|
+
|
|
528
|
+
renderWithRouter(
|
|
529
|
+
<QueryOverviewPanel
|
|
530
|
+
{...defaultProps}
|
|
531
|
+
onPlanMaterialization={onPlanMaterialization}
|
|
532
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
533
|
+
onSetPartition={onSetPartition}
|
|
470
534
|
/>,
|
|
471
535
|
);
|
|
472
|
-
|
|
536
|
+
|
|
537
|
+
// Open config form
|
|
538
|
+
const configureBtn = screen.getByText('Configure');
|
|
539
|
+
await act(async () => {
|
|
540
|
+
fireEvent.click(configureBtn);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(
|
|
545
|
+
screen.getByText('Configure Materialization'),
|
|
546
|
+
).toBeInTheDocument();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return { onFetchNodePartitions, onSetPartition, onPlanMaterialization };
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
it('shows partition setup form when incremental is selected and no temporal partitions', async () => {
|
|
553
|
+
await setupPartitionTest();
|
|
554
|
+
|
|
555
|
+
// Select incremental strategy
|
|
556
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
557
|
+
await act(async () => {
|
|
558
|
+
fireEvent.click(incrementalRadio);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Should show partition setup header
|
|
562
|
+
await waitFor(() => {
|
|
563
|
+
expect(
|
|
564
|
+
screen.getByText('Set up temporal partitions for incremental builds'),
|
|
565
|
+
).toBeInTheDocument();
|
|
566
|
+
});
|
|
473
567
|
});
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
568
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
name: 'default.avg_repair_price',
|
|
480
|
-
short_name: 'avg_repair_price',
|
|
481
|
-
combiner: 'SUM(sum_revenue) / SUM(count_orders)',
|
|
482
|
-
is_derived: true,
|
|
483
|
-
components: ['sum_revenue', 'count_orders'],
|
|
484
|
-
};
|
|
569
|
+
it('shows column dropdown with date-like columns prioritized', async () => {
|
|
570
|
+
await setupPartitionTest();
|
|
485
571
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
];
|
|
572
|
+
// Select incremental strategy
|
|
573
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
574
|
+
await act(async () => {
|
|
575
|
+
fireEvent.click(incrementalRadio);
|
|
576
|
+
});
|
|
492
577
|
|
|
493
|
-
|
|
578
|
+
await waitFor(() => {
|
|
579
|
+
// Multiple Column labels may exist (one per node)
|
|
580
|
+
const columnLabels = screen.getAllByText('Column');
|
|
581
|
+
expect(columnLabels.length).toBeGreaterThan(0);
|
|
582
|
+
});
|
|
494
583
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
584
|
+
// Should have date-like columns with star markers
|
|
585
|
+
expect(screen.getByText(/dateint.*★/)).toBeInTheDocument();
|
|
586
|
+
expect(screen.getByText(/date_id.*★/)).toBeInTheDocument();
|
|
587
|
+
});
|
|
498
588
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
<MetricDetailsPanel metric={null} onClose={onClose} />,
|
|
502
|
-
);
|
|
503
|
-
expect(container.firstChild).toBeNull();
|
|
504
|
-
});
|
|
589
|
+
it('shows granularity dropdown with Day, Hour, Month options', async () => {
|
|
590
|
+
await setupPartitionTest();
|
|
505
591
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
grainGroups={mockGrainGroups}
|
|
511
|
-
onClose={onClose}
|
|
512
|
-
/>,
|
|
513
|
-
);
|
|
514
|
-
expect(screen.getByText('Derived Metric')).toBeInTheDocument();
|
|
515
|
-
});
|
|
592
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
593
|
+
await act(async () => {
|
|
594
|
+
fireEvent.click(incrementalRadio);
|
|
595
|
+
});
|
|
516
596
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
597
|
+
await waitFor(() => {
|
|
598
|
+
const granularityLabels = screen.getAllByText('Granularity');
|
|
599
|
+
expect(granularityLabels.length).toBeGreaterThan(0);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Check granularity options exist - use getAllByRole since there may be multiple
|
|
603
|
+
const granularitySelects = screen.getAllByRole('combobox');
|
|
604
|
+
// Find the one with Day selected
|
|
605
|
+
const daySelect = granularitySelects.find(
|
|
606
|
+
select => select.value === 'day',
|
|
607
|
+
);
|
|
608
|
+
expect(daySelect).toBeInTheDocument();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('shows format input field with placeholder', async () => {
|
|
612
|
+
await setupPartitionTest();
|
|
613
|
+
|
|
614
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
615
|
+
await act(async () => {
|
|
616
|
+
fireEvent.click(incrementalRadio);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await waitFor(() => {
|
|
620
|
+
const formatLabels = screen.getAllByText('Format');
|
|
621
|
+
expect(formatLabels.length).toBeGreaterThan(0);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Format input should have placeholder - there may be multiple
|
|
625
|
+
const formatInputs = screen.getAllByPlaceholderText('yyyyMMdd');
|
|
626
|
+
expect(formatInputs.length).toBeGreaterThan(0);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('disables Set button when no column is selected', async () => {
|
|
630
|
+
await setupPartitionTest();
|
|
631
|
+
|
|
632
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
633
|
+
await act(async () => {
|
|
634
|
+
fireEvent.click(incrementalRadio);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
await waitFor(() => {
|
|
638
|
+
// Wait for partition setup form to show
|
|
639
|
+
expect(
|
|
640
|
+
screen.getByText('Set up temporal partitions for incremental builds'),
|
|
641
|
+
).toBeInTheDocument();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Set button should be disabled when no column selected (initially empty)
|
|
645
|
+
const setBtns = screen.getAllByText('Set');
|
|
646
|
+
// At least one Set button should be disabled
|
|
647
|
+
const hasDisabledBtn = setBtns.some(btn => btn.disabled);
|
|
648
|
+
expect(hasDisabledBtn).toBe(true);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('enables Set button when column is selected', async () => {
|
|
652
|
+
await setupPartitionTest();
|
|
653
|
+
|
|
654
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
655
|
+
await act(async () => {
|
|
656
|
+
fireEvent.click(incrementalRadio);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
await waitFor(() => {
|
|
660
|
+
const columnLabels = screen.getAllByText('Column');
|
|
661
|
+
expect(columnLabels.length).toBeGreaterThan(0);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Select a column in the first dropdown
|
|
665
|
+
const columnSelects = screen.getAllByRole('combobox');
|
|
666
|
+
await act(async () => {
|
|
667
|
+
fireEvent.change(columnSelects[0], { target: { value: 'date_id' } });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Set button should now be enabled
|
|
671
|
+
const setBtns = screen.getAllByText('Set');
|
|
672
|
+
expect(setBtns[0]).not.toBeDisabled();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('calls onSetPartition when Set button is clicked', async () => {
|
|
676
|
+
const { onSetPartition } = await setupPartitionTest();
|
|
677
|
+
|
|
678
|
+
const incrementalRadio = screen.getByLabelText('Incremental');
|
|
679
|
+
await act(async () => {
|
|
680
|
+
fireEvent.click(incrementalRadio);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await waitFor(() => {
|
|
684
|
+
const columnLabels = screen.getAllByText('Column');
|
|
685
|
+
expect(columnLabels.length).toBeGreaterThan(0);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Select a column
|
|
689
|
+
const columnSelects = screen.getAllByRole('combobox');
|
|
690
|
+
await act(async () => {
|
|
691
|
+
fireEvent.change(columnSelects[0], { target: { value: 'date_id' } });
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Click Set
|
|
695
|
+
const setBtns = screen.getAllByText('Set');
|
|
696
|
+
await act(async () => {
|
|
697
|
+
fireEvent.click(setBtns[0]);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
await waitFor(() => {
|
|
701
|
+
expect(onSetPartition).toHaveBeenCalledWith(
|
|
702
|
+
'default.repair_orders',
|
|
703
|
+
'date_id',
|
|
704
|
+
'temporal',
|
|
705
|
+
'yyyyMMdd',
|
|
706
|
+
'day',
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('shows success state when partition is already configured', async () => {
|
|
712
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
713
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
714
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
renderWithRouter(
|
|
718
|
+
<QueryOverviewPanel
|
|
719
|
+
{...defaultProps}
|
|
720
|
+
onPlanMaterialization={jest.fn()}
|
|
721
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
722
|
+
/>,
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// Open config form
|
|
726
|
+
const configureBtn = screen.getByText('Configure');
|
|
727
|
+
await act(async () => {
|
|
728
|
+
fireEvent.click(configureBtn);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
await waitFor(() => {
|
|
732
|
+
expect(
|
|
733
|
+
screen.getByText('Configure Materialization'),
|
|
734
|
+
).toBeInTheDocument();
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// When partition already exists, the incremental strategy should default to enabled
|
|
738
|
+
// and partition setup form should NOT be shown
|
|
739
|
+
await waitFor(() => {
|
|
740
|
+
// The incremental option should show the partition name badge
|
|
741
|
+
// If partition setup prompt is NOT shown, it means partition is configured
|
|
742
|
+
expect(
|
|
743
|
+
screen.queryByText(
|
|
744
|
+
'Set up temporal partitions for incremental builds',
|
|
745
|
+
),
|
|
746
|
+
).not.toBeInTheDocument();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
527
749
|
});
|
|
528
750
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
751
|
+
describe('Backfill Date Range in Config Form', () => {
|
|
752
|
+
const setupBackfillTest = async () => {
|
|
753
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
754
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
755
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const onPlanMaterialization = jest.fn().mockResolvedValue({});
|
|
759
|
+
|
|
760
|
+
renderWithRouter(
|
|
761
|
+
<QueryOverviewPanel
|
|
762
|
+
{...defaultProps}
|
|
763
|
+
onPlanMaterialization={onPlanMaterialization}
|
|
764
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
765
|
+
/>,
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Open config form
|
|
769
|
+
const configureBtn = screen.getByText('Configure');
|
|
770
|
+
await act(async () => {
|
|
771
|
+
fireEvent.click(configureBtn);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await waitFor(() => {
|
|
775
|
+
expect(
|
|
776
|
+
screen.getByText('Configure Materialization'),
|
|
777
|
+
).toBeInTheDocument();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
return { onPlanMaterialization };
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
it('shows "Run initial backfill" checkbox for incremental strategy', async () => {
|
|
784
|
+
await setupBackfillTest();
|
|
785
|
+
|
|
786
|
+
// Incremental should be the default when partition exists
|
|
787
|
+
await waitFor(() => {
|
|
788
|
+
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('shows backfill date range when checkbox is checked', async () => {
|
|
793
|
+
await setupBackfillTest();
|
|
794
|
+
|
|
795
|
+
await waitFor(() => {
|
|
796
|
+
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Checkbox should be checked by default
|
|
800
|
+
const checkbox = screen.getByRole('checkbox', {
|
|
801
|
+
name: /Run initial backfill/i,
|
|
802
|
+
});
|
|
803
|
+
expect(checkbox).toBeChecked();
|
|
804
|
+
|
|
805
|
+
// Date range should be visible
|
|
806
|
+
expect(screen.getByText('Backfill Date Range')).toBeInTheDocument();
|
|
807
|
+
expect(screen.getByText('From')).toBeInTheDocument();
|
|
808
|
+
expect(screen.getByText('To')).toBeInTheDocument();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('hides backfill date range when checkbox is unchecked', async () => {
|
|
812
|
+
await setupBackfillTest();
|
|
813
|
+
|
|
814
|
+
await waitFor(() => {
|
|
815
|
+
expect(screen.getByText('Run initial backfill')).toBeInTheDocument();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Uncheck the checkbox
|
|
819
|
+
const checkbox = screen.getByRole('checkbox', {
|
|
820
|
+
name: /Run initial backfill/i,
|
|
821
|
+
});
|
|
822
|
+
await act(async () => {
|
|
823
|
+
fireEvent.click(checkbox);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Date range should be hidden
|
|
827
|
+
expect(screen.queryByText('Backfill Date Range')).not.toBeInTheDocument();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('shows "Today" and "Specific date" options for end date', async () => {
|
|
831
|
+
await setupBackfillTest();
|
|
832
|
+
|
|
833
|
+
await waitFor(() => {
|
|
834
|
+
expect(screen.getByText('Backfill Date Range')).toBeInTheDocument();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Find the select for backfill "To" field
|
|
838
|
+
// There are multiple selects, we need the one with 'today' value
|
|
839
|
+
const selects = screen.getAllByRole('combobox');
|
|
840
|
+
const toSelect = selects.find(s => s.value === 'today');
|
|
841
|
+
expect(toSelect).toBeInTheDocument();
|
|
842
|
+
|
|
843
|
+
// Change to specific date
|
|
844
|
+
await act(async () => {
|
|
845
|
+
fireEvent.change(toSelect, { target: { value: 'specific' } });
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// Should show an additional date input
|
|
849
|
+
await waitFor(() => {
|
|
850
|
+
// There should be date inputs visible
|
|
851
|
+
const dateInputs = document.querySelectorAll('input[type="date"]');
|
|
852
|
+
expect(dateInputs.length).toBeGreaterThanOrEqual(2);
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('hides backfill options for full strategy', async () => {
|
|
857
|
+
await setupBackfillTest();
|
|
858
|
+
|
|
859
|
+
// Switch to full strategy
|
|
860
|
+
const fullRadio = screen.getByLabelText('Full');
|
|
861
|
+
await act(async () => {
|
|
862
|
+
fireEvent.click(fullRadio);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Backfill checkbox should not be visible
|
|
866
|
+
expect(
|
|
867
|
+
screen.queryByText('Run initial backfill'),
|
|
868
|
+
).not.toBeInTheDocument();
|
|
869
|
+
});
|
|
539
870
|
});
|
|
540
871
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
872
|
+
describe('Schedule Configuration', () => {
|
|
873
|
+
const setupScheduleTest = async () => {
|
|
874
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
875
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
876
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
renderWithRouter(
|
|
880
|
+
<QueryOverviewPanel
|
|
881
|
+
{...defaultProps}
|
|
882
|
+
onPlanMaterialization={jest.fn()}
|
|
883
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
884
|
+
/>,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
// Open config form
|
|
888
|
+
const configureBtn = screen.getByText('Configure');
|
|
889
|
+
await act(async () => {
|
|
890
|
+
fireEvent.click(configureBtn);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
await waitFor(() => {
|
|
894
|
+
expect(
|
|
895
|
+
screen.getByText('Configure Materialization'),
|
|
896
|
+
).toBeInTheDocument();
|
|
897
|
+
});
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
it('shows schedule dropdown with recommended, hourly, and custom options', async () => {
|
|
901
|
+
await setupScheduleTest();
|
|
902
|
+
|
|
903
|
+
expect(screen.getByText('Schedule')).toBeInTheDocument();
|
|
904
|
+
|
|
905
|
+
// The schedule select should have options - find one with 'auto' value
|
|
906
|
+
const selects = screen.getAllByRole('combobox');
|
|
907
|
+
const scheduleSelect = selects.find(s => s.value === 'auto');
|
|
908
|
+
expect(scheduleSelect).toBeInTheDocument();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('shows custom cron input when custom is selected', async () => {
|
|
912
|
+
await setupScheduleTest();
|
|
913
|
+
|
|
914
|
+
// Find the schedule select (has 'auto' value initially)
|
|
915
|
+
const selects = screen.getAllByRole('combobox');
|
|
916
|
+
const scheduleSelect = selects.find(s => s.value === 'auto');
|
|
917
|
+
|
|
918
|
+
await act(async () => {
|
|
919
|
+
fireEvent.change(scheduleSelect, { target: { value: 'custom' } });
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Custom input should appear
|
|
923
|
+
await waitFor(() => {
|
|
924
|
+
expect(screen.getByPlaceholderText('0 6 * * *')).toBeInTheDocument();
|
|
925
|
+
});
|
|
926
|
+
});
|
|
551
927
|
});
|
|
552
928
|
|
|
553
|
-
describe('
|
|
554
|
-
it('
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
929
|
+
describe('Lookback Window', () => {
|
|
930
|
+
it('shows lookback window input for incremental strategy', async () => {
|
|
931
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
932
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
933
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
renderWithRouter(
|
|
937
|
+
<QueryOverviewPanel
|
|
938
|
+
{...defaultProps}
|
|
939
|
+
onPlanMaterialization={jest.fn()}
|
|
940
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
560
941
|
/>,
|
|
561
942
|
);
|
|
562
|
-
|
|
943
|
+
|
|
944
|
+
// Open config form
|
|
945
|
+
const configureBtn = screen.getByText('Configure');
|
|
946
|
+
await act(async () => {
|
|
947
|
+
fireEvent.click(configureBtn);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
await waitFor(() => {
|
|
951
|
+
expect(screen.getByText('Lookback Window')).toBeInTheDocument();
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// Should have placeholder
|
|
955
|
+
expect(screen.getByPlaceholderText('1 day')).toBeInTheDocument();
|
|
956
|
+
|
|
957
|
+
// Should have hint text
|
|
958
|
+
expect(screen.getByText(/For late-arriving data/)).toBeInTheDocument();
|
|
563
959
|
});
|
|
564
960
|
|
|
565
|
-
it('
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
961
|
+
it('hides lookback window for full strategy', async () => {
|
|
962
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
963
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
964
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
renderWithRouter(
|
|
968
|
+
<QueryOverviewPanel
|
|
969
|
+
{...defaultProps}
|
|
970
|
+
onPlanMaterialization={jest.fn()}
|
|
971
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
571
972
|
/>,
|
|
572
973
|
);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
974
|
+
|
|
975
|
+
// Open config form
|
|
976
|
+
const configureBtn = screen.getByText('Configure');
|
|
977
|
+
await act(async () => {
|
|
978
|
+
fireEvent.click(configureBtn);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
await waitFor(() => {
|
|
982
|
+
expect(
|
|
983
|
+
screen.getByText('Configure Materialization'),
|
|
984
|
+
).toBeInTheDocument();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Switch to full strategy
|
|
988
|
+
const fullRadio = screen.getByLabelText('Full');
|
|
989
|
+
await act(async () => {
|
|
990
|
+
fireEvent.click(fullRadio);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Lookback window should be hidden
|
|
994
|
+
expect(screen.queryByText('Lookback Window')).not.toBeInTheDocument();
|
|
576
995
|
});
|
|
577
996
|
});
|
|
578
997
|
|
|
579
|
-
describe('
|
|
580
|
-
it('
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
998
|
+
describe('Druid Cube Configuration', () => {
|
|
999
|
+
it('shows Druid cube checkbox', async () => {
|
|
1000
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1001
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1002
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
renderWithRouter(
|
|
1006
|
+
<QueryOverviewPanel
|
|
1007
|
+
{...defaultProps}
|
|
1008
|
+
onPlanMaterialization={jest.fn()}
|
|
1009
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
586
1010
|
/>,
|
|
587
1011
|
);
|
|
588
|
-
|
|
1012
|
+
|
|
1013
|
+
// Open config form
|
|
1014
|
+
const configureBtn = screen.getByText('Configure');
|
|
1015
|
+
await act(async () => {
|
|
1016
|
+
fireEvent.click(configureBtn);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
await waitFor(() => {
|
|
1020
|
+
expect(
|
|
1021
|
+
screen.getByText('Enable Druid cube materialization'),
|
|
1022
|
+
).toBeInTheDocument();
|
|
1023
|
+
});
|
|
589
1024
|
});
|
|
590
1025
|
|
|
591
|
-
it('shows
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1026
|
+
it('shows cube name inputs when Druid is enabled and no cube loaded', async () => {
|
|
1027
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1028
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1029
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
renderWithRouter(
|
|
1033
|
+
<QueryOverviewPanel
|
|
1034
|
+
{...defaultProps}
|
|
1035
|
+
onPlanMaterialization={jest.fn()}
|
|
1036
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
597
1037
|
/>,
|
|
598
1038
|
);
|
|
599
|
-
|
|
600
|
-
|
|
1039
|
+
|
|
1040
|
+
// Open config form
|
|
1041
|
+
const configureBtn = screen.getByText('Configure');
|
|
1042
|
+
await act(async () => {
|
|
1043
|
+
fireEvent.click(configureBtn);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await waitFor(() => {
|
|
1047
|
+
expect(screen.getByText('Cube Name')).toBeInTheDocument();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Should show namespace and name inputs
|
|
1051
|
+
expect(screen.getByPlaceholderText('users.myname')).toBeInTheDocument();
|
|
1052
|
+
expect(screen.getByPlaceholderText('my_cube')).toBeInTheDocument();
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('shows preview of pre-aggregations to combine', async () => {
|
|
1056
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1057
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1058
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
renderWithRouter(
|
|
1062
|
+
<QueryOverviewPanel
|
|
1063
|
+
{...defaultProps}
|
|
1064
|
+
onPlanMaterialization={jest.fn()}
|
|
1065
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
1066
|
+
/>,
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// Open config form
|
|
1070
|
+
const configureBtn = screen.getByText('Configure');
|
|
1071
|
+
await act(async () => {
|
|
1072
|
+
fireEvent.click(configureBtn);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await waitFor(() => {
|
|
1076
|
+
expect(
|
|
1077
|
+
screen.getByText('Pre-aggregations to combine:'),
|
|
1078
|
+
).toBeInTheDocument();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Should show the pre-agg source (may appear multiple times)
|
|
1082
|
+
const repairOrdersElements = screen.getAllByText('repair_orders');
|
|
1083
|
+
expect(repairOrdersElements.length).toBeGreaterThan(0);
|
|
601
1084
|
});
|
|
602
1085
|
});
|
|
603
1086
|
|
|
604
|
-
describe('
|
|
605
|
-
it('
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1087
|
+
describe('Form Submission', () => {
|
|
1088
|
+
it('shows correct button text for Druid cube materialization', async () => {
|
|
1089
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1090
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1091
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
renderWithRouter(
|
|
1095
|
+
<QueryOverviewPanel
|
|
1096
|
+
{...defaultProps}
|
|
1097
|
+
onPlanMaterialization={jest.fn()}
|
|
1098
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
611
1099
|
/>,
|
|
612
1100
|
);
|
|
613
|
-
|
|
1101
|
+
|
|
1102
|
+
// Open config form
|
|
1103
|
+
const configureBtn = screen.getByText('Configure');
|
|
1104
|
+
await act(async () => {
|
|
1105
|
+
fireEvent.click(configureBtn);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
await waitFor(() => {
|
|
1109
|
+
expect(
|
|
1110
|
+
screen.getByText('Create Pre-Agg Workflows & Schedule Cube'),
|
|
1111
|
+
).toBeInTheDocument();
|
|
1112
|
+
});
|
|
614
1113
|
});
|
|
615
1114
|
|
|
616
|
-
it('shows
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1115
|
+
it('shows Cancel and submit buttons', async () => {
|
|
1116
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1117
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1118
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
renderWithRouter(
|
|
1122
|
+
<QueryOverviewPanel
|
|
1123
|
+
{...defaultProps}
|
|
1124
|
+
onPlanMaterialization={jest.fn()}
|
|
1125
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
622
1126
|
/>,
|
|
623
1127
|
);
|
|
624
|
-
|
|
1128
|
+
|
|
1129
|
+
// Open config form
|
|
1130
|
+
const configureBtn = screen.getByText('Configure');
|
|
1131
|
+
await act(async () => {
|
|
1132
|
+
fireEvent.click(configureBtn);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
await waitFor(() => {
|
|
1136
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
1137
|
+
});
|
|
625
1138
|
});
|
|
626
1139
|
|
|
627
|
-
it('
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1140
|
+
it('closes form when Cancel is clicked', async () => {
|
|
1141
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1142
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1143
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
renderWithRouter(
|
|
1147
|
+
<QueryOverviewPanel
|
|
1148
|
+
{...defaultProps}
|
|
1149
|
+
onPlanMaterialization={jest.fn()}
|
|
1150
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
633
1151
|
/>,
|
|
634
1152
|
);
|
|
635
|
-
|
|
1153
|
+
|
|
1154
|
+
// Open config form
|
|
1155
|
+
const configureBtn = screen.getByText('Configure');
|
|
1156
|
+
await act(async () => {
|
|
1157
|
+
fireEvent.click(configureBtn);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
await waitFor(() => {
|
|
1161
|
+
expect(
|
|
1162
|
+
screen.getByText('Configure Materialization'),
|
|
1163
|
+
).toBeInTheDocument();
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// Click Cancel
|
|
1167
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
1168
|
+
|
|
1169
|
+
// Form should be closed
|
|
1170
|
+
await waitFor(() => {
|
|
1171
|
+
expect(
|
|
1172
|
+
screen.queryByText('Configure Materialization'),
|
|
1173
|
+
).not.toBeInTheDocument();
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('calls onPlanMaterialization with config when submitted', async () => {
|
|
1178
|
+
const onFetchNodePartitions = jest.fn().mockResolvedValue({
|
|
1179
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1180
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const onPlanMaterialization = jest.fn().mockResolvedValue({});
|
|
1184
|
+
|
|
1185
|
+
renderWithRouter(
|
|
1186
|
+
<QueryOverviewPanel
|
|
1187
|
+
{...defaultProps}
|
|
1188
|
+
onPlanMaterialization={onPlanMaterialization}
|
|
1189
|
+
onFetchNodePartitions={onFetchNodePartitions}
|
|
1190
|
+
/>,
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
// Open config form
|
|
1194
|
+
const configureBtn = screen.getByText('Configure');
|
|
1195
|
+
await act(async () => {
|
|
1196
|
+
fireEvent.click(configureBtn);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
await waitFor(() => {
|
|
1200
|
+
expect(
|
|
1201
|
+
screen.getByText('Create Pre-Agg Workflows & Schedule Cube'),
|
|
1202
|
+
).toBeInTheDocument();
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// Submit the form
|
|
1206
|
+
const submitBtn = screen.getByText(
|
|
1207
|
+
'Create Pre-Agg Workflows & Schedule Cube',
|
|
1208
|
+
);
|
|
1209
|
+
await act(async () => {
|
|
1210
|
+
fireEvent.click(submitBtn);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
await waitFor(() => {
|
|
1214
|
+
expect(onPlanMaterialization).toHaveBeenCalled();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Check the config structure
|
|
1218
|
+
const callArgs = onPlanMaterialization.mock.calls[0][1];
|
|
1219
|
+
expect(callArgs).toHaveProperty('strategy');
|
|
1220
|
+
expect(callArgs).toHaveProperty('schedule');
|
|
1221
|
+
expect(callArgs).toHaveProperty('enableDruidCube', true);
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
describe('PreAggDetailsPanel', () => {
|
|
1227
|
+
const mockPreAgg = {
|
|
1228
|
+
parent_name: 'default.repair_orders',
|
|
1229
|
+
aggregability: 'FULL',
|
|
1230
|
+
grain: ['date_id', 'customer_id'],
|
|
1231
|
+
components: [
|
|
1232
|
+
{
|
|
1233
|
+
name: 'sum_revenue',
|
|
1234
|
+
expression: 'SUM(revenue)',
|
|
1235
|
+
aggregation: 'SUM',
|
|
1236
|
+
merge: 'SUM',
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: 'count_orders',
|
|
1240
|
+
expression: 'COUNT(*)',
|
|
1241
|
+
aggregation: 'COUNT',
|
|
1242
|
+
merge: 'SUM',
|
|
1243
|
+
},
|
|
1244
|
+
],
|
|
1245
|
+
sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
const mockMetricFormulas = [
|
|
1249
|
+
{
|
|
1250
|
+
name: 'default.total_revenue',
|
|
1251
|
+
short_name: 'total_revenue',
|
|
1252
|
+
combiner: 'SUM(sum_revenue)',
|
|
1253
|
+
is_derived: false,
|
|
1254
|
+
components: ['sum_revenue'],
|
|
1255
|
+
},
|
|
1256
|
+
];
|
|
1257
|
+
|
|
1258
|
+
const onClose = jest.fn();
|
|
1259
|
+
|
|
1260
|
+
beforeEach(() => {
|
|
1261
|
+
jest.clearAllMocks();
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('returns null when no preAgg provided', () => {
|
|
1265
|
+
const { container } = render(
|
|
1266
|
+
<PreAggDetailsPanel preAgg={null} onClose={onClose} />,
|
|
1267
|
+
);
|
|
1268
|
+
expect(container.firstChild).toBeNull();
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it('renders pre-aggregation badge', () => {
|
|
1272
|
+
render(
|
|
1273
|
+
<PreAggDetailsPanel
|
|
1274
|
+
preAgg={mockPreAgg}
|
|
1275
|
+
metricFormulas={mockMetricFormulas}
|
|
1276
|
+
onClose={onClose}
|
|
1277
|
+
/>,
|
|
1278
|
+
);
|
|
1279
|
+
expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('displays source name', () => {
|
|
1283
|
+
render(
|
|
1284
|
+
<PreAggDetailsPanel
|
|
1285
|
+
preAgg={mockPreAgg}
|
|
1286
|
+
metricFormulas={mockMetricFormulas}
|
|
1287
|
+
onClose={onClose}
|
|
1288
|
+
/>,
|
|
1289
|
+
);
|
|
1290
|
+
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
1291
|
+
expect(screen.getByText('default.repair_orders')).toBeInTheDocument();
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
it('displays close button', () => {
|
|
1295
|
+
render(
|
|
1296
|
+
<PreAggDetailsPanel
|
|
1297
|
+
preAgg={mockPreAgg}
|
|
1298
|
+
metricFormulas={mockMetricFormulas}
|
|
1299
|
+
onClose={onClose}
|
|
1300
|
+
/>,
|
|
1301
|
+
);
|
|
1302
|
+
expect(screen.getByTitle('Close panel')).toBeInTheDocument();
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
it('calls onClose when close button clicked', () => {
|
|
1306
|
+
render(
|
|
1307
|
+
<PreAggDetailsPanel
|
|
1308
|
+
preAgg={mockPreAgg}
|
|
1309
|
+
metricFormulas={mockMetricFormulas}
|
|
1310
|
+
onClose={onClose}
|
|
1311
|
+
/>,
|
|
1312
|
+
);
|
|
1313
|
+
fireEvent.click(screen.getByTitle('Close panel'));
|
|
1314
|
+
expect(onClose).toHaveBeenCalled();
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
describe('Grain Section', () => {
|
|
1318
|
+
it('displays grain section', () => {
|
|
1319
|
+
render(
|
|
1320
|
+
<PreAggDetailsPanel
|
|
1321
|
+
preAgg={mockPreAgg}
|
|
1322
|
+
metricFormulas={mockMetricFormulas}
|
|
1323
|
+
onClose={onClose}
|
|
1324
|
+
/>,
|
|
1325
|
+
);
|
|
1326
|
+
expect(screen.getByText('Grain (GROUP BY)')).toBeInTheDocument();
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
it('shows grain columns as pills', () => {
|
|
1330
|
+
render(
|
|
1331
|
+
<PreAggDetailsPanel
|
|
1332
|
+
preAgg={mockPreAgg}
|
|
1333
|
+
metricFormulas={mockMetricFormulas}
|
|
1334
|
+
onClose={onClose}
|
|
1335
|
+
/>,
|
|
1336
|
+
);
|
|
1337
|
+
expect(screen.getByText('date_id')).toBeInTheDocument();
|
|
1338
|
+
expect(screen.getByText('customer_id')).toBeInTheDocument();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('shows empty message when no grain', () => {
|
|
1342
|
+
const noGrainPreAgg = { ...mockPreAgg, grain: [] };
|
|
1343
|
+
render(
|
|
1344
|
+
<PreAggDetailsPanel
|
|
1345
|
+
preAgg={noGrainPreAgg}
|
|
1346
|
+
metricFormulas={mockMetricFormulas}
|
|
1347
|
+
onClose={onClose}
|
|
1348
|
+
/>,
|
|
1349
|
+
);
|
|
1350
|
+
expect(screen.getByText('No grain columns')).toBeInTheDocument();
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
describe('Related Metrics Section', () => {
|
|
1355
|
+
it('displays metrics using this section', () => {
|
|
1356
|
+
render(
|
|
1357
|
+
<PreAggDetailsPanel
|
|
1358
|
+
preAgg={mockPreAgg}
|
|
1359
|
+
metricFormulas={mockMetricFormulas}
|
|
1360
|
+
onClose={onClose}
|
|
1361
|
+
/>,
|
|
1362
|
+
);
|
|
1363
|
+
expect(screen.getByText('Metrics Using This')).toBeInTheDocument();
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
it('shows related metrics', () => {
|
|
1367
|
+
render(
|
|
1368
|
+
<PreAggDetailsPanel
|
|
1369
|
+
preAgg={mockPreAgg}
|
|
1370
|
+
metricFormulas={mockMetricFormulas}
|
|
1371
|
+
onClose={onClose}
|
|
1372
|
+
/>,
|
|
1373
|
+
);
|
|
1374
|
+
expect(screen.getByText('total_revenue')).toBeInTheDocument();
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
describe('Components Table', () => {
|
|
1379
|
+
it('displays components section', () => {
|
|
1380
|
+
render(
|
|
1381
|
+
<PreAggDetailsPanel
|
|
1382
|
+
preAgg={mockPreAgg}
|
|
1383
|
+
metricFormulas={mockMetricFormulas}
|
|
1384
|
+
onClose={onClose}
|
|
1385
|
+
/>,
|
|
1386
|
+
);
|
|
1387
|
+
expect(screen.getByText('Components (2)')).toBeInTheDocument();
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it('shows component names', () => {
|
|
1391
|
+
render(
|
|
1392
|
+
<PreAggDetailsPanel
|
|
1393
|
+
preAgg={mockPreAgg}
|
|
1394
|
+
metricFormulas={mockMetricFormulas}
|
|
1395
|
+
onClose={onClose}
|
|
1396
|
+
/>,
|
|
1397
|
+
);
|
|
1398
|
+
expect(screen.getByText('sum_revenue')).toBeInTheDocument();
|
|
1399
|
+
expect(screen.getByText('count_orders')).toBeInTheDocument();
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it('shows component expressions', () => {
|
|
1403
|
+
render(
|
|
1404
|
+
<PreAggDetailsPanel
|
|
1405
|
+
preAgg={mockPreAgg}
|
|
1406
|
+
metricFormulas={mockMetricFormulas}
|
|
1407
|
+
onClose={onClose}
|
|
1408
|
+
/>,
|
|
1409
|
+
);
|
|
1410
|
+
expect(screen.getByText('SUM(revenue)')).toBeInTheDocument();
|
|
1411
|
+
expect(screen.getByText('COUNT(*)')).toBeInTheDocument();
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
it('shows aggregation functions', () => {
|
|
1415
|
+
render(
|
|
1416
|
+
<PreAggDetailsPanel
|
|
1417
|
+
preAgg={mockPreAgg}
|
|
1418
|
+
metricFormulas={mockMetricFormulas}
|
|
1419
|
+
onClose={onClose}
|
|
1420
|
+
/>,
|
|
1421
|
+
);
|
|
1422
|
+
expect(screen.getAllByText('SUM').length).toBeGreaterThan(0);
|
|
1423
|
+
expect(screen.getByText('COUNT')).toBeInTheDocument();
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
describe('SQL Section', () => {
|
|
1428
|
+
it('displays SQL section when sql is present', () => {
|
|
1429
|
+
render(
|
|
1430
|
+
<PreAggDetailsPanel
|
|
1431
|
+
preAgg={mockPreAgg}
|
|
1432
|
+
metricFormulas={mockMetricFormulas}
|
|
1433
|
+
onClose={onClose}
|
|
1434
|
+
/>,
|
|
1435
|
+
);
|
|
1436
|
+
expect(screen.getByText('Pre-Aggregation SQL')).toBeInTheDocument();
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('shows copy button', () => {
|
|
1440
|
+
render(
|
|
1441
|
+
<PreAggDetailsPanel
|
|
1442
|
+
preAgg={mockPreAgg}
|
|
1443
|
+
metricFormulas={mockMetricFormulas}
|
|
1444
|
+
onClose={onClose}
|
|
1445
|
+
/>,
|
|
1446
|
+
);
|
|
1447
|
+
expect(screen.getByText('Copy SQL')).toBeInTheDocument();
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('copies SQL when copy button is clicked', () => {
|
|
1451
|
+
render(
|
|
1452
|
+
<PreAggDetailsPanel
|
|
1453
|
+
preAgg={mockPreAgg}
|
|
1454
|
+
metricFormulas={mockMetricFormulas}
|
|
1455
|
+
onClose={onClose}
|
|
1456
|
+
/>,
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
const copyBtn = screen.getByText('Copy SQL');
|
|
1460
|
+
fireEvent.click(copyBtn);
|
|
1461
|
+
|
|
1462
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
1463
|
+
mockPreAgg.sql,
|
|
1464
|
+
);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('hides SQL section when sql is not present', () => {
|
|
1468
|
+
const preAggWithoutSql = { ...mockPreAgg, sql: null };
|
|
1469
|
+
render(
|
|
1470
|
+
<PreAggDetailsPanel
|
|
1471
|
+
preAgg={preAggWithoutSql}
|
|
1472
|
+
metricFormulas={mockMetricFormulas}
|
|
1473
|
+
onClose={onClose}
|
|
1474
|
+
/>,
|
|
1475
|
+
);
|
|
1476
|
+
|
|
1477
|
+
expect(screen.queryByText('Pre-Aggregation SQL')).not.toBeInTheDocument();
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
describe('Pre-aggregation Type Badge', () => {
|
|
1482
|
+
it('shows Pre-aggregation badge', () => {
|
|
1483
|
+
render(
|
|
1484
|
+
<PreAggDetailsPanel
|
|
1485
|
+
preAgg={mockPreAgg}
|
|
1486
|
+
metricFormulas={mockMetricFormulas}
|
|
1487
|
+
onClose={onClose}
|
|
1488
|
+
/>,
|
|
1489
|
+
);
|
|
1490
|
+
expect(screen.getByText('Pre-aggregation')).toBeInTheDocument();
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it('shows parent name in title', () => {
|
|
1494
|
+
render(
|
|
1495
|
+
<PreAggDetailsPanel
|
|
1496
|
+
preAgg={mockPreAgg}
|
|
1497
|
+
metricFormulas={mockMetricFormulas}
|
|
1498
|
+
onClose={onClose}
|
|
1499
|
+
/>,
|
|
1500
|
+
);
|
|
1501
|
+
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
describe('Merge Function Display', () => {
|
|
1506
|
+
it('shows merge functions for components', () => {
|
|
1507
|
+
render(
|
|
1508
|
+
<PreAggDetailsPanel
|
|
1509
|
+
preAgg={mockPreAgg}
|
|
1510
|
+
metricFormulas={mockMetricFormulas}
|
|
1511
|
+
onClose={onClose}
|
|
1512
|
+
/>,
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
// Components have merge functions
|
|
1516
|
+
expect(screen.getAllByText('SUM').length).toBeGreaterThan(0);
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
describe('MetricDetailsPanel', () => {
|
|
1522
|
+
const mockMetric = {
|
|
1523
|
+
name: 'default.avg_repair_price',
|
|
1524
|
+
short_name: 'avg_repair_price',
|
|
1525
|
+
combiner: 'SUM(sum_revenue) / SUM(count_orders)',
|
|
1526
|
+
is_derived: true,
|
|
1527
|
+
components: ['sum_revenue', 'count_orders'],
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
const mockGrainGroups = [
|
|
1531
|
+
{
|
|
1532
|
+
parent_name: 'default.repair_orders',
|
|
1533
|
+
components: [{ name: 'sum_revenue' }, { name: 'count_orders' }],
|
|
1534
|
+
},
|
|
1535
|
+
];
|
|
1536
|
+
|
|
1537
|
+
const onClose = jest.fn();
|
|
1538
|
+
|
|
1539
|
+
beforeEach(() => {
|
|
1540
|
+
jest.clearAllMocks();
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it('returns null when no metric provided', () => {
|
|
1544
|
+
const { container } = render(
|
|
1545
|
+
<MetricDetailsPanel metric={null} onClose={onClose} />,
|
|
1546
|
+
);
|
|
1547
|
+
expect(container.firstChild).toBeNull();
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('renders metric badge', () => {
|
|
1551
|
+
render(
|
|
1552
|
+
<MetricDetailsPanel
|
|
1553
|
+
metric={mockMetric}
|
|
1554
|
+
grainGroups={mockGrainGroups}
|
|
1555
|
+
onClose={onClose}
|
|
1556
|
+
/>,
|
|
1557
|
+
);
|
|
1558
|
+
expect(screen.getByText('Derived Metric')).toBeInTheDocument();
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
it('renders regular metric badge for non-derived', () => {
|
|
1562
|
+
const nonDerivedMetric = { ...mockMetric, is_derived: false };
|
|
1563
|
+
render(
|
|
1564
|
+
<MetricDetailsPanel
|
|
1565
|
+
metric={nonDerivedMetric}
|
|
1566
|
+
grainGroups={mockGrainGroups}
|
|
1567
|
+
onClose={onClose}
|
|
1568
|
+
/>,
|
|
1569
|
+
);
|
|
1570
|
+
expect(screen.getByText('Metric')).toBeInTheDocument();
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
it('displays metric name', () => {
|
|
1574
|
+
render(
|
|
1575
|
+
<MetricDetailsPanel
|
|
1576
|
+
metric={mockMetric}
|
|
1577
|
+
grainGroups={mockGrainGroups}
|
|
1578
|
+
onClose={onClose}
|
|
1579
|
+
/>,
|
|
1580
|
+
);
|
|
1581
|
+
expect(screen.getByText('avg_repair_price')).toBeInTheDocument();
|
|
1582
|
+
expect(screen.getByText('default.avg_repair_price')).toBeInTheDocument();
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('calls onClose when close button clicked', () => {
|
|
1586
|
+
render(
|
|
1587
|
+
<MetricDetailsPanel
|
|
1588
|
+
metric={mockMetric}
|
|
1589
|
+
grainGroups={mockGrainGroups}
|
|
1590
|
+
onClose={onClose}
|
|
1591
|
+
/>,
|
|
1592
|
+
);
|
|
1593
|
+
fireEvent.click(screen.getByTitle('Close panel'));
|
|
1594
|
+
expect(onClose).toHaveBeenCalled();
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
describe('Formula Section', () => {
|
|
1598
|
+
it('displays combiner formula section', () => {
|
|
1599
|
+
render(
|
|
1600
|
+
<MetricDetailsPanel
|
|
1601
|
+
metric={mockMetric}
|
|
1602
|
+
grainGroups={mockGrainGroups}
|
|
1603
|
+
onClose={onClose}
|
|
1604
|
+
/>,
|
|
1605
|
+
);
|
|
1606
|
+
expect(screen.getByText('Combiner Formula')).toBeInTheDocument();
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it('shows the formula', () => {
|
|
1610
|
+
render(
|
|
1611
|
+
<MetricDetailsPanel
|
|
1612
|
+
metric={mockMetric}
|
|
1613
|
+
grainGroups={mockGrainGroups}
|
|
1614
|
+
onClose={onClose}
|
|
1615
|
+
/>,
|
|
1616
|
+
);
|
|
1617
|
+
expect(
|
|
1618
|
+
screen.getByText('SUM(sum_revenue) / SUM(count_orders)'),
|
|
1619
|
+
).toBeInTheDocument();
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
describe('Components Section', () => {
|
|
1624
|
+
it('displays components used section', () => {
|
|
1625
|
+
render(
|
|
1626
|
+
<MetricDetailsPanel
|
|
1627
|
+
metric={mockMetric}
|
|
1628
|
+
grainGroups={mockGrainGroups}
|
|
1629
|
+
onClose={onClose}
|
|
1630
|
+
/>,
|
|
1631
|
+
);
|
|
1632
|
+
expect(screen.getByText('Components Used')).toBeInTheDocument();
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it('shows component tags', () => {
|
|
1636
|
+
render(
|
|
1637
|
+
<MetricDetailsPanel
|
|
1638
|
+
metric={mockMetric}
|
|
1639
|
+
grainGroups={mockGrainGroups}
|
|
1640
|
+
onClose={onClose}
|
|
1641
|
+
/>,
|
|
1642
|
+
);
|
|
1643
|
+
expect(screen.getByText('sum_revenue')).toBeInTheDocument();
|
|
1644
|
+
expect(screen.getByText('count_orders')).toBeInTheDocument();
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
describe('Source Pre-aggregations Section', () => {
|
|
1649
|
+
it('displays source pre-aggregations section', () => {
|
|
1650
|
+
render(
|
|
1651
|
+
<MetricDetailsPanel
|
|
1652
|
+
metric={mockMetric}
|
|
1653
|
+
grainGroups={mockGrainGroups}
|
|
1654
|
+
onClose={onClose}
|
|
1655
|
+
/>,
|
|
1656
|
+
);
|
|
1657
|
+
expect(screen.getByText('Source Pre-aggregations')).toBeInTheDocument();
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
it('shows related pre-agg sources', () => {
|
|
1661
|
+
render(
|
|
1662
|
+
<MetricDetailsPanel
|
|
1663
|
+
metric={mockMetric}
|
|
1664
|
+
grainGroups={mockGrainGroups}
|
|
1665
|
+
onClose={onClose}
|
|
1666
|
+
/>,
|
|
1667
|
+
);
|
|
1668
|
+
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
it('shows empty message when no sources found', () => {
|
|
1672
|
+
render(
|
|
1673
|
+
<MetricDetailsPanel
|
|
1674
|
+
metric={mockMetric}
|
|
1675
|
+
grainGroups={[]}
|
|
1676
|
+
onClose={onClose}
|
|
1677
|
+
/>,
|
|
1678
|
+
);
|
|
1679
|
+
expect(screen.getByText('No source found')).toBeInTheDocument();
|
|
1680
|
+
});
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
describe('Multiple Source Pre-aggregations', () => {
|
|
1684
|
+
it('shows multiple sources when metric uses components from different pre-aggs', () => {
|
|
1685
|
+
const multiSourceGrainGroups = [
|
|
1686
|
+
{
|
|
1687
|
+
parent_name: 'default.repair_orders',
|
|
1688
|
+
components: [{ name: 'sum_revenue' }],
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
parent_name: 'default.inventory',
|
|
1692
|
+
components: [{ name: 'count_orders' }],
|
|
1693
|
+
},
|
|
1694
|
+
];
|
|
1695
|
+
|
|
1696
|
+
render(
|
|
1697
|
+
<MetricDetailsPanel
|
|
1698
|
+
metric={mockMetric}
|
|
1699
|
+
grainGroups={multiSourceGrainGroups}
|
|
1700
|
+
onClose={onClose}
|
|
1701
|
+
/>,
|
|
1702
|
+
);
|
|
1703
|
+
|
|
1704
|
+
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
1705
|
+
expect(screen.getByText('inventory')).toBeInTheDocument();
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
describe('Full Metric Name Display', () => {
|
|
1710
|
+
it('displays full metric name as subtitle', () => {
|
|
1711
|
+
render(
|
|
1712
|
+
<MetricDetailsPanel
|
|
1713
|
+
metric={mockMetric}
|
|
1714
|
+
grainGroups={mockGrainGroups}
|
|
1715
|
+
onClose={onClose}
|
|
1716
|
+
/>,
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
expect(screen.getByText('default.avg_repair_price')).toBeInTheDocument();
|
|
1720
|
+
});
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
describe('Full Name Display', () => {
|
|
1724
|
+
it('renders the full metric name', () => {
|
|
1725
|
+
render(
|
|
1726
|
+
<MemoryRouter>
|
|
1727
|
+
<MetricDetailsPanel
|
|
1728
|
+
metric={mockMetric}
|
|
1729
|
+
grainGroups={mockGrainGroups}
|
|
1730
|
+
onClose={onClose}
|
|
1731
|
+
/>
|
|
1732
|
+
</MemoryRouter>,
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1735
|
+
expect(screen.getByText('default.avg_repair_price')).toBeInTheDocument();
|
|
1736
|
+
});
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
describe('QueryOverviewPanel - Pre-Agg Cards', () => {
|
|
1741
|
+
const mockMeasuresWithPreaggs = {
|
|
1742
|
+
grain_groups: [
|
|
1743
|
+
{
|
|
1744
|
+
parent_name: 'default.repair_orders',
|
|
1745
|
+
aggregability: 'FULL',
|
|
1746
|
+
grain: ['customer_id', 'date_id'], // alphabetically sorted for consistency
|
|
1747
|
+
components: [
|
|
1748
|
+
{ name: 'sum_revenue', expression: 'SUM(revenue)' },
|
|
1749
|
+
{ name: 'count_orders', expression: 'COUNT(*)' },
|
|
1750
|
+
],
|
|
1751
|
+
sql: 'SELECT date_id, customer_id, SUM(revenue) FROM orders GROUP BY 1, 2',
|
|
1752
|
+
},
|
|
1753
|
+
],
|
|
1754
|
+
metric_formulas: [
|
|
1755
|
+
{
|
|
1756
|
+
name: 'default.num_repair_orders',
|
|
1757
|
+
short_name: 'num_repair_orders',
|
|
1758
|
+
combiner: 'SUM(count_orders)',
|
|
1759
|
+
components: ['count_orders'],
|
|
1760
|
+
},
|
|
1761
|
+
],
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
// Key format is: parent_name|sorted_grain_cols
|
|
1765
|
+
const mockPlannedPreaggs = {
|
|
1766
|
+
'default.repair_orders|customer_id,date_id': {
|
|
1767
|
+
id: 'preagg-123',
|
|
1768
|
+
parent_name: 'default.repair_orders',
|
|
1769
|
+
grain_columns: ['customer_id', 'date_id'],
|
|
1770
|
+
strategy: 'incremental_time',
|
|
1771
|
+
schedule: '0 6 * * *',
|
|
1772
|
+
lookback_window: '1 day',
|
|
1773
|
+
workflow_urls: ['https://workflow.example.com/scheduled-123'],
|
|
1774
|
+
availability: {
|
|
1775
|
+
updated_at: '2024-01-15T10:30:00Z',
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
const baseProps = {
|
|
1781
|
+
measuresResult: mockMeasuresWithPreaggs,
|
|
1782
|
+
metricsResult: { sql: 'SELECT ...' },
|
|
1783
|
+
selectedMetrics: ['default.num_repair_orders'],
|
|
1784
|
+
selectedDimensions: ['default.date_dim.dateint'],
|
|
1785
|
+
loadedCubeName: null,
|
|
1786
|
+
plannedPreaggs: mockPlannedPreaggs,
|
|
1787
|
+
onPlanMaterialization: jest.fn(),
|
|
1788
|
+
onUpdateConfig: jest.fn(),
|
|
1789
|
+
onCreateWorkflow: jest.fn(),
|
|
1790
|
+
onRunBackfill: jest.fn(),
|
|
1791
|
+
onDeactivatePreaggWorkflow: jest.fn(),
|
|
1792
|
+
onFetchNodePartitions: jest.fn().mockResolvedValue({
|
|
1793
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
1794
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
1795
|
+
}),
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
beforeEach(() => {
|
|
1799
|
+
jest.clearAllMocks();
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
describe('Pre-Agg Card Display', () => {
|
|
1803
|
+
it('shows pre-agg cards with names', () => {
|
|
1804
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1805
|
+
expect(screen.getByText('repair_orders')).toBeInTheDocument();
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it('shows Active status pill for configured pre-aggs', () => {
|
|
1809
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1810
|
+
expect(screen.getByText('● Active')).toBeInTheDocument();
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
it('shows Not Set status for unconfigured pre-aggs', () => {
|
|
1814
|
+
renderWithRouter(
|
|
1815
|
+
<QueryOverviewPanel {...baseProps} plannedPreaggs={{}} />,
|
|
1816
|
+
);
|
|
1817
|
+
expect(screen.getByText('○ Not Set')).toBeInTheDocument();
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
it('displays grain information', () => {
|
|
1821
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1822
|
+
// Grain is displayed as joined string
|
|
1823
|
+
expect(screen.getByText(/customer_id.*date_id/i)).toBeInTheDocument();
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
it('shows schedule summary for active pre-aggs', () => {
|
|
1827
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1828
|
+
// The schedule '0 6 * * *' should show "Daily at 6:00am" or the raw schedule
|
|
1829
|
+
// Schedule is shown for active preaggs (may appear multiple times)
|
|
1830
|
+
const scheduleTexts = screen.getAllByText(/Daily|6:00|0 6/i);
|
|
1831
|
+
expect(scheduleTexts.length).toBeGreaterThan(0);
|
|
1832
|
+
});
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
describe('Pre-Agg Card Expansion', () => {
|
|
1836
|
+
it('expands card when clicked to show details', async () => {
|
|
1837
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1838
|
+
|
|
1839
|
+
// Find and click the expand button
|
|
1840
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1841
|
+
await act(async () => {
|
|
1842
|
+
fireEvent.click(expandBtn);
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
// Should show strategy details
|
|
1846
|
+
await waitFor(() => {
|
|
1847
|
+
expect(
|
|
1848
|
+
screen.getByText('Incremental (Time-based)'),
|
|
1849
|
+
).toBeInTheDocument();
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
it('shows schedule in expanded view', async () => {
|
|
1854
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1855
|
+
|
|
1856
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1857
|
+
await act(async () => {
|
|
1858
|
+
fireEvent.click(expandBtn);
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
await waitFor(() => {
|
|
1862
|
+
expect(screen.getByText('0 6 * * *')).toBeInTheDocument();
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
it('shows lookback window in expanded view', async () => {
|
|
1867
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1868
|
+
|
|
1869
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1870
|
+
await act(async () => {
|
|
1871
|
+
fireEvent.click(expandBtn);
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
await waitFor(() => {
|
|
1875
|
+
expect(screen.getByText('1 day')).toBeInTheDocument();
|
|
1876
|
+
});
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
it('shows last run time when available', async () => {
|
|
1880
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1881
|
+
|
|
1882
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1883
|
+
await act(async () => {
|
|
1884
|
+
fireEvent.click(expandBtn);
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
await waitFor(() => {
|
|
1888
|
+
expect(screen.getByText('Last Run:')).toBeInTheDocument();
|
|
1889
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
it('shows workflow links when available', async () => {
|
|
1893
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1894
|
+
|
|
1895
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1896
|
+
await act(async () => {
|
|
1897
|
+
fireEvent.click(expandBtn);
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
await waitFor(() => {
|
|
1901
|
+
expect(screen.getByText('Scheduled')).toBeInTheDocument();
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
it('collapses when clicking expand button again', async () => {
|
|
1906
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1907
|
+
|
|
1908
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1909
|
+
await act(async () => {
|
|
1910
|
+
fireEvent.click(expandBtn);
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
await waitFor(() => {
|
|
1914
|
+
expect(
|
|
1915
|
+
screen.getByText('Incremental (Time-based)'),
|
|
1916
|
+
).toBeInTheDocument();
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
// Click collapse button
|
|
1920
|
+
const collapseBtn = screen.getByRole('button', { name: 'Collapse' });
|
|
1921
|
+
await act(async () => {
|
|
1922
|
+
fireEvent.click(collapseBtn);
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
await waitFor(() => {
|
|
1926
|
+
expect(
|
|
1927
|
+
screen.queryByText('Incremental (Time-based)'),
|
|
1928
|
+
).not.toBeInTheDocument();
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
describe('Pre-Agg Edit Config Form', () => {
|
|
1934
|
+
it('opens edit form when Edit Config button is clicked', async () => {
|
|
1935
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1936
|
+
|
|
1937
|
+
// Expand the card first
|
|
1938
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1939
|
+
await act(async () => {
|
|
1940
|
+
fireEvent.click(expandBtn);
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
await waitFor(() => {
|
|
1944
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
// Click Edit Config
|
|
1948
|
+
await act(async () => {
|
|
1949
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
await waitFor(() => {
|
|
1953
|
+
expect(
|
|
1954
|
+
screen.getByText('Edit Materialization Config'),
|
|
1955
|
+
).toBeInTheDocument();
|
|
1956
|
+
});
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
it('shows strategy options in edit form', async () => {
|
|
1960
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1961
|
+
|
|
1962
|
+
// Expand and open edit
|
|
1963
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1964
|
+
await act(async () => {
|
|
1965
|
+
fireEvent.click(expandBtn);
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
await waitFor(() => {
|
|
1969
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
await act(async () => {
|
|
1973
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
await waitFor(() => {
|
|
1977
|
+
expect(screen.getByText('Full')).toBeInTheDocument();
|
|
1978
|
+
expect(screen.getByText('Incremental (Time)')).toBeInTheDocument();
|
|
1979
|
+
});
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
it('closes edit form when close button is clicked', async () => {
|
|
1983
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
1984
|
+
|
|
1985
|
+
// Expand and open edit
|
|
1986
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
1987
|
+
await act(async () => {
|
|
1988
|
+
fireEvent.click(expandBtn);
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
await waitFor(() => {
|
|
1992
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
await act(async () => {
|
|
1996
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
await waitFor(() => {
|
|
2000
|
+
expect(
|
|
2001
|
+
screen.getByText('Edit Materialization Config'),
|
|
2002
|
+
).toBeInTheDocument();
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// Find close button (×) and click
|
|
2006
|
+
const closeBtn = screen.getByRole('button', { name: '×' });
|
|
2007
|
+
await act(async () => {
|
|
2008
|
+
fireEvent.click(closeBtn);
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
await waitFor(() => {
|
|
2012
|
+
expect(
|
|
2013
|
+
screen.queryByText('Edit Materialization Config'),
|
|
2014
|
+
).not.toBeInTheDocument();
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
it('closes edit form when Cancel is clicked', async () => {
|
|
2019
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2020
|
+
|
|
2021
|
+
// Expand and open edit
|
|
2022
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2023
|
+
await act(async () => {
|
|
2024
|
+
fireEvent.click(expandBtn);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
await waitFor(() => {
|
|
2028
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
await act(async () => {
|
|
2032
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
await waitFor(() => {
|
|
2036
|
+
expect(
|
|
2037
|
+
screen.getByText('Edit Materialization Config'),
|
|
2038
|
+
).toBeInTheDocument();
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
// Click Cancel
|
|
2042
|
+
const cancelBtns = screen.getAllByText('Cancel');
|
|
2043
|
+
await act(async () => {
|
|
2044
|
+
fireEvent.click(cancelBtns[cancelBtns.length - 1]);
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
await waitFor(() => {
|
|
2048
|
+
expect(
|
|
2049
|
+
screen.queryByText('Edit Materialization Config'),
|
|
2050
|
+
).not.toBeInTheDocument();
|
|
2051
|
+
});
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
it('calls onUpdateConfig when Save is clicked', async () => {
|
|
2055
|
+
const onUpdateConfig = jest.fn().mockResolvedValue({});
|
|
2056
|
+
renderWithRouter(
|
|
2057
|
+
<QueryOverviewPanel {...baseProps} onUpdateConfig={onUpdateConfig} />,
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
// Expand and open edit
|
|
2061
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2062
|
+
await act(async () => {
|
|
2063
|
+
fireEvent.click(expandBtn);
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
await waitFor(() => {
|
|
2067
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
await act(async () => {
|
|
2071
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
await waitFor(() => {
|
|
2075
|
+
expect(screen.getByText('Save')).toBeInTheDocument();
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// Click Save
|
|
2079
|
+
await act(async () => {
|
|
2080
|
+
fireEvent.click(screen.getByText('Save'));
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
await waitFor(() => {
|
|
2084
|
+
expect(onUpdateConfig).toHaveBeenCalledWith(
|
|
2085
|
+
'preagg-123',
|
|
2086
|
+
expect.any(Object),
|
|
2087
|
+
);
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
describe('Pre-Agg Workflow Actions', () => {
|
|
2093
|
+
it('shows Refresh button when workflow exists', async () => {
|
|
2094
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2095
|
+
|
|
2096
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2097
|
+
await act(async () => {
|
|
2098
|
+
fireEvent.click(expandBtn);
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
await waitFor(() => {
|
|
2102
|
+
expect(screen.getByText('↻ Refresh')).toBeInTheDocument();
|
|
2103
|
+
});
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it('calls onCreateWorkflow when Refresh is clicked', async () => {
|
|
2107
|
+
const onCreateWorkflow = jest.fn().mockResolvedValue({});
|
|
2108
|
+
renderWithRouter(
|
|
2109
|
+
<QueryOverviewPanel
|
|
2110
|
+
{...baseProps}
|
|
2111
|
+
onCreateWorkflow={onCreateWorkflow}
|
|
2112
|
+
/>,
|
|
2113
|
+
);
|
|
2114
|
+
|
|
2115
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2116
|
+
await act(async () => {
|
|
2117
|
+
fireEvent.click(expandBtn);
|
|
2118
|
+
});
|
|
2119
|
+
|
|
2120
|
+
await waitFor(() => {
|
|
2121
|
+
expect(screen.getByText('↻ Refresh')).toBeInTheDocument();
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
await act(async () => {
|
|
2125
|
+
fireEvent.click(screen.getByText('↻ Refresh'));
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
await waitFor(() => {
|
|
2129
|
+
expect(onCreateWorkflow).toHaveBeenCalledWith('preagg-123', true);
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
it('shows Run Backfill button', async () => {
|
|
2134
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2135
|
+
|
|
2136
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2137
|
+
await act(async () => {
|
|
2138
|
+
fireEvent.click(expandBtn);
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
await waitFor(() => {
|
|
2142
|
+
expect(screen.getByText('Run Backfill')).toBeInTheDocument();
|
|
2143
|
+
});
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
it('shows Deactivate button when workflow exists', async () => {
|
|
2147
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2148
|
+
|
|
2149
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2150
|
+
await act(async () => {
|
|
2151
|
+
fireEvent.click(expandBtn);
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
await waitFor(() => {
|
|
2155
|
+
expect(screen.getByText('⏹ Deactivate')).toBeInTheDocument();
|
|
2156
|
+
});
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
it('calls onDeactivatePreaggWorkflow when Deactivate is clicked and confirmed', async () => {
|
|
2160
|
+
const onDeactivatePreaggWorkflow = jest.fn().mockResolvedValue({});
|
|
2161
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
2162
|
+
|
|
2163
|
+
renderWithRouter(
|
|
2164
|
+
<QueryOverviewPanel
|
|
2165
|
+
{...baseProps}
|
|
2166
|
+
onDeactivatePreaggWorkflow={onDeactivatePreaggWorkflow}
|
|
2167
|
+
/>,
|
|
2168
|
+
);
|
|
2169
|
+
|
|
2170
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2171
|
+
await act(async () => {
|
|
2172
|
+
fireEvent.click(expandBtn);
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
await waitFor(() => {
|
|
2176
|
+
expect(screen.getByText('⏹ Deactivate')).toBeInTheDocument();
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
await act(async () => {
|
|
2180
|
+
fireEvent.click(screen.getByText('⏹ Deactivate'));
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
await waitFor(() => {
|
|
2184
|
+
expect(window.confirm).toHaveBeenCalled();
|
|
2185
|
+
expect(onDeactivatePreaggWorkflow).toHaveBeenCalledWith('preagg-123');
|
|
2186
|
+
});
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
it('does not deactivate when confirmation is cancelled', async () => {
|
|
2190
|
+
const onDeactivatePreaggWorkflow = jest.fn().mockResolvedValue({});
|
|
2191
|
+
window.confirm = jest.fn().mockReturnValue(false);
|
|
2192
|
+
|
|
2193
|
+
renderWithRouter(
|
|
2194
|
+
<QueryOverviewPanel
|
|
2195
|
+
{...baseProps}
|
|
2196
|
+
onDeactivatePreaggWorkflow={onDeactivatePreaggWorkflow}
|
|
2197
|
+
/>,
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2200
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2201
|
+
await act(async () => {
|
|
2202
|
+
fireEvent.click(expandBtn);
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
await waitFor(() => {
|
|
2206
|
+
expect(screen.getByText('⏹ Deactivate')).toBeInTheDocument();
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
await act(async () => {
|
|
2210
|
+
fireEvent.click(screen.getByText('⏹ Deactivate'));
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
expect(window.confirm).toHaveBeenCalled();
|
|
2214
|
+
expect(onDeactivatePreaggWorkflow).not.toHaveBeenCalled();
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
describe('Create Workflow Button', () => {
|
|
2219
|
+
it('shows Create Workflow button when no workflow exists', async () => {
|
|
2220
|
+
const preaggWithoutWorkflow = {
|
|
2221
|
+
'default.repair_orders|customer_id,date_id': {
|
|
2222
|
+
id: 'preagg-123',
|
|
2223
|
+
parent_name: 'default.repair_orders',
|
|
2224
|
+
grain_columns: ['date_id', 'customer_id'],
|
|
2225
|
+
strategy: 'incremental_time',
|
|
2226
|
+
schedule: '0 6 * * *',
|
|
2227
|
+
workflow_urls: [], // No workflows
|
|
2228
|
+
},
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
renderWithRouter(
|
|
2232
|
+
<QueryOverviewPanel
|
|
2233
|
+
{...baseProps}
|
|
2234
|
+
plannedPreaggs={preaggWithoutWorkflow}
|
|
2235
|
+
/>,
|
|
2236
|
+
);
|
|
2237
|
+
|
|
2238
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2239
|
+
await act(async () => {
|
|
2240
|
+
fireEvent.click(expandBtn);
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
await waitFor(() => {
|
|
2244
|
+
expect(screen.getByText('Create Workflow')).toBeInTheDocument();
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
it('calls onCreateWorkflow when Create Workflow is clicked', async () => {
|
|
2249
|
+
const onCreateWorkflow = jest.fn().mockResolvedValue({
|
|
2250
|
+
workflow_urls: ['https://workflow.example.com/new-123'],
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
const preaggWithoutWorkflow = {
|
|
2254
|
+
'default.repair_orders|customer_id,date_id': {
|
|
2255
|
+
id: 'preagg-123',
|
|
2256
|
+
parent_name: 'default.repair_orders',
|
|
2257
|
+
grain_columns: ['date_id', 'customer_id'],
|
|
2258
|
+
strategy: 'incremental_time',
|
|
2259
|
+
schedule: '0 6 * * *',
|
|
2260
|
+
workflow_urls: [],
|
|
2261
|
+
},
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
renderWithRouter(
|
|
2265
|
+
<QueryOverviewPanel
|
|
2266
|
+
{...baseProps}
|
|
2267
|
+
plannedPreaggs={preaggWithoutWorkflow}
|
|
2268
|
+
onCreateWorkflow={onCreateWorkflow}
|
|
2269
|
+
/>,
|
|
2270
|
+
);
|
|
2271
|
+
|
|
2272
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2273
|
+
await act(async () => {
|
|
2274
|
+
fireEvent.click(expandBtn);
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
await waitFor(() => {
|
|
2278
|
+
expect(screen.getByText('Create Workflow')).toBeInTheDocument();
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
await act(async () => {
|
|
2282
|
+
fireEvent.click(screen.getByText('Create Workflow'));
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
await waitFor(() => {
|
|
2286
|
+
expect(onCreateWorkflow).toHaveBeenCalledWith('preagg-123');
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
describe('QueryOverviewPanel - Backfill Modal', () => {
|
|
2293
|
+
const mockMeasuresWithPreaggs = {
|
|
2294
|
+
grain_groups: [
|
|
2295
|
+
{
|
|
2296
|
+
parent_name: 'default.repair_orders',
|
|
2297
|
+
aggregability: 'FULL',
|
|
2298
|
+
grain: ['date_id'],
|
|
2299
|
+
components: [{ name: 'count_orders' }],
|
|
2300
|
+
},
|
|
2301
|
+
],
|
|
2302
|
+
metric_formulas: [
|
|
2303
|
+
{
|
|
2304
|
+
name: 'default.num_repair_orders',
|
|
2305
|
+
short_name: 'num_repair_orders',
|
|
2306
|
+
combiner: 'SUM(count_orders)',
|
|
2307
|
+
components: ['count_orders'],
|
|
2308
|
+
},
|
|
2309
|
+
],
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
// Key must match normalized grain
|
|
2313
|
+
const mockPlannedPreaggs = {
|
|
2314
|
+
'default.repair_orders|date_id': {
|
|
2315
|
+
id: 'preagg-456',
|
|
2316
|
+
parent_name: 'default.repair_orders',
|
|
2317
|
+
grain_columns: ['date_id'],
|
|
2318
|
+
strategy: 'incremental_time',
|
|
2319
|
+
schedule: '0 6 * * *',
|
|
2320
|
+
workflow_urls: ['https://workflow.example.com/test'],
|
|
2321
|
+
},
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
const baseProps = {
|
|
2325
|
+
measuresResult: mockMeasuresWithPreaggs,
|
|
2326
|
+
metricsResult: { sql: 'SELECT ...' },
|
|
2327
|
+
selectedMetrics: ['default.num_repair_orders'],
|
|
2328
|
+
selectedDimensions: ['default.date_dim.date_id'], // Must have at least one dimension
|
|
2329
|
+
plannedPreaggs: mockPlannedPreaggs,
|
|
2330
|
+
onRunBackfill: jest.fn(),
|
|
2331
|
+
onFetchNodePartitions: jest.fn().mockResolvedValue({
|
|
2332
|
+
columns: [],
|
|
2333
|
+
temporalPartitions: [],
|
|
2334
|
+
}),
|
|
2335
|
+
};
|
|
2336
|
+
|
|
2337
|
+
beforeEach(() => {
|
|
2338
|
+
jest.clearAllMocks();
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
it('opens backfill modal when Run Backfill is clicked on preagg card', async () => {
|
|
2342
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2343
|
+
|
|
2344
|
+
// Find and expand the preagg card
|
|
2345
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2346
|
+
await act(async () => {
|
|
2347
|
+
fireEvent.click(expandBtn);
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
// Wait for Run Backfill button to appear in expanded view
|
|
2351
|
+
await waitFor(() => {
|
|
2352
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2353
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2356
|
+
// Click the Run Backfill button on preagg
|
|
2357
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2358
|
+
await act(async () => {
|
|
2359
|
+
fireEvent.click(backfillBtns[backfillBtns.length - 1]);
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
// Modal should open with heading
|
|
2363
|
+
await waitFor(() => {
|
|
2364
|
+
const headings = screen.getAllByRole('heading', { level: 3 });
|
|
2365
|
+
const backfillHeading = headings.find(
|
|
2366
|
+
h => h.textContent === 'Run Backfill',
|
|
2367
|
+
);
|
|
2368
|
+
expect(backfillHeading).toBeInTheDocument();
|
|
2369
|
+
});
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
it('shows start and end date inputs in modal', async () => {
|
|
2373
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2374
|
+
|
|
2375
|
+
// Expand card
|
|
2376
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2377
|
+
await act(async () => {
|
|
2378
|
+
fireEvent.click(expandBtn);
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// Open modal
|
|
2382
|
+
await waitFor(() => {
|
|
2383
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2384
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2388
|
+
await act(async () => {
|
|
2389
|
+
fireEvent.click(backfillBtns[backfillBtns.length - 1]);
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
await waitFor(() => {
|
|
2393
|
+
expect(screen.getByText('Start Date')).toBeInTheDocument();
|
|
2394
|
+
expect(screen.getByText('End Date')).toBeInTheDocument();
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
it('calls onRunBackfill when Start Backfill is clicked in modal', async () => {
|
|
2399
|
+
const onRunBackfill = jest
|
|
2400
|
+
.fn()
|
|
2401
|
+
.mockResolvedValue({ job_url: 'https://job.example.com' });
|
|
2402
|
+
renderWithRouter(
|
|
2403
|
+
<QueryOverviewPanel {...baseProps} onRunBackfill={onRunBackfill} />,
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
// Expand card
|
|
2407
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2408
|
+
await act(async () => {
|
|
2409
|
+
fireEvent.click(expandBtn);
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
// Open modal
|
|
2413
|
+
await waitFor(() => {
|
|
2414
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2415
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2419
|
+
await act(async () => {
|
|
2420
|
+
fireEvent.click(backfillBtns[backfillBtns.length - 1]);
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
// Wait for modal
|
|
2424
|
+
await waitFor(() => {
|
|
2425
|
+
expect(screen.getByText('Start Date')).toBeInTheDocument();
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
// Click Start Backfill
|
|
2429
|
+
await act(async () => {
|
|
2430
|
+
fireEvent.click(screen.getByText('Start Backfill'));
|
|
2431
|
+
});
|
|
2432
|
+
|
|
2433
|
+
await waitFor(() => {
|
|
2434
|
+
expect(onRunBackfill).toHaveBeenCalledWith(
|
|
2435
|
+
'preagg-456',
|
|
2436
|
+
expect.any(String),
|
|
2437
|
+
expect.any(String),
|
|
2438
|
+
);
|
|
2439
|
+
});
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
it('closes modal when Cancel is clicked', async () => {
|
|
2443
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2444
|
+
|
|
2445
|
+
// Expand card
|
|
2446
|
+
const expandBtn = screen.getByRole('button', { name: 'Expand' });
|
|
2447
|
+
await act(async () => {
|
|
2448
|
+
fireEvent.click(expandBtn);
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
// Open modal
|
|
2452
|
+
await waitFor(() => {
|
|
2453
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2454
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2458
|
+
await act(async () => {
|
|
2459
|
+
fireEvent.click(backfillBtns[backfillBtns.length - 1]);
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
// Wait for modal
|
|
2463
|
+
await waitFor(() => {
|
|
2464
|
+
expect(screen.getByText('Start Date')).toBeInTheDocument();
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
// Click Cancel
|
|
2468
|
+
const cancelBtns = screen.getAllByText('Cancel');
|
|
2469
|
+
await act(async () => {
|
|
2470
|
+
fireEvent.click(cancelBtns[cancelBtns.length - 1]);
|
|
2471
|
+
});
|
|
2472
|
+
|
|
2473
|
+
// Modal should close
|
|
2474
|
+
await waitFor(() => {
|
|
2475
|
+
expect(screen.queryByText('Start Date')).not.toBeInTheDocument();
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
describe('QueryOverviewPanel - Cube Materialization Section', () => {
|
|
2481
|
+
// Cube section only appears when workflowUrls.length > 0
|
|
2482
|
+
// Also, selectedMetrics AND selectedDimensions must be non-empty to render main content
|
|
2483
|
+
const mockMeasuresResult = {
|
|
2484
|
+
grain_groups: [
|
|
2485
|
+
{
|
|
2486
|
+
parent_name: 'default.repair_orders',
|
|
2487
|
+
grain: ['date_id'],
|
|
2488
|
+
components: [{ name: 'count_orders' }],
|
|
2489
|
+
},
|
|
2490
|
+
],
|
|
2491
|
+
metric_formulas: [
|
|
2492
|
+
{
|
|
2493
|
+
name: 'default.num_repair_orders',
|
|
2494
|
+
short_name: 'num_repair_orders',
|
|
2495
|
+
combiner: 'SUM(count_orders)',
|
|
2496
|
+
components: ['count_orders'],
|
|
2497
|
+
},
|
|
2498
|
+
],
|
|
2499
|
+
};
|
|
2500
|
+
|
|
2501
|
+
const mockCubeMaterialization = {
|
|
2502
|
+
strategy: 'incremental_time',
|
|
2503
|
+
schedule: '0 8 * * *',
|
|
2504
|
+
lookbackWindow: '2 days',
|
|
2505
|
+
druidDatasource: 'dj__test_cube',
|
|
2506
|
+
preaggTables: ['default.repair_orders'],
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// These workflow URLs trigger the cube section to show
|
|
2510
|
+
const mockWorkflowUrls = [
|
|
2511
|
+
'https://workflow.example.com/scheduled',
|
|
2512
|
+
'https://workflow.example.com/adhoc_backfill',
|
|
2513
|
+
];
|
|
2514
|
+
|
|
2515
|
+
const baseProps = {
|
|
2516
|
+
measuresResult: mockMeasuresResult,
|
|
2517
|
+
metricsResult: { sql: 'SELECT ...' },
|
|
2518
|
+
selectedMetrics: ['default.num_repair_orders'],
|
|
2519
|
+
selectedDimensions: ['default.date_dim.date_id'], // Must have at least one dimension
|
|
2520
|
+
plannedPreaggs: {},
|
|
2521
|
+
loadedCubeName: 'default.test_cube',
|
|
2522
|
+
cubeMaterialization: mockCubeMaterialization,
|
|
2523
|
+
workflowUrls: mockWorkflowUrls, // This triggers cube section
|
|
2524
|
+
onUpdateCubeConfig: jest.fn(),
|
|
2525
|
+
onRefreshCubeWorkflow: jest.fn(),
|
|
2526
|
+
onRunCubeBackfill: jest.fn(),
|
|
2527
|
+
onDeactivateCubeWorkflow: jest.fn(),
|
|
2528
|
+
onFetchNodePartitions: jest.fn().mockResolvedValue({
|
|
2529
|
+
columns: [],
|
|
2530
|
+
temporalPartitions: [],
|
|
2531
|
+
}),
|
|
2532
|
+
};
|
|
2533
|
+
|
|
2534
|
+
beforeEach(() => {
|
|
2535
|
+
jest.clearAllMocks();
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
describe('Cube Summary Display', () => {
|
|
2539
|
+
it('shows cube section when workflowUrls exist', () => {
|
|
2540
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2541
|
+
expect(screen.getByText('Druid Cube')).toBeInTheDocument();
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
it('displays cube datasource name', () => {
|
|
2545
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2546
|
+
expect(screen.getByText('dj__test_cube')).toBeInTheDocument();
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
it('shows Active status for configured cube', () => {
|
|
2550
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2551
|
+
const activePills = screen.getAllByText('● Active');
|
|
2552
|
+
expect(activePills.length).toBeGreaterThan(0);
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
it('shows Workflow active status', () => {
|
|
2556
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2557
|
+
expect(screen.getByText('Workflow active')).toBeInTheDocument();
|
|
2558
|
+
});
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
describe('Cube Card Expansion', () => {
|
|
2562
|
+
it('expands cube card when clicked', async () => {
|
|
2563
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2564
|
+
|
|
2565
|
+
// Find cube expand button (first one)
|
|
2566
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2567
|
+
await act(async () => {
|
|
2568
|
+
fireEvent.click(expandBtns[0]);
|
|
2569
|
+
});
|
|
2570
|
+
|
|
2571
|
+
// Should show strategy details
|
|
2572
|
+
await waitFor(() => {
|
|
2573
|
+
expect(screen.getByText('Strategy:')).toBeInTheDocument();
|
|
2574
|
+
});
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
it('shows workflow links when expanded', async () => {
|
|
2578
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2579
|
+
|
|
2580
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2581
|
+
await act(async () => {
|
|
2582
|
+
fireEvent.click(expandBtns[0]);
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
await waitFor(() => {
|
|
2586
|
+
// Check for workflow links
|
|
2587
|
+
expect(screen.getByText('Workflows:')).toBeInTheDocument();
|
|
2588
|
+
});
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
it('shows action buttons when expanded', async () => {
|
|
2592
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2593
|
+
|
|
2594
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2595
|
+
await act(async () => {
|
|
2596
|
+
fireEvent.click(expandBtns[0]);
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
await waitFor(() => {
|
|
2600
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2601
|
+
expect(screen.getByText('↻ Refresh')).toBeInTheDocument();
|
|
2602
|
+
});
|
|
2603
|
+
});
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
describe('Cube Edit Config Form', () => {
|
|
2607
|
+
it('opens edit form when Edit Config is clicked', async () => {
|
|
2608
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2609
|
+
|
|
2610
|
+
// Expand cube card first
|
|
2611
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2612
|
+
await act(async () => {
|
|
2613
|
+
fireEvent.click(expandBtns[0]);
|
|
2614
|
+
});
|
|
2615
|
+
|
|
2616
|
+
await waitFor(() => {
|
|
2617
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
// Click Edit Config
|
|
2621
|
+
await act(async () => {
|
|
2622
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
await waitFor(() => {
|
|
2626
|
+
expect(
|
|
2627
|
+
screen.getByText('Edit Materialization Config'),
|
|
2628
|
+
).toBeInTheDocument();
|
|
2629
|
+
});
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
it('shows strategy options in edit form', async () => {
|
|
2633
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2634
|
+
|
|
2635
|
+
// Expand and open edit
|
|
2636
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2637
|
+
await act(async () => {
|
|
2638
|
+
fireEvent.click(expandBtns[0]);
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
await waitFor(() => {
|
|
2642
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
await act(async () => {
|
|
2646
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
2647
|
+
});
|
|
2648
|
+
|
|
2649
|
+
await waitFor(() => {
|
|
2650
|
+
expect(screen.getByText('Full')).toBeInTheDocument();
|
|
2651
|
+
expect(screen.getByText('Incremental (Time)')).toBeInTheDocument();
|
|
2652
|
+
});
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2655
|
+
it('calls onUpdateCubeConfig when Save is clicked', async () => {
|
|
2656
|
+
const onUpdateCubeConfig = jest.fn().mockResolvedValue({});
|
|
2657
|
+
renderWithRouter(
|
|
2658
|
+
<QueryOverviewPanel
|
|
2659
|
+
{...baseProps}
|
|
2660
|
+
onUpdateCubeConfig={onUpdateCubeConfig}
|
|
2661
|
+
/>,
|
|
2662
|
+
);
|
|
2663
|
+
|
|
2664
|
+
// Expand and open edit
|
|
2665
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2666
|
+
await act(async () => {
|
|
2667
|
+
fireEvent.click(expandBtns[0]);
|
|
2668
|
+
});
|
|
2669
|
+
|
|
2670
|
+
await waitFor(() => {
|
|
2671
|
+
expect(screen.getByText('Edit Config')).toBeInTheDocument();
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
await act(async () => {
|
|
2675
|
+
fireEvent.click(screen.getByText('Edit Config'));
|
|
2676
|
+
});
|
|
2677
|
+
|
|
2678
|
+
await waitFor(() => {
|
|
2679
|
+
expect(screen.getByText('Save')).toBeInTheDocument();
|
|
2680
|
+
});
|
|
2681
|
+
|
|
2682
|
+
await act(async () => {
|
|
2683
|
+
fireEvent.click(screen.getByText('Save'));
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
await waitFor(() => {
|
|
2687
|
+
expect(onUpdateCubeConfig).toHaveBeenCalled();
|
|
2688
|
+
});
|
|
2689
|
+
});
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
describe('Cube Workflow Actions', () => {
|
|
2693
|
+
it('calls onRefreshCubeWorkflow when Refresh is clicked', async () => {
|
|
2694
|
+
const onRefreshCubeWorkflow = jest.fn().mockResolvedValue({});
|
|
2695
|
+
renderWithRouter(
|
|
2696
|
+
<QueryOverviewPanel
|
|
2697
|
+
{...baseProps}
|
|
2698
|
+
onRefreshCubeWorkflow={onRefreshCubeWorkflow}
|
|
2699
|
+
/>,
|
|
2700
|
+
);
|
|
2701
|
+
|
|
2702
|
+
// Expand cube card
|
|
2703
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2704
|
+
await act(async () => {
|
|
2705
|
+
fireEvent.click(expandBtns[0]);
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
await waitFor(() => {
|
|
2709
|
+
expect(screen.getByText('↻ Refresh')).toBeInTheDocument();
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
await act(async () => {
|
|
2713
|
+
fireEvent.click(screen.getByText('↻ Refresh'));
|
|
2714
|
+
});
|
|
2715
|
+
|
|
2716
|
+
await waitFor(() => {
|
|
2717
|
+
expect(onRefreshCubeWorkflow).toHaveBeenCalled();
|
|
2718
|
+
});
|
|
2719
|
+
});
|
|
2720
|
+
|
|
2721
|
+
it('calls onDeactivateCubeWorkflow when Deactivate is clicked and confirmed', async () => {
|
|
2722
|
+
const onDeactivateCubeWorkflow = jest.fn().mockResolvedValue({});
|
|
2723
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
2724
|
+
|
|
2725
|
+
renderWithRouter(
|
|
2726
|
+
<QueryOverviewPanel
|
|
2727
|
+
{...baseProps}
|
|
2728
|
+
onDeactivateCubeWorkflow={onDeactivateCubeWorkflow}
|
|
2729
|
+
/>,
|
|
2730
|
+
);
|
|
2731
|
+
|
|
2732
|
+
// Expand cube card
|
|
2733
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2734
|
+
await act(async () => {
|
|
2735
|
+
fireEvent.click(expandBtns[0]);
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
await waitFor(() => {
|
|
2739
|
+
expect(screen.getByText('⏹ Deactivate')).toBeInTheDocument();
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
await act(async () => {
|
|
2743
|
+
fireEvent.click(screen.getByText('⏹ Deactivate'));
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
await waitFor(() => {
|
|
2747
|
+
expect(window.confirm).toHaveBeenCalled();
|
|
2748
|
+
expect(onDeactivateCubeWorkflow).toHaveBeenCalled();
|
|
2749
|
+
});
|
|
2750
|
+
});
|
|
2751
|
+
});
|
|
2752
|
+
|
|
2753
|
+
describe('Cube Backfill Modal', () => {
|
|
2754
|
+
it('opens cube backfill modal when Run Backfill is clicked', async () => {
|
|
2755
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2756
|
+
|
|
2757
|
+
// Expand cube card
|
|
2758
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2759
|
+
await act(async () => {
|
|
2760
|
+
fireEvent.click(expandBtns[0]);
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
await waitFor(() => {
|
|
2764
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2765
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2769
|
+
await act(async () => {
|
|
2770
|
+
fireEvent.click(backfillBtns[0]);
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
// Modal should show
|
|
2774
|
+
await waitFor(() => {
|
|
2775
|
+
expect(screen.getByText('Run Cube Backfill')).toBeInTheDocument();
|
|
2776
|
+
});
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
it('shows backfill description in modal', async () => {
|
|
2780
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2781
|
+
|
|
2782
|
+
// Expand and open modal
|
|
2783
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2784
|
+
await act(async () => {
|
|
2785
|
+
fireEvent.click(expandBtns[0]);
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
await waitFor(() => {
|
|
2789
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2790
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2794
|
+
await act(async () => {
|
|
2795
|
+
fireEvent.click(backfillBtns[0]);
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
await waitFor(() => {
|
|
2799
|
+
expect(
|
|
2800
|
+
screen.getByText(/Run a backfill for the specified date range/i),
|
|
2801
|
+
).toBeInTheDocument();
|
|
2802
|
+
});
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
it('calls onRunCubeBackfill when Start Backfill is clicked', async () => {
|
|
2806
|
+
const onRunCubeBackfill = jest.fn().mockResolvedValue({
|
|
2807
|
+
workflow_urls: ['https://workflow.example.com/backfill'],
|
|
2808
|
+
});
|
|
2809
|
+
renderWithRouter(
|
|
2810
|
+
<QueryOverviewPanel
|
|
2811
|
+
{...baseProps}
|
|
2812
|
+
onRunCubeBackfill={onRunCubeBackfill}
|
|
2813
|
+
/>,
|
|
2814
|
+
);
|
|
2815
|
+
|
|
2816
|
+
// Expand and open modal
|
|
2817
|
+
const expandBtns = screen.getAllByRole('button', { name: 'Expand' });
|
|
2818
|
+
await act(async () => {
|
|
2819
|
+
fireEvent.click(expandBtns[0]);
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
await waitFor(() => {
|
|
2823
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2824
|
+
expect(backfillBtns.length).toBeGreaterThan(0);
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
const backfillBtns = screen.getAllByText('Run Backfill');
|
|
2828
|
+
await act(async () => {
|
|
2829
|
+
fireEvent.click(backfillBtns[0]);
|
|
2830
|
+
});
|
|
2831
|
+
|
|
2832
|
+
await waitFor(() => {
|
|
2833
|
+
expect(screen.getByText('Run Cube Backfill')).toBeInTheDocument();
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
// Click Start Backfill
|
|
2837
|
+
await act(async () => {
|
|
2838
|
+
fireEvent.click(screen.getByText('Start Backfill'));
|
|
2839
|
+
});
|
|
2840
|
+
|
|
2841
|
+
await waitFor(() => {
|
|
2842
|
+
expect(onRunCubeBackfill).toHaveBeenCalled();
|
|
2843
|
+
});
|
|
2844
|
+
});
|
|
2845
|
+
});
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
describe('QueryOverviewPanel - Custom Schedule and Druid Config', () => {
|
|
2849
|
+
// This tests the materialization config form which shows when there are unconfigured preaggs
|
|
2850
|
+
const mockMeasuresResult = {
|
|
2851
|
+
grain_groups: [
|
|
2852
|
+
{
|
|
2853
|
+
parent_name: 'default.repair_orders',
|
|
2854
|
+
grain: ['date_id'],
|
|
2855
|
+
components: [{ name: 'count_orders' }],
|
|
2856
|
+
},
|
|
2857
|
+
],
|
|
2858
|
+
metric_formulas: [
|
|
2859
|
+
{
|
|
2860
|
+
name: 'default.num_repair_orders',
|
|
2861
|
+
short_name: 'num_repair_orders',
|
|
2862
|
+
combiner: 'SUM(count_orders)',
|
|
2863
|
+
components: ['count_orders'],
|
|
2864
|
+
},
|
|
2865
|
+
],
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
const baseProps = {
|
|
2869
|
+
measuresResult: mockMeasuresResult,
|
|
2870
|
+
metricsResult: { sql: 'SELECT ...' },
|
|
2871
|
+
selectedMetrics: ['default.num_repair_orders'],
|
|
2872
|
+
selectedDimensions: ['default.date_dim.date_id'], // Must have at least one dimension
|
|
2873
|
+
plannedPreaggs: {}, // No planned preaggs = shows Configure button
|
|
2874
|
+
onPlanMaterialization: jest.fn(),
|
|
2875
|
+
onFetchNodePartitions: jest.fn().mockResolvedValue({
|
|
2876
|
+
columns: [{ name: 'date_id', type: 'int' }],
|
|
2877
|
+
temporalPartitions: [{ name: 'date_id', granularity: 'DAY' }],
|
|
2878
|
+
}),
|
|
2879
|
+
};
|
|
2880
|
+
|
|
2881
|
+
beforeEach(() => {
|
|
2882
|
+
jest.clearAllMocks();
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
describe('Custom Schedule Input', () => {
|
|
2886
|
+
it('shows schedule options in config form', async () => {
|
|
2887
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2888
|
+
|
|
2889
|
+
// Open config form
|
|
2890
|
+
const configureBtn = screen.getByText('Configure');
|
|
2891
|
+
await act(async () => {
|
|
2892
|
+
fireEvent.click(configureBtn);
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2895
|
+
// Wait for form to load
|
|
2896
|
+
await waitFor(() => {
|
|
2897
|
+
expect(
|
|
2898
|
+
screen.getByText('Configure Materialization'),
|
|
2899
|
+
).toBeInTheDocument();
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2902
|
+
// Should have schedule label
|
|
2903
|
+
expect(screen.getByText('Schedule')).toBeInTheDocument();
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
it('allows selecting custom schedule type', async () => {
|
|
2907
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2908
|
+
|
|
2909
|
+
// Open config form
|
|
2910
|
+
const configureBtn = screen.getByText('Configure');
|
|
2911
|
+
await act(async () => {
|
|
2912
|
+
fireEvent.click(configureBtn);
|
|
2913
|
+
});
|
|
2914
|
+
|
|
2915
|
+
await waitFor(() => {
|
|
2916
|
+
expect(
|
|
2917
|
+
screen.getByText('Configure Materialization'),
|
|
2918
|
+
).toBeInTheDocument();
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
// Find and change schedule select
|
|
2922
|
+
const selects = screen.getAllByRole('combobox');
|
|
2923
|
+
expect(selects.length).toBeGreaterThan(0);
|
|
2924
|
+
});
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2927
|
+
describe('Druid Cube Namespace and Name Inputs', () => {
|
|
2928
|
+
it('shows Druid cube config when checkbox is enabled', async () => {
|
|
2929
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2930
|
+
|
|
2931
|
+
// Open config form
|
|
2932
|
+
const configureBtn = screen.getByText('Configure');
|
|
2933
|
+
await act(async () => {
|
|
2934
|
+
fireEvent.click(configureBtn);
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
await waitFor(() => {
|
|
2938
|
+
expect(
|
|
2939
|
+
screen.getByText('Enable Druid cube materialization'),
|
|
2940
|
+
).toBeInTheDocument();
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
// The checkbox should be checked by default, showing cube name inputs
|
|
2944
|
+
await waitFor(() => {
|
|
2945
|
+
expect(screen.getByText('Cube Name')).toBeInTheDocument();
|
|
2946
|
+
});
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
it('shows cube namespace and name placeholders', async () => {
|
|
2950
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2951
|
+
|
|
2952
|
+
// Open config form
|
|
2953
|
+
const configureBtn = screen.getByText('Configure');
|
|
2954
|
+
await act(async () => {
|
|
2955
|
+
fireEvent.click(configureBtn);
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
await waitFor(() => {
|
|
2959
|
+
expect(screen.getByPlaceholderText('users.myname')).toBeInTheDocument();
|
|
2960
|
+
expect(screen.getByPlaceholderText('my_cube')).toBeInTheDocument();
|
|
2961
|
+
});
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
it('shows full cube name preview', async () => {
|
|
2965
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2966
|
+
|
|
2967
|
+
// Open config form
|
|
2968
|
+
const configureBtn = screen.getByText('Configure');
|
|
2969
|
+
await act(async () => {
|
|
2970
|
+
fireEvent.click(configureBtn);
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
await waitFor(() => {
|
|
2974
|
+
expect(screen.getByText('Full name:')).toBeInTheDocument();
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
describe('Form submission button text', () => {
|
|
2980
|
+
it('shows Create Pre-Agg Workflows button when Druid is enabled', async () => {
|
|
2981
|
+
renderWithRouter(<QueryOverviewPanel {...baseProps} />);
|
|
2982
|
+
|
|
2983
|
+
// Open config form
|
|
2984
|
+
const configureBtn = screen.getByText('Configure');
|
|
2985
|
+
await act(async () => {
|
|
2986
|
+
fireEvent.click(configureBtn);
|
|
2987
|
+
});
|
|
2988
|
+
|
|
2989
|
+
await waitFor(() => {
|
|
2990
|
+
expect(
|
|
2991
|
+
screen.getByText('Create Pre-Agg Workflows & Schedule Cube'),
|
|
2992
|
+
).toBeInTheDocument();
|
|
2993
|
+
});
|
|
636
2994
|
});
|
|
637
2995
|
});
|
|
638
2996
|
});
|