datajunction-ui 0.0.26 → 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.
Files changed (28) hide show
  1. package/package.json +2 -2
  2. package/src/app/components/Search.jsx +41 -33
  3. package/src/app/components/__tests__/Search.test.jsx +46 -11
  4. package/src/app/index.tsx +3 -3
  5. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
  6. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
  7. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
  8. package/src/app/pages/AddEditNodePage/index.jsx +61 -17
  9. package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
  10. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
  11. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
  12. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
  13. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
  14. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
  15. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
  16. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
  17. package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
  18. package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
  19. package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
  20. package/src/app/pages/Root/index.tsx +1 -6
  21. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
  22. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
  23. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
  24. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
  25. package/src/app/services/DJService.js +492 -3
  26. package/src/app/services/__tests__/DJService.test.jsx +582 -0
  27. package/src/mocks/mockNodes.jsx +36 -0
  28. package/webpack.config.js +27 -0
@@ -2191,4 +2191,586 @@ describe('DataJunctionAPI', () => {
2191
2191
  );
2192
2192
  expect(result).toHaveLength(2);
2193
2193
  });
2194
+
2195
+ // Test listCubesForPreset (lines 121-155)
2196
+ it('calls listCubesForPreset correctly', async () => {
2197
+ fetch.mockResponseOnce(
2198
+ JSON.stringify({
2199
+ data: {
2200
+ findNodes: [
2201
+ { name: 'default.cube1', current: { displayName: 'Cube 1' } },
2202
+ { name: 'default.cube2', current: { displayName: null } },
2203
+ ],
2204
+ },
2205
+ }),
2206
+ );
2207
+
2208
+ const result = await DataJunctionAPI.listCubesForPreset();
2209
+ expect(fetch).toHaveBeenCalledWith(
2210
+ expect.stringContaining('/graphql'),
2211
+ expect.objectContaining({
2212
+ method: 'POST',
2213
+ credentials: 'include',
2214
+ }),
2215
+ );
2216
+ expect(result).toEqual([
2217
+ { name: 'default.cube1', display_name: 'Cube 1' },
2218
+ { name: 'default.cube2', display_name: null },
2219
+ ]);
2220
+ });
2221
+
2222
+ it('handles listCubesForPreset error gracefully', async () => {
2223
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
2224
+ fetch.mockRejectOnce(new Error('Network error'));
2225
+
2226
+ const result = await DataJunctionAPI.listCubesForPreset();
2227
+ expect(result).toEqual([]);
2228
+ consoleSpy.mockRestore();
2229
+ });
2230
+
2231
+ // Test cubeForPlanner (lines 159-233)
2232
+ it('calls cubeForPlanner correctly', async () => {
2233
+ fetch.mockResponseOnce(
2234
+ JSON.stringify({
2235
+ data: {
2236
+ findNodes: [
2237
+ {
2238
+ name: 'default.cube1',
2239
+ current: {
2240
+ displayName: 'Cube 1',
2241
+ cubeMetrics: [{ name: 'metric1' }, { name: 'metric2' }],
2242
+ cubeDimensions: [{ name: 'dim1' }],
2243
+ materializations: [
2244
+ {
2245
+ name: 'druid_cube',
2246
+ strategy: 'incremental_time',
2247
+ schedule: '0 6 * * *',
2248
+ config: {
2249
+ lookback_window: '1 DAY',
2250
+ druid_datasource: 'ds1',
2251
+ preagg_tables: ['table1'],
2252
+ workflow_urls: ['http://workflow.url'],
2253
+ timestamp_column: 'ts',
2254
+ timestamp_format: 'yyyy-MM-dd',
2255
+ },
2256
+ },
2257
+ ],
2258
+ },
2259
+ },
2260
+ ],
2261
+ },
2262
+ }),
2263
+ );
2264
+
2265
+ const result = await DataJunctionAPI.cubeForPlanner('default.cube1');
2266
+ expect(result).toEqual({
2267
+ name: 'default.cube1',
2268
+ display_name: 'Cube 1',
2269
+ cube_node_metrics: ['metric1', 'metric2'],
2270
+ cube_node_dimensions: ['dim1'],
2271
+ cubeMaterialization: {
2272
+ strategy: 'incremental_time',
2273
+ schedule: '0 6 * * *',
2274
+ lookbackWindow: '1 DAY',
2275
+ druidDatasource: 'ds1',
2276
+ preaggTables: ['table1'],
2277
+ workflowUrls: ['http://workflow.url'],
2278
+ timestampColumn: 'ts',
2279
+ timestampFormat: 'yyyy-MM-dd',
2280
+ },
2281
+ });
2282
+ });
2283
+
2284
+ it('returns null for cubeForPlanner when cube not found', async () => {
2285
+ fetch.mockResponseOnce(
2286
+ JSON.stringify({
2287
+ data: { findNodes: [] },
2288
+ }),
2289
+ );
2290
+
2291
+ const result = await DataJunctionAPI.cubeForPlanner('nonexistent');
2292
+ expect(result).toBeNull();
2293
+ });
2294
+
2295
+ it('handles cubeForPlanner without druid materialization', async () => {
2296
+ fetch.mockResponseOnce(
2297
+ JSON.stringify({
2298
+ data: {
2299
+ findNodes: [
2300
+ {
2301
+ name: 'default.cube1',
2302
+ current: {
2303
+ displayName: 'Cube 1',
2304
+ cubeMetrics: [{ name: 'metric1' }],
2305
+ cubeDimensions: [],
2306
+ materializations: [],
2307
+ },
2308
+ },
2309
+ ],
2310
+ },
2311
+ }),
2312
+ );
2313
+
2314
+ const result = await DataJunctionAPI.cubeForPlanner('default.cube1');
2315
+ expect(result.cubeMaterialization).toBeNull();
2316
+ });
2317
+
2318
+ it('handles cubeForPlanner error gracefully', async () => {
2319
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
2320
+ fetch.mockRejectOnce(new Error('Network error'));
2321
+
2322
+ const result = await DataJunctionAPI.cubeForPlanner('default.cube1');
2323
+ expect(result).toBeNull();
2324
+ consoleSpy.mockRestore();
2325
+ });
2326
+
2327
+ // Test getNodesByNames (lines 460-493)
2328
+ it('calls getNodesByNames correctly', async () => {
2329
+ fetch.mockResponseOnce(
2330
+ JSON.stringify({
2331
+ data: {
2332
+ findNodes: [
2333
+ {
2334
+ name: 'default.node1',
2335
+ type: 'METRIC',
2336
+ current: {
2337
+ displayName: 'Node 1',
2338
+ status: 'VALID',
2339
+ mode: 'PUBLISHED',
2340
+ },
2341
+ },
2342
+ ],
2343
+ },
2344
+ }),
2345
+ );
2346
+
2347
+ const result = await DataJunctionAPI.getNodesByNames(['default.node1']);
2348
+ expect(result).toHaveLength(1);
2349
+ expect(result[0].name).toBe('default.node1');
2350
+ });
2351
+
2352
+ it('returns empty array for getNodesByNames with empty input', async () => {
2353
+ const result = await DataJunctionAPI.getNodesByNames([]);
2354
+ expect(result).toEqual([]);
2355
+ });
2356
+
2357
+ it('returns empty array for getNodesByNames with null input', async () => {
2358
+ const result = await DataJunctionAPI.getNodesByNames(null);
2359
+ expect(result).toEqual([]);
2360
+ });
2361
+
2362
+ // Test getNodeColumnsWithPartitions (lines 880-926)
2363
+ it('calls getNodeColumnsWithPartitions correctly', async () => {
2364
+ fetch.mockResponseOnce(
2365
+ JSON.stringify({
2366
+ data: {
2367
+ findNodes: [
2368
+ {
2369
+ name: 'default.node1',
2370
+ current: {
2371
+ columns: [
2372
+ {
2373
+ name: 'id',
2374
+ type: 'int',
2375
+ partition: null,
2376
+ },
2377
+ {
2378
+ name: 'date_col',
2379
+ type: 'date',
2380
+ partition: {
2381
+ type_: 'TEMPORAL',
2382
+ format: 'yyyyMMdd',
2383
+ granularity: 'day',
2384
+ },
2385
+ },
2386
+ ],
2387
+ },
2388
+ },
2389
+ ],
2390
+ },
2391
+ }),
2392
+ );
2393
+
2394
+ const result = await DataJunctionAPI.getNodeColumnsWithPartitions(
2395
+ 'default.node1',
2396
+ );
2397
+ expect(result.columns).toHaveLength(2);
2398
+ expect(result.temporalPartitions).toHaveLength(1);
2399
+ expect(result.temporalPartitions[0].name).toBe('date_col');
2400
+ });
2401
+
2402
+ it('returns empty for getNodeColumnsWithPartitions when node not found', async () => {
2403
+ fetch.mockResponseOnce(
2404
+ JSON.stringify({
2405
+ data: { findNodes: [] },
2406
+ }),
2407
+ );
2408
+
2409
+ const result = await DataJunctionAPI.getNodeColumnsWithPartitions(
2410
+ 'nonexistent',
2411
+ );
2412
+ expect(result).toEqual({ columns: [], temporalPartitions: [] });
2413
+ });
2414
+
2415
+ it('handles getNodeColumnsWithPartitions error gracefully', async () => {
2416
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
2417
+ fetch.mockRejectOnce(new Error('Network error'));
2418
+
2419
+ const result = await DataJunctionAPI.getNodeColumnsWithPartitions(
2420
+ 'default.node1',
2421
+ );
2422
+ expect(result).toEqual({ columns: [], temporalPartitions: [] });
2423
+ consoleSpy.mockRestore();
2424
+ });
2425
+
2426
+ // Test measuresV3 (lines 1085-1103)
2427
+ it('calls measuresV3 correctly', async () => {
2428
+ fetch.mockResponseOnce(JSON.stringify({ preaggs: [] }));
2429
+ await DataJunctionAPI.measuresV3(['metric1'], ['dim1'], 'filter=value');
2430
+ expect(fetch).toHaveBeenCalledWith(
2431
+ expect.stringContaining('/sql/measures/v3/?'),
2432
+ expect.objectContaining({ credentials: 'include' }),
2433
+ );
2434
+ const url = fetch.mock.calls[0][0];
2435
+ expect(url).toContain('metrics=metric1');
2436
+ expect(url).toContain('dimensions=dim1');
2437
+ expect(url).toContain('filters=filter');
2438
+ });
2439
+
2440
+ it('calls measuresV3 without filters', async () => {
2441
+ fetch.mockResponseOnce(JSON.stringify({ preaggs: [] }));
2442
+ await DataJunctionAPI.measuresV3(['metric1'], ['dim1']);
2443
+ const url = fetch.mock.calls[0][0];
2444
+ expect(url).not.toContain('filters=');
2445
+ });
2446
+
2447
+ // Test metricsV3 (lines 1106-1124)
2448
+ it('calls metricsV3 correctly', async () => {
2449
+ fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
2450
+ await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], 'filter=value');
2451
+ expect(fetch).toHaveBeenCalledWith(
2452
+ expect.stringContaining('/sql/metrics/v3/?'),
2453
+ expect.objectContaining({ credentials: 'include' }),
2454
+ );
2455
+ const url = fetch.mock.calls[0][0];
2456
+ expect(url).toContain('metrics=metric1');
2457
+ expect(url).toContain('dimensions=dim1');
2458
+ expect(url).toContain('filters=filter');
2459
+ });
2460
+
2461
+ it('calls metricsV3 without filters', async () => {
2462
+ fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
2463
+ await DataJunctionAPI.metricsV3(['metric1'], ['dim1']);
2464
+ const url = fetch.mock.calls[0][0];
2465
+ expect(url).not.toContain('filters=');
2466
+ });
2467
+
2468
+ // Test materializeCubeV2 (lines 1649-1671)
2469
+ it('calls materializeCubeV2 correctly', async () => {
2470
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
2471
+ const result = await DataJunctionAPI.materializeCubeV2(
2472
+ 'default.cube1',
2473
+ '0 6 * * *',
2474
+ 'incremental_time',
2475
+ '1 DAY',
2476
+ true,
2477
+ );
2478
+ expect(fetch).toHaveBeenCalledWith(
2479
+ 'http://localhost:8000/cubes/default.cube1/materialize',
2480
+ expect.objectContaining({
2481
+ method: 'POST',
2482
+ body: expect.stringContaining('schedule'),
2483
+ }),
2484
+ );
2485
+ expect(result).toEqual({ status: 200, json: { message: 'Success' } });
2486
+ });
2487
+
2488
+ // Test listPreaggs (lines 1845-1858)
2489
+ it('calls listPreaggs correctly', async () => {
2490
+ fetch.mockResponseOnce(JSON.stringify([{ id: 1, name: 'preagg1' }]));
2491
+ const result = await DataJunctionAPI.listPreaggs({ node_name: 'node1' });
2492
+ expect(fetch).toHaveBeenCalledWith(
2493
+ expect.stringContaining('/preaggs/?node_name=node1'),
2494
+ expect.objectContaining({ credentials: 'include' }),
2495
+ );
2496
+ expect(result).toHaveLength(1);
2497
+ });
2498
+
2499
+ // Test planPreaggs (lines 1861-1896)
2500
+ it('calls planPreaggs correctly', async () => {
2501
+ fetch.mockResponseOnce(JSON.stringify({ preaggs: [] }));
2502
+ const result = await DataJunctionAPI.planPreaggs(
2503
+ ['metric1'],
2504
+ ['dim1'],
2505
+ 'full',
2506
+ '0 6 * * *',
2507
+ '1 DAY',
2508
+ );
2509
+ expect(fetch).toHaveBeenCalledWith(
2510
+ 'http://localhost:8000/preaggs/plan',
2511
+ expect.objectContaining({
2512
+ method: 'POST',
2513
+ body: expect.stringContaining('metrics'),
2514
+ }),
2515
+ );
2516
+ });
2517
+
2518
+ it('handles planPreaggs error', async () => {
2519
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Error' }), {
2520
+ status: 400,
2521
+ });
2522
+ const result = await DataJunctionAPI.planPreaggs(['metric1'], ['dim1']);
2523
+ expect(result._error).toBe(true);
2524
+ expect(result._status).toBe(400);
2525
+ });
2526
+
2527
+ // Test getPreagg (lines 1899-1905)
2528
+ it('calls getPreagg correctly', async () => {
2529
+ fetch.mockResponseOnce(JSON.stringify({ id: 1, name: 'preagg1' }));
2530
+ const result = await DataJunctionAPI.getPreagg(1);
2531
+ expect(fetch).toHaveBeenCalledWith(
2532
+ 'http://localhost:8000/preaggs/1',
2533
+ expect.objectContaining({ credentials: 'include' }),
2534
+ );
2535
+ expect(result.id).toBe(1);
2536
+ });
2537
+
2538
+ // Test materializePreagg (lines 1908-1927)
2539
+ it('calls materializePreagg correctly', async () => {
2540
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Success' }));
2541
+ const result = await DataJunctionAPI.materializePreagg(1);
2542
+ expect(fetch).toHaveBeenCalledWith(
2543
+ 'http://localhost:8000/preaggs/1/materialize',
2544
+ expect.objectContaining({ method: 'POST' }),
2545
+ );
2546
+ });
2547
+
2548
+ it('handles materializePreagg error', async () => {
2549
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2550
+ status: 500,
2551
+ });
2552
+ const result = await DataJunctionAPI.materializePreagg(1);
2553
+ expect(result._error).toBe(true);
2554
+ });
2555
+
2556
+ // Test updatePreaggConfig (lines 1930-1959)
2557
+ it('calls updatePreaggConfig correctly', async () => {
2558
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Updated' }));
2559
+ const result = await DataJunctionAPI.updatePreaggConfig(
2560
+ 1,
2561
+ 'incremental_time',
2562
+ '0 6 * * *',
2563
+ '2 DAY',
2564
+ );
2565
+ expect(fetch).toHaveBeenCalledWith(
2566
+ 'http://localhost:8000/preaggs/1/config',
2567
+ expect.objectContaining({ method: 'PATCH' }),
2568
+ );
2569
+ });
2570
+
2571
+ it('handles updatePreaggConfig error', async () => {
2572
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2573
+ status: 400,
2574
+ });
2575
+ const result = await DataJunctionAPI.updatePreaggConfig(1, 'full');
2576
+ expect(result._error).toBe(true);
2577
+ });
2578
+
2579
+ // Test deactivatePreaggWorkflow (lines 1962-1978)
2580
+ it('calls deactivatePreaggWorkflow correctly', async () => {
2581
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Deactivated' }));
2582
+ const result = await DataJunctionAPI.deactivatePreaggWorkflow(1);
2583
+ expect(fetch).toHaveBeenCalledWith(
2584
+ 'http://localhost:8000/preaggs/1/workflow',
2585
+ expect.objectContaining({ method: 'DELETE' }),
2586
+ );
2587
+ });
2588
+
2589
+ it('handles deactivatePreaggWorkflow error', async () => {
2590
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2591
+ status: 500,
2592
+ });
2593
+ const result = await DataJunctionAPI.deactivatePreaggWorkflow(1);
2594
+ expect(result._error).toBe(true);
2595
+ });
2596
+
2597
+ // Test runPreaggBackfill (lines 1981-2005)
2598
+ it('calls runPreaggBackfill correctly', async () => {
2599
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Backfill started' }));
2600
+ const result = await DataJunctionAPI.runPreaggBackfill(
2601
+ 1,
2602
+ '2024-01-01',
2603
+ '2024-12-31',
2604
+ );
2605
+ expect(fetch).toHaveBeenCalledWith(
2606
+ 'http://localhost:8000/preaggs/1/backfill',
2607
+ expect.objectContaining({
2608
+ method: 'POST',
2609
+ body: expect.stringContaining('start_date'),
2610
+ }),
2611
+ );
2612
+ });
2613
+
2614
+ it('handles runPreaggBackfill error', async () => {
2615
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2616
+ status: 500,
2617
+ });
2618
+ const result = await DataJunctionAPI.runPreaggBackfill(1, '2024-01-01');
2619
+ expect(result._error).toBe(true);
2620
+ });
2621
+
2622
+ // Test getCubeDetails (lines 2008-2016)
2623
+ it('calls getCubeDetails correctly', async () => {
2624
+ fetch.mockResponseOnce(JSON.stringify({ name: 'cube1' }));
2625
+ const result = await DataJunctionAPI.getCubeDetails('default.cube1');
2626
+ expect(fetch).toHaveBeenCalledWith(
2627
+ 'http://localhost:8000/cubes/default.cube1',
2628
+ expect.objectContaining({ credentials: 'include' }),
2629
+ );
2630
+ expect(result.status).toBe(200);
2631
+ expect(result.json).toEqual({ name: 'cube1' });
2632
+ });
2633
+
2634
+ it('handles getCubeDetails error', async () => {
2635
+ fetch.mockResponseOnce('Not found', { status: 404 });
2636
+ const result = await DataJunctionAPI.getCubeDetails('nonexistent');
2637
+ expect(result.status).toBe(404);
2638
+ expect(result.json).toBeNull();
2639
+ });
2640
+
2641
+ // Test getCubeWorkflowUrls (lines 2019-2044)
2642
+ it('calls getCubeWorkflowUrls correctly', async () => {
2643
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
2644
+ fetch.mockResponseOnce(
2645
+ JSON.stringify({
2646
+ name: 'cube1',
2647
+ materializations: [
2648
+ {
2649
+ name: 'druid_cube',
2650
+ config: { workflow_urls: ['http://url1', 'http://url2'] },
2651
+ },
2652
+ ],
2653
+ }),
2654
+ );
2655
+
2656
+ const result = await DataJunctionAPI.getCubeWorkflowUrls('default.cube1');
2657
+ expect(result).toEqual(['http://url1', 'http://url2']);
2658
+ consoleSpy.mockRestore();
2659
+ });
2660
+
2661
+ it('returns empty array for getCubeWorkflowUrls when no materializations', async () => {
2662
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
2663
+ fetch.mockResponseOnce(
2664
+ JSON.stringify({ name: 'cube1', materializations: [] }),
2665
+ );
2666
+ const result = await DataJunctionAPI.getCubeWorkflowUrls('default.cube1');
2667
+ expect(result).toEqual([]);
2668
+ consoleSpy.mockRestore();
2669
+ });
2670
+
2671
+ // Test getCubeMaterialization (lines 2047-2070)
2672
+ it('calls getCubeMaterialization correctly', async () => {
2673
+ fetch.mockResponseOnce(
2674
+ JSON.stringify({
2675
+ materializations: [
2676
+ {
2677
+ id: 1,
2678
+ name: 'druid_cube',
2679
+ strategy: 'incremental_time',
2680
+ schedule: '0 6 * * *',
2681
+ lookback_window: '1 DAY',
2682
+ config: {
2683
+ druid_datasource: 'ds1',
2684
+ preagg_tables: ['table1'],
2685
+ workflow_urls: ['http://url'],
2686
+ timestamp_column: 'ts',
2687
+ timestamp_format: 'yyyy-MM-dd',
2688
+ },
2689
+ },
2690
+ ],
2691
+ }),
2692
+ );
2693
+
2694
+ const result = await DataJunctionAPI.getCubeMaterialization(
2695
+ 'default.cube1',
2696
+ );
2697
+ expect(result).toHaveProperty('strategy', 'incremental_time');
2698
+ expect(result).toHaveProperty('druidDatasource', 'ds1');
2699
+ });
2700
+
2701
+ it('returns null for getCubeMaterialization when no druid_cube', async () => {
2702
+ fetch.mockResponseOnce(JSON.stringify({ materializations: [] }));
2703
+ const result = await DataJunctionAPI.getCubeMaterialization(
2704
+ 'default.cube1',
2705
+ );
2706
+ expect(result).toBeNull();
2707
+ });
2708
+
2709
+ // Test refreshCubeWorkflow (lines 2073-2087)
2710
+ it('calls refreshCubeWorkflow correctly', async () => {
2711
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Refreshed' }));
2712
+ const result = await DataJunctionAPI.refreshCubeWorkflow(
2713
+ 'default.cube1',
2714
+ '0 6 * * *',
2715
+ 'incremental_time',
2716
+ '1 DAY',
2717
+ );
2718
+ expect(fetch).toHaveBeenCalledWith(
2719
+ 'http://localhost:8000/cubes/default.cube1/materialize',
2720
+ expect.objectContaining({ method: 'POST' }),
2721
+ );
2722
+ });
2723
+
2724
+ // Test deactivateCubeWorkflow (lines 2090-2109)
2725
+ it('calls deactivateCubeWorkflow correctly', async () => {
2726
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Deactivated' }));
2727
+ const result = await DataJunctionAPI.deactivateCubeWorkflow(
2728
+ 'default.cube1',
2729
+ );
2730
+ expect(fetch).toHaveBeenCalledWith(
2731
+ 'http://localhost:8000/cubes/default.cube1/materialize',
2732
+ expect.objectContaining({ method: 'DELETE' }),
2733
+ );
2734
+ expect(result.status).toBe(200);
2735
+ });
2736
+
2737
+ it('handles deactivateCubeWorkflow error', async () => {
2738
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2739
+ status: 500,
2740
+ });
2741
+ const result = await DataJunctionAPI.deactivateCubeWorkflow(
2742
+ 'default.cube1',
2743
+ );
2744
+ expect(result.status).toBe(500);
2745
+ expect(result.json.message).toBeDefined();
2746
+ });
2747
+
2748
+ // Test runCubeBackfill (lines 2112-2137)
2749
+ it('calls runCubeBackfill correctly', async () => {
2750
+ fetch.mockResponseOnce(JSON.stringify({ message: 'Backfill started' }));
2751
+ const result = await DataJunctionAPI.runCubeBackfill(
2752
+ 'default.cube1',
2753
+ '2024-01-01',
2754
+ '2024-12-31',
2755
+ );
2756
+ expect(fetch).toHaveBeenCalledWith(
2757
+ 'http://localhost:8000/cubes/default.cube1/backfill',
2758
+ expect.objectContaining({
2759
+ method: 'POST',
2760
+ body: expect.stringContaining('start_date'),
2761
+ }),
2762
+ );
2763
+ });
2764
+
2765
+ it('handles runCubeBackfill error', async () => {
2766
+ fetch.mockResponseOnce(JSON.stringify({ detail: 'Error' }), {
2767
+ status: 500,
2768
+ });
2769
+ const result = await DataJunctionAPI.runCubeBackfill(
2770
+ 'default.cube1',
2771
+ '2024-01-01',
2772
+ );
2773
+ expect(result._error).toBe(true);
2774
+ expect(result.message).toBeDefined();
2775
+ });
2194
2776
  });
@@ -362,6 +362,42 @@ export const mocks = {
362
362
  owners: [{ username: 'dj' }],
363
363
  },
364
364
 
365
+ // Derived metric - parent is another metric (no FROM clause in query)
366
+ mockGetDerivedMetricNode: {
367
+ name: 'default.revenue_per_order',
368
+ type: 'METRIC',
369
+ current: {
370
+ displayName: 'Default: Revenue Per Order',
371
+ description: 'Average revenue per order (derived metric)',
372
+ primaryKey: [],
373
+ query: 'SELECT default.total_revenue / default.num_orders',
374
+ parents: [
375
+ {
376
+ name: 'default.total_revenue',
377
+ type: 'METRIC',
378
+ },
379
+ {
380
+ name: 'default.num_orders',
381
+ type: 'METRIC',
382
+ },
383
+ ],
384
+ metricMetadata: {
385
+ direction: 'HIGHER_IS_BETTER',
386
+ unit: {
387
+ name: 'DOLLAR',
388
+ },
389
+ expression: null, // Derived metrics don't have a simple expression
390
+ significantDigits: 2,
391
+ incompatibleDruidFunctions: [],
392
+ },
393
+ requiredDimensions: [],
394
+ mode: 'PUBLISHED',
395
+ customMetadata: null,
396
+ },
397
+ tags: [],
398
+ owners: [{ username: 'dj' }],
399
+ },
400
+
365
401
  attributes: [
366
402
  {
367
403
  uniqueness_scope: [],
package/webpack.config.js CHANGED
@@ -24,16 +24,43 @@ module.exports = {
24
24
  output: {
25
25
  path: path.resolve(__dirname, './dist'),
26
26
  filename: 'static/[name].[fullhash].js',
27
+ chunkFilename: 'static/[name].[fullhash].chunk.js', // For lazy-loaded chunks
27
28
  library: 'datajunction-ui',
28
29
  libraryTarget: 'umd',
29
30
  globalObject: 'this',
30
31
  umdNamedDefine: true,
31
32
  publicPath: '/',
32
33
  },
34
+ optimization: {
35
+ splitChunks: {
36
+ chunks: 'all',
37
+ cacheGroups: {
38
+ // Split ReactFlow and dagre into a separate chunk since they're heavy
39
+ // and only used on the QueryPlannerPage
40
+ reactflow: {
41
+ test: /[\\/]node_modules[\\/](reactflow|@reactflow|dagre|d3-.*)[\\/]/,
42
+ name: 'reactflow-vendor',
43
+ chunks: 'async',
44
+ priority: 20,
45
+ },
46
+ // Common vendor chunks
47
+ vendor: {
48
+ test: /[\\/]node_modules[\\/]/,
49
+ name: 'vendors',
50
+ chunks: 'initial',
51
+ priority: 10,
52
+ },
53
+ },
54
+ },
55
+ },
33
56
  devServer: {
34
57
  historyApiFallback: {
35
58
  disableDotRule: true,
36
59
  },
60
+ host: '0.0.0.0', // Allow connections from outside container
61
+ port: 3000,
62
+ hot: true, // Enable hot module replacement
63
+ watchFiles: ['src/**/*'], // Watch for changes
37
64
  },
38
65
  resolve: {
39
66
  extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.scss'],