datajunction-ui 0.0.26 → 0.0.27
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 +3 -3
- 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 -6
- 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
|
@@ -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
|
});
|
package/src/mocks/mockNodes.jsx
CHANGED
|
@@ -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'],
|