datajunction-ui 0.0.44 → 0.0.45
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 +1 -1
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +349 -1
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +46 -1
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +281 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +225 -100
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +193 -0
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +388 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +31 -51
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +720 -34
- package/src/app/pages/QueryPlannerPage/index.jsx +237 -117
- package/src/app/pages/QueryPlannerPage/styles.css +765 -15
- package/src/app/services/DJService.js +29 -6
- package/src/app/services/__tests__/DJService.test.jsx +163 -0
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
3
|
import { createRenderer } from 'react-test-renderer/shallow';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
5
|
|
|
@@ -160,4 +160,352 @@ describe('<NamespaceHeader />', () => {
|
|
|
160
160
|
expect(screen.getByText('namespace')).toBeInTheDocument();
|
|
161
161
|
expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
it('should open dropdown when clicking the git managed button', async () => {
|
|
165
|
+
const mockDjClient = {
|
|
166
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
167
|
+
total_deployments: 5,
|
|
168
|
+
primary_source: {
|
|
169
|
+
type: 'git',
|
|
170
|
+
repository: 'github.com/test/repo',
|
|
171
|
+
branch: 'main',
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
listDeployments: jest.fn().mockResolvedValue([
|
|
175
|
+
{
|
|
176
|
+
uuid: 'deploy-1',
|
|
177
|
+
status: 'success',
|
|
178
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
179
|
+
source: {
|
|
180
|
+
type: 'git',
|
|
181
|
+
repository: 'github.com/test/repo',
|
|
182
|
+
branch: 'main',
|
|
183
|
+
commit_sha: 'abc1234567890',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
]),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
render(
|
|
190
|
+
<MemoryRouter>
|
|
191
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
192
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
193
|
+
</DJClientContext.Provider>
|
|
194
|
+
</MemoryRouter>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Click the dropdown button
|
|
202
|
+
fireEvent.click(screen.getByText(/Git Managed/));
|
|
203
|
+
|
|
204
|
+
// Should show repository link in dropdown
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(screen.getByText(/github.com\/test\/repo/)).toBeInTheDocument();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should open dropdown when clicking local deploy button', async () => {
|
|
211
|
+
const mockDjClient = {
|
|
212
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
213
|
+
total_deployments: 2,
|
|
214
|
+
primary_source: {
|
|
215
|
+
type: 'local',
|
|
216
|
+
hostname: 'localhost',
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
listDeployments: jest.fn().mockResolvedValue([
|
|
220
|
+
{
|
|
221
|
+
uuid: 'deploy-1',
|
|
222
|
+
status: 'success',
|
|
223
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
224
|
+
created_by: 'testuser',
|
|
225
|
+
source: {
|
|
226
|
+
type: 'local',
|
|
227
|
+
hostname: 'localhost',
|
|
228
|
+
reason: 'testing',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
]),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
render(
|
|
235
|
+
<MemoryRouter>
|
|
236
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
237
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
238
|
+
</DJClientContext.Provider>
|
|
239
|
+
</MemoryRouter>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
expect(screen.getByText(/Local Deploy/)).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Click the dropdown button
|
|
247
|
+
fireEvent.click(screen.getByText(/Local Deploy/));
|
|
248
|
+
|
|
249
|
+
// Should show local deploy info in dropdown
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(screen.getByText(/Local deploys by testuser/)).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should show recent deployments list with git source', async () => {
|
|
256
|
+
const mockDjClient = {
|
|
257
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
258
|
+
total_deployments: 3,
|
|
259
|
+
primary_source: {
|
|
260
|
+
type: 'git',
|
|
261
|
+
repository: 'github.com/test/repo',
|
|
262
|
+
branch: 'main',
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
listDeployments: jest.fn().mockResolvedValue([
|
|
266
|
+
{
|
|
267
|
+
uuid: 'deploy-1',
|
|
268
|
+
status: 'success',
|
|
269
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
270
|
+
source: {
|
|
271
|
+
type: 'git',
|
|
272
|
+
repository: 'github.com/test/repo',
|
|
273
|
+
branch: 'feature-branch',
|
|
274
|
+
commit_sha: 'abc1234567890',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
uuid: 'deploy-2',
|
|
279
|
+
status: 'failed',
|
|
280
|
+
created_at: '2024-01-14T10:00:00Z',
|
|
281
|
+
source: {
|
|
282
|
+
type: 'git',
|
|
283
|
+
repository: 'github.com/test/repo',
|
|
284
|
+
branch: 'main',
|
|
285
|
+
commit_sha: 'def4567890123',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
]),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
render(
|
|
292
|
+
<MemoryRouter>
|
|
293
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
294
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
295
|
+
</DJClientContext.Provider>
|
|
296
|
+
</MemoryRouter>,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
fireEvent.click(screen.getByText(/Git Managed/));
|
|
304
|
+
|
|
305
|
+
// Should show branch names in deployment list
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(screen.getByText(/feature-branch/)).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Should show short commit SHA
|
|
311
|
+
expect(screen.getByText(/abc1234/)).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should show local deployments with reason', async () => {
|
|
315
|
+
const mockDjClient = {
|
|
316
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
317
|
+
total_deployments: 2,
|
|
318
|
+
primary_source: {
|
|
319
|
+
type: 'local',
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
listDeployments: jest.fn().mockResolvedValue([
|
|
323
|
+
{
|
|
324
|
+
uuid: 'deploy-1',
|
|
325
|
+
status: 'success',
|
|
326
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
327
|
+
source: {
|
|
328
|
+
type: 'local',
|
|
329
|
+
reason: 'hotfix deployment',
|
|
330
|
+
hostname: 'dev-machine',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
]),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
render(
|
|
337
|
+
<MemoryRouter>
|
|
338
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
339
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
340
|
+
</DJClientContext.Provider>
|
|
341
|
+
</MemoryRouter>,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await waitFor(() => {
|
|
345
|
+
expect(screen.getByText(/Local Deploy/)).toBeInTheDocument();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
fireEvent.click(screen.getByText(/Local Deploy/));
|
|
349
|
+
|
|
350
|
+
// Should show reason in deployment list
|
|
351
|
+
await waitFor(() => {
|
|
352
|
+
expect(screen.getByText(/hotfix deployment/)).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should close dropdown when clicking outside', async () => {
|
|
357
|
+
const mockDjClient = {
|
|
358
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
359
|
+
total_deployments: 5,
|
|
360
|
+
primary_source: {
|
|
361
|
+
type: 'git',
|
|
362
|
+
repository: 'github.com/test/repo',
|
|
363
|
+
branch: 'main',
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
render(
|
|
370
|
+
<MemoryRouter>
|
|
371
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
372
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
373
|
+
</DJClientContext.Provider>
|
|
374
|
+
</MemoryRouter>,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Open dropdown
|
|
382
|
+
fireEvent.click(screen.getByText(/Git Managed/));
|
|
383
|
+
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(screen.getByText(/github.com\/test\/repo/)).toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Click outside (on the breadcrumb)
|
|
389
|
+
fireEvent.mouseDown(document.body);
|
|
390
|
+
|
|
391
|
+
// Dropdown should close
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
expect(
|
|
394
|
+
screen.queryByText(/github.com\/test\/repo.*\(main\)/),
|
|
395
|
+
).not.toBeInTheDocument();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should toggle dropdown arrow indicator', async () => {
|
|
400
|
+
const mockDjClient = {
|
|
401
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
402
|
+
total_deployments: 5,
|
|
403
|
+
primary_source: {
|
|
404
|
+
type: 'git',
|
|
405
|
+
repository: 'github.com/test/repo',
|
|
406
|
+
branch: 'main',
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
render(
|
|
413
|
+
<MemoryRouter>
|
|
414
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
415
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
416
|
+
</DJClientContext.Provider>
|
|
417
|
+
</MemoryRouter>,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
await waitFor(() => {
|
|
421
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Initially shows down arrow
|
|
425
|
+
expect(screen.getByText('▼')).toBeInTheDocument();
|
|
426
|
+
|
|
427
|
+
// Click to open
|
|
428
|
+
fireEvent.click(screen.getByText(/Git Managed/));
|
|
429
|
+
|
|
430
|
+
// Should show up arrow when open
|
|
431
|
+
await waitFor(() => {
|
|
432
|
+
expect(screen.getByText('▲')).toBeInTheDocument();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should handle git repository URL with https prefix', async () => {
|
|
437
|
+
const mockDjClient = {
|
|
438
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
439
|
+
total_deployments: 1,
|
|
440
|
+
primary_source: {
|
|
441
|
+
type: 'git',
|
|
442
|
+
repository: 'https://github.com/test/repo',
|
|
443
|
+
branch: 'main',
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
render(
|
|
450
|
+
<MemoryRouter>
|
|
451
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
452
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
453
|
+
</DJClientContext.Provider>
|
|
454
|
+
</MemoryRouter>,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
await waitFor(() => {
|
|
458
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
fireEvent.click(screen.getByText(/Git Managed/));
|
|
462
|
+
|
|
463
|
+
await waitFor(() => {
|
|
464
|
+
// Find link by its text content (repository URL)
|
|
465
|
+
const link = screen.getByRole('link', {
|
|
466
|
+
name: /github\.com\/test\/repo/,
|
|
467
|
+
});
|
|
468
|
+
expect(link).toHaveAttribute('href', 'https://github.com/test/repo');
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should render adhoc deployment label when no created_by', async () => {
|
|
473
|
+
const mockDjClient = {
|
|
474
|
+
namespaceSources: jest.fn().mockResolvedValue({
|
|
475
|
+
total_deployments: 1,
|
|
476
|
+
primary_source: {
|
|
477
|
+
type: 'local',
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
listDeployments: jest.fn().mockResolvedValue([
|
|
481
|
+
{
|
|
482
|
+
uuid: 'deploy-1',
|
|
483
|
+
status: 'success',
|
|
484
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
485
|
+
created_by: null,
|
|
486
|
+
source: {
|
|
487
|
+
type: 'local',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
]),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
render(
|
|
494
|
+
<MemoryRouter>
|
|
495
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
496
|
+
<NamespaceHeader namespace="test.namespace" />
|
|
497
|
+
</DJClientContext.Provider>
|
|
498
|
+
</MemoryRouter>,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
await waitFor(() => {
|
|
502
|
+
expect(screen.getByText(/Local Deploy/)).toBeInTheDocument();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
fireEvent.click(screen.getByText(/Local Deploy/));
|
|
506
|
+
|
|
507
|
+
await waitFor(() => {
|
|
508
|
+
expect(screen.getByText(/Local\/adhoc deployments/)).toBeInTheDocument();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
163
511
|
});
|
|
@@ -292,6 +292,7 @@ export function QueryOverviewPanel({
|
|
|
292
292
|
onClearWorkflowUrls,
|
|
293
293
|
loadedCubeName = null, // Existing cube name if loaded from preset
|
|
294
294
|
cubeMaterialization = null, // Full cube materialization info {schedule, strategy, lookbackWindow, ...}
|
|
295
|
+
cubeAvailability = null, // Cube availability info for data freshness
|
|
295
296
|
onUpdateCubeConfig,
|
|
296
297
|
onRefreshCubeWorkflow,
|
|
297
298
|
onRunCubeBackfill,
|
|
@@ -578,6 +579,9 @@ export function QueryOverviewPanel({
|
|
|
578
579
|
const grainGroups = measuresResult.grain_groups || [];
|
|
579
580
|
const metricFormulas = measuresResult.metric_formulas || [];
|
|
580
581
|
const sql = metricsResult.sql || '';
|
|
582
|
+
const dialect = metricsResult.dialect || null;
|
|
583
|
+
const cubeName = metricsResult.cube_name || null;
|
|
584
|
+
const isFastQuery = !!cubeName; // Fast if using materialized cube
|
|
581
585
|
|
|
582
586
|
// Determine if materialization is already configured (has active workflows)
|
|
583
587
|
const isMaterialized =
|
|
@@ -609,11 +613,29 @@ export function QueryOverviewPanel({
|
|
|
609
613
|
{/* Header */}
|
|
610
614
|
<div className="details-header">
|
|
611
615
|
<h2 className="details-title">Query Plan</h2>
|
|
612
|
-
<p className="details-
|
|
616
|
+
<p className="details-info-row">
|
|
613
617
|
{selectedMetrics.length} metric
|
|
614
618
|
{selectedMetrics.length !== 1 ? 's' : ''} ×{' '}
|
|
615
619
|
{selectedDimensions.length} dimension
|
|
616
620
|
{selectedDimensions.length !== 1 ? 's' : ''}
|
|
621
|
+
{isFastQuery && (
|
|
622
|
+
<>
|
|
623
|
+
{' · '}
|
|
624
|
+
<span className="info-materialized">
|
|
625
|
+
<span style={{ fontFamily: 'sans-serif' }}>⚡</span>{' '}
|
|
626
|
+
Materialized cube available
|
|
627
|
+
</span>
|
|
628
|
+
{cubeAvailability?.validThroughTs && (
|
|
629
|
+
<>
|
|
630
|
+
{' '}
|
|
631
|
+
· Valid thru{' '}
|
|
632
|
+
{new Date(
|
|
633
|
+
cubeAvailability.validThroughTs,
|
|
634
|
+
).toLocaleDateString()}
|
|
635
|
+
</>
|
|
636
|
+
)}
|
|
637
|
+
</>
|
|
638
|
+
)}
|
|
617
639
|
</p>
|
|
618
640
|
</div>
|
|
619
641
|
|
|
@@ -2198,6 +2220,29 @@ export function QueryOverviewPanel({
|
|
|
2198
2220
|
<span className="section-icon">⌘</span>
|
|
2199
2221
|
Generated SQL
|
|
2200
2222
|
</h3>
|
|
2223
|
+
<span className="sql-info-inline">
|
|
2224
|
+
{sqlViewMode === 'optimized' && isFastQuery ? (
|
|
2225
|
+
<>
|
|
2226
|
+
<span className="info-materialized">
|
|
2227
|
+
<span style={{ fontFamily: 'sans-serif' }}>⚡</span> Using
|
|
2228
|
+
materialized cube
|
|
2229
|
+
</span>
|
|
2230
|
+
{cubeAvailability?.validThroughTs && (
|
|
2231
|
+
<>
|
|
2232
|
+
{' · Valid thru '}
|
|
2233
|
+
{new Date(
|
|
2234
|
+
cubeAvailability.validThroughTs,
|
|
2235
|
+
).toLocaleDateString()}
|
|
2236
|
+
</>
|
|
2237
|
+
)}
|
|
2238
|
+
</>
|
|
2239
|
+
) : sqlViewMode === 'raw' ? (
|
|
2240
|
+
<span className="info-base-tables">
|
|
2241
|
+
<span style={{ fontFamily: 'sans-serif' }}>⚠️</span> Using
|
|
2242
|
+
base tables
|
|
2243
|
+
</span>
|
|
2244
|
+
) : null}
|
|
2245
|
+
</span>
|
|
2201
2246
|
<div className="sql-view-toggle">
|
|
2202
2247
|
<button
|
|
2203
2248
|
className={`sql-toggle-btn ${
|