datajunction-ui 0.0.98 → 0.0.100
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/pages/QueryPlannerPage/ResultsView.jsx +55 -5
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +676 -356
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +151 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +9 -9
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +101 -3
- package/src/app/pages/QueryPlannerPage/index.jsx +118 -11
- package/src/app/pages/QueryPlannerPage/styles.css +232 -14
- package/src/app/pages/Root/__tests__/index.test.jsx +1 -1
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/services/DJService.js +27 -0
|
@@ -710,5 +710,156 @@ describe('ResultsView', () => {
|
|
|
710
710
|
expect(link).toBeInTheDocument();
|
|
711
711
|
expect(link).toHaveAttribute('href', 'https://example.com/query/123');
|
|
712
712
|
});
|
|
713
|
+
|
|
714
|
+
// ── Pivoted chart paths (buildPivotedData + ChartView branches) ──────────
|
|
715
|
+
|
|
716
|
+
it('renders pivoted line chart for time + 1 categorical + 1 metric (groupByCol path)', () => {
|
|
717
|
+
// detectChartConfig line 84-90: timeCols + nonTimeCatCols.length === 1
|
|
718
|
+
// buildPivotedData lines 131-178, useMemo lines 505-512, ChartView line 353
|
|
719
|
+
const pivotedLineResults = {
|
|
720
|
+
results: [
|
|
721
|
+
{
|
|
722
|
+
columns: [
|
|
723
|
+
{ name: 'date', type: 'DATE' },
|
|
724
|
+
{ name: 'country', type: 'STRING' },
|
|
725
|
+
{ name: 'revenue', type: 'FLOAT' },
|
|
726
|
+
],
|
|
727
|
+
rows: [
|
|
728
|
+
['2024-01-01', 'US', 1000],
|
|
729
|
+
['2024-01-01', 'UK', 500],
|
|
730
|
+
['2024-01-02', 'US', 1200],
|
|
731
|
+
['2024-01-02', null, 300], // null group value → '(null)'
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
render(<ResultsView {...defaultProps} results={pivotedLineResults} />);
|
|
738
|
+
fireEvent.click(screen.getByText('Chart'));
|
|
739
|
+
|
|
740
|
+
expect(screen.getByTestId('LineChart')).toBeInTheDocument();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('renders pivoted multi-metric small multiples (time + 1 cat + 2 metrics)', () => {
|
|
744
|
+
// ChartView lines 331-334: pivotedByMetric.length > 1
|
|
745
|
+
const pivotedMultiMetricResults = {
|
|
746
|
+
results: [
|
|
747
|
+
{
|
|
748
|
+
columns: [
|
|
749
|
+
{ name: 'date', type: 'DATE' },
|
|
750
|
+
{ name: 'country', type: 'STRING' },
|
|
751
|
+
{ name: 'revenue', type: 'FLOAT' },
|
|
752
|
+
{ name: 'orders', type: 'FLOAT' },
|
|
753
|
+
],
|
|
754
|
+
rows: [
|
|
755
|
+
['2024-01-01', 'US', 1000, 10],
|
|
756
|
+
['2024-01-01', 'UK', 500, 5],
|
|
757
|
+
['2024-01-02', 'US', 1200, 12],
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
render(
|
|
764
|
+
<ResultsView {...defaultProps} results={pivotedMultiMetricResults} />,
|
|
765
|
+
);
|
|
766
|
+
fireEvent.click(screen.getByText('Chart'));
|
|
767
|
+
|
|
768
|
+
// pivotedByMetric.length === 2 → small-multiples wrapper
|
|
769
|
+
expect(document.querySelector('.small-multiples')).toBeInTheDocument();
|
|
770
|
+
const labels = document.querySelectorAll('.small-multiple-label');
|
|
771
|
+
expect(labels.length).toBe(2);
|
|
772
|
+
expect(labels[0]).toHaveTextContent('revenue');
|
|
773
|
+
expect(labels[1]).toHaveTextContent('orders');
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('renders grouped bar chart for 2 categorical columns (lines 101-107)', () => {
|
|
777
|
+
// detectChartConfig: nonTimeCatCols.length === 2 → bar with groupByCol
|
|
778
|
+
const groupedBarResults = {
|
|
779
|
+
results: [
|
|
780
|
+
{
|
|
781
|
+
columns: [
|
|
782
|
+
{ name: 'region', type: 'STRING' },
|
|
783
|
+
{ name: 'category', type: 'STRING' },
|
|
784
|
+
{ name: 'revenue', type: 'FLOAT' },
|
|
785
|
+
],
|
|
786
|
+
rows: [
|
|
787
|
+
['North', 'A', 1000],
|
|
788
|
+
['South', 'A', 800],
|
|
789
|
+
['North', 'B', 600],
|
|
790
|
+
['South', null, 400], // null group value
|
|
791
|
+
],
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
render(<ResultsView {...defaultProps} results={groupedBarResults} />);
|
|
797
|
+
fireEvent.click(screen.getByText('Chart'));
|
|
798
|
+
|
|
799
|
+
expect(screen.getByTestId('BarChart')).toBeInTheDocument();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('renders bar chart for 3+ categorical columns falling back to first as x-axis (lines 108-110)', () => {
|
|
803
|
+
const threeCatResults = {
|
|
804
|
+
results: [
|
|
805
|
+
{
|
|
806
|
+
columns: [
|
|
807
|
+
{ name: 'a', type: 'STRING' },
|
|
808
|
+
{ name: 'b', type: 'STRING' },
|
|
809
|
+
{ name: 'c', type: 'STRING' },
|
|
810
|
+
{ name: 'metric', type: 'FLOAT' },
|
|
811
|
+
],
|
|
812
|
+
rows: [
|
|
813
|
+
['x1', 'y1', 'z1', 100],
|
|
814
|
+
['x2', 'y2', 'z2', 200],
|
|
815
|
+
],
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
render(<ResultsView {...defaultProps} results={threeCatResults} />);
|
|
821
|
+
fireEvent.click(screen.getByText('Chart'));
|
|
822
|
+
|
|
823
|
+
expect(screen.getByTestId('BarChart')).toBeInTheDocument();
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('Pane Resize', () => {
|
|
828
|
+
it('handles SQL pane drag resize via horizontal-resizer', () => {
|
|
829
|
+
render(<ResultsView {...defaultProps} />);
|
|
830
|
+
|
|
831
|
+
const resizer = document.querySelector('.horizontal-resizer');
|
|
832
|
+
expect(resizer).toBeInTheDocument();
|
|
833
|
+
|
|
834
|
+
// Mousedown starts the drag
|
|
835
|
+
fireEvent.mouseDown(resizer, { clientY: 300 });
|
|
836
|
+
|
|
837
|
+
// Mousemove updates height (in jsdom offsetHeight is 0, so newHeight clamps to min 80)
|
|
838
|
+
fireEvent.mouseMove(document, { clientY: 350 });
|
|
839
|
+
|
|
840
|
+
// Inline style should be applied to sql-pane
|
|
841
|
+
const sqlPane = document.querySelector('.sql-pane');
|
|
842
|
+
expect(sqlPane.style.maxHeight).toBeTruthy();
|
|
843
|
+
|
|
844
|
+
// Mouseup removes event listeners
|
|
845
|
+
fireEvent.mouseUp(document);
|
|
846
|
+
|
|
847
|
+
// Further moves should not update (listeners removed)
|
|
848
|
+
const heightAfterUp = sqlPane.style.maxHeight;
|
|
849
|
+
fireEvent.mouseMove(document, { clientY: 500 });
|
|
850
|
+
expect(sqlPane.style.maxHeight).toBe(heightAfterUp);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('does not update height when no resultsPanesRef container', () => {
|
|
854
|
+
// Renders and immediately fires mousedown on resizer
|
|
855
|
+
render(<ResultsView {...defaultProps} />);
|
|
856
|
+
const resizer = document.querySelector('.horizontal-resizer');
|
|
857
|
+
// Should not throw even if container measures are 0
|
|
858
|
+
expect(() => {
|
|
859
|
+
fireEvent.mouseDown(resizer, { clientY: 100 });
|
|
860
|
+
fireEvent.mouseMove(document, { clientY: 200 });
|
|
861
|
+
fireEvent.mouseUp(document);
|
|
862
|
+
}).not.toThrow();
|
|
863
|
+
});
|
|
713
864
|
});
|
|
714
865
|
});
|
|
@@ -15,22 +15,22 @@ const mockDimensions = [
|
|
|
15
15
|
{
|
|
16
16
|
name: 'default.date_dim.dateint',
|
|
17
17
|
type: 'timestamp',
|
|
18
|
-
path: ['default.
|
|
18
|
+
path: ['default.date_dim'],
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: 'default.date_dim.month',
|
|
22
22
|
type: 'int',
|
|
23
|
-
path: ['default.
|
|
23
|
+
path: ['default.date_dim'],
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'default.date_dim.year',
|
|
27
27
|
type: 'int',
|
|
28
|
-
path: ['default.
|
|
28
|
+
path: ['default.date_dim'],
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'default.customer.country',
|
|
32
32
|
type: 'string',
|
|
33
|
-
path: ['default.
|
|
33
|
+
path: ['default.customer'],
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
|
|
@@ -313,8 +313,8 @@ describe('SelectionPanel', () => {
|
|
|
313
313
|
|
|
314
314
|
it('deduplicates dimensions with same name', () => {
|
|
315
315
|
const duplicateDimensions = [
|
|
316
|
-
{ name: 'default.date_dim.month', path: ['path1'
|
|
317
|
-
{ name: 'default.date_dim.month', path: ['short'
|
|
316
|
+
{ name: 'default.date_dim.month', path: ['path1'] },
|
|
317
|
+
{ name: 'default.date_dim.month', path: ['short'] },
|
|
318
318
|
];
|
|
319
319
|
render(
|
|
320
320
|
<SelectionPanel
|
|
@@ -702,7 +702,7 @@ describe('SelectionPanel', () => {
|
|
|
702
702
|
{
|
|
703
703
|
name: 'default.date_dim.dateint',
|
|
704
704
|
type: 'timestamp',
|
|
705
|
-
path: ['default.
|
|
705
|
+
path: ['default.date_dim.dateint'],
|
|
706
706
|
},
|
|
707
707
|
];
|
|
708
708
|
|
|
@@ -714,9 +714,9 @@ describe('SelectionPanel', () => {
|
|
|
714
714
|
/>,
|
|
715
715
|
);
|
|
716
716
|
|
|
717
|
-
//
|
|
717
|
+
// Display name (last 2 segments) appears in the dimension item
|
|
718
718
|
expect(
|
|
719
|
-
screen.getAllByText('
|
|
719
|
+
screen.getAllByText('date_dim.dateint').length,
|
|
720
720
|
).toBeGreaterThanOrEqual(1);
|
|
721
721
|
});
|
|
722
722
|
});
|
|
@@ -55,6 +55,7 @@ jest.mock('../MetricFlowGraph', () => ({
|
|
|
55
55
|
const mockDjClient = {
|
|
56
56
|
metrics: jest.fn(),
|
|
57
57
|
commonDimensions: jest.fn(),
|
|
58
|
+
commonMetrics: jest.fn(),
|
|
58
59
|
measuresV3: jest.fn(),
|
|
59
60
|
metricsV3: jest.fn(),
|
|
60
61
|
listCubesForPreset: jest.fn(),
|
|
@@ -89,7 +90,7 @@ const mockCommonDimensions = [
|
|
|
89
90
|
node_name: 'default.date_dim',
|
|
90
91
|
node_display_name: 'Date',
|
|
91
92
|
properties: [],
|
|
92
|
-
path: ['default.
|
|
93
|
+
path: ['default.date_dim'],
|
|
93
94
|
},
|
|
94
95
|
{
|
|
95
96
|
name: 'default.date_dim.month',
|
|
@@ -97,7 +98,7 @@ const mockCommonDimensions = [
|
|
|
97
98
|
node_name: 'default.date_dim',
|
|
98
99
|
node_display_name: 'Date',
|
|
99
100
|
properties: [],
|
|
100
|
-
path: ['default.
|
|
101
|
+
path: ['default.date_dim'],
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
name: 'default.hard_hat.country',
|
|
@@ -105,7 +106,7 @@ const mockCommonDimensions = [
|
|
|
105
106
|
node_name: 'default.hard_hat',
|
|
106
107
|
node_display_name: 'Hard Hat',
|
|
107
108
|
properties: [],
|
|
108
|
-
path: ['default.
|
|
109
|
+
path: ['default.hard_hat'],
|
|
109
110
|
},
|
|
110
111
|
];
|
|
111
112
|
|
|
@@ -192,6 +193,7 @@ describe('QueryPlannerPage', () => {
|
|
|
192
193
|
beforeEach(() => {
|
|
193
194
|
mockDjClient.metrics.mockResolvedValue(mockMetrics);
|
|
194
195
|
mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
|
|
196
|
+
mockDjClient.commonMetrics.mockResolvedValue([]);
|
|
195
197
|
mockDjClient.measuresV3.mockResolvedValue(mockMeasuresResult);
|
|
196
198
|
mockDjClient.metricsV3.mockResolvedValue(mockMetricsResult);
|
|
197
199
|
mockDjClient.listCubesForPreset.mockResolvedValue(mockCubes);
|
|
@@ -1425,4 +1427,100 @@ describe('QueryPlannerPage', () => {
|
|
|
1425
1427
|
).toBeInTheDocument();
|
|
1426
1428
|
});
|
|
1427
1429
|
});
|
|
1430
|
+
|
|
1431
|
+
describe('Pane Resize Handlers', () => {
|
|
1432
|
+
it('resizes selection panel when dragging left vertical-resizer', async () => {
|
|
1433
|
+
renderPage();
|
|
1434
|
+
await waitFor(() => expect(mockDjClient.metrics).toHaveBeenCalled());
|
|
1435
|
+
|
|
1436
|
+
const resizers = document.querySelectorAll('.vertical-resizer');
|
|
1437
|
+
// First resizer is between selection panel and main content
|
|
1438
|
+
const selectionResizer = resizers[0];
|
|
1439
|
+
expect(selectionResizer).toBeInTheDocument();
|
|
1440
|
+
|
|
1441
|
+
// Mousedown begins drag
|
|
1442
|
+
fireEvent.mouseDown(selectionResizer, { clientX: 200 });
|
|
1443
|
+
|
|
1444
|
+
// Move right → wider selection panel
|
|
1445
|
+
fireEvent.mouseMove(document, { clientX: 280 });
|
|
1446
|
+
|
|
1447
|
+
const selectionPane = document.querySelector('.planner-selection');
|
|
1448
|
+
// Inline width should be applied (clamped to min 200)
|
|
1449
|
+
expect(selectionPane.style.width).toBeTruthy();
|
|
1450
|
+
|
|
1451
|
+
// Mouseup ends drag
|
|
1452
|
+
fireEvent.mouseUp(document);
|
|
1453
|
+
|
|
1454
|
+
// Further moves should not update
|
|
1455
|
+
const widthAfterUp = selectionPane.style.width;
|
|
1456
|
+
fireEvent.mouseMove(document, { clientX: 600 });
|
|
1457
|
+
expect(selectionPane.style.width).toBe(widthAfterUp);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('resizes details panel when dragging right vertical-resizer', async () => {
|
|
1461
|
+
renderPage();
|
|
1462
|
+
await waitFor(() => expect(mockDjClient.metrics).toHaveBeenCalled());
|
|
1463
|
+
|
|
1464
|
+
const resizers = document.querySelectorAll('.vertical-resizer');
|
|
1465
|
+
// Second resizer is between graph and details panel (visible in non-results state)
|
|
1466
|
+
expect(resizers.length).toBeGreaterThanOrEqual(2);
|
|
1467
|
+
const detailsResizer = resizers[1];
|
|
1468
|
+
|
|
1469
|
+
// Mousedown begins drag
|
|
1470
|
+
fireEvent.mouseDown(detailsResizer, { clientX: 800 });
|
|
1471
|
+
|
|
1472
|
+
// Move left → wider details panel
|
|
1473
|
+
fireEvent.mouseMove(document, { clientX: 700 });
|
|
1474
|
+
|
|
1475
|
+
const detailsPane = document.querySelector('.planner-details');
|
|
1476
|
+
expect(detailsPane.style.width).toBeTruthy();
|
|
1477
|
+
|
|
1478
|
+
// Mouseup ends drag
|
|
1479
|
+
fireEvent.mouseUp(document);
|
|
1480
|
+
|
|
1481
|
+
const widthAfterUp = detailsPane.style.width;
|
|
1482
|
+
fireEvent.mouseMove(document, { clientX: 400 });
|
|
1483
|
+
expect(detailsPane.style.width).toBe(widthAfterUp);
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
describe('URL Init Edge Cases', () => {
|
|
1488
|
+
it('handles invalid cube data returned from cubeForPlanner (missing cube_node_metrics)', async () => {
|
|
1489
|
+
// cubeForPlanner returns data without cube_node_metrics array
|
|
1490
|
+
mockDjClient.cubeForPlanner.mockResolvedValue({ name: 'bad.cube' });
|
|
1491
|
+
|
|
1492
|
+
renderPage(['/query-planner?cube=bad.cube']);
|
|
1493
|
+
|
|
1494
|
+
await waitFor(() => {
|
|
1495
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalledWith('bad.cube');
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// Page should still render without crashing (invalid cube data logged, not applied)
|
|
1499
|
+
expect(screen.getByText('Load from Cube')).toBeInTheDocument();
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
it('handles cubeForPlanner throwing during URL init', async () => {
|
|
1503
|
+
mockDjClient.cubeForPlanner.mockRejectedValue(new Error('Network error'));
|
|
1504
|
+
|
|
1505
|
+
renderPage(['/query-planner?cube=bad.cube']);
|
|
1506
|
+
|
|
1507
|
+
await waitFor(() => {
|
|
1508
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalled();
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Page should still render gracefully
|
|
1512
|
+
expect(screen.getByText('Load from Cube')).toBeInTheDocument();
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
it('handles metrics API failure on mount gracefully', async () => {
|
|
1516
|
+
mockDjClient.metrics.mockRejectedValue(new Error('API down'));
|
|
1517
|
+
|
|
1518
|
+
// Should not throw
|
|
1519
|
+
renderPage();
|
|
1520
|
+
|
|
1521
|
+
await waitFor(() => {
|
|
1522
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
});
|
|
1428
1526
|
});
|
|
@@ -53,6 +53,9 @@ export function QueryPlannerPage() {
|
|
|
53
53
|
const pendingDimensionsFromUrl = useRef([]);
|
|
54
54
|
const pendingCubeFromUrl = useRef(null);
|
|
55
55
|
|
|
56
|
+
// Compatible metrics when dimensions are selected (phase 1: dimension-first flow)
|
|
57
|
+
const [compatibleMetrics, setCompatibleMetrics] = useState(null); // null = no filter active
|
|
58
|
+
|
|
56
59
|
// Results state
|
|
57
60
|
const [measuresResult, setMeasuresResult] = useState(null);
|
|
58
61
|
const [metricsResult, setMetricsResult] = useState(null);
|
|
@@ -81,6 +84,67 @@ export function QueryPlannerPage() {
|
|
|
81
84
|
// Node selection for details panel
|
|
82
85
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
83
86
|
|
|
87
|
+
// Resizable pane widths (px); null = use CSS default
|
|
88
|
+
const [selectionWidth, setSelectionWidth] = useState(null);
|
|
89
|
+
const [detailsWidth, setDetailsWidth] = useState(null);
|
|
90
|
+
const plannerLayoutRef = useRef(null);
|
|
91
|
+
|
|
92
|
+
const handleSelectionResizerMouseDown = useCallback(
|
|
93
|
+
e => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
const container = plannerLayoutRef.current;
|
|
96
|
+
if (!container) return;
|
|
97
|
+
const startX = e.clientX;
|
|
98
|
+
const startWidth = selectionWidth ?? container.offsetWidth * 0.2; // match CSS default 20%
|
|
99
|
+
|
|
100
|
+
const onMouseMove = moveEvent => {
|
|
101
|
+
const delta = moveEvent.clientX - startX;
|
|
102
|
+
const newWidth = Math.max(
|
|
103
|
+
200,
|
|
104
|
+
Math.min(container.offsetWidth * 0.5, startWidth + delta),
|
|
105
|
+
);
|
|
106
|
+
setSelectionWidth(newWidth);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onMouseUp = () => {
|
|
110
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
111
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
115
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
116
|
+
},
|
|
117
|
+
[selectionWidth],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleDetailsResizerMouseDown = useCallback(
|
|
121
|
+
e => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
const container = plannerLayoutRef.current;
|
|
124
|
+
if (!container) return;
|
|
125
|
+
const startX = e.clientX;
|
|
126
|
+
const startWidth = detailsWidth ?? container.offsetWidth * 0.4; // match CSS default 40%
|
|
127
|
+
|
|
128
|
+
const onMouseMove = moveEvent => {
|
|
129
|
+
const delta = startX - moveEvent.clientX;
|
|
130
|
+
const newWidth = Math.max(
|
|
131
|
+
280,
|
|
132
|
+
Math.min(container.offsetWidth * 0.7, startWidth + delta),
|
|
133
|
+
);
|
|
134
|
+
setDetailsWidth(newWidth);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const onMouseUp = () => {
|
|
138
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
139
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
143
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
144
|
+
},
|
|
145
|
+
[detailsWidth],
|
|
146
|
+
);
|
|
147
|
+
|
|
84
148
|
// Materialization state - map of grain_key -> pre-agg info
|
|
85
149
|
const [plannedPreaggs, setPlannedPreaggs] = useState({});
|
|
86
150
|
|
|
@@ -259,6 +323,33 @@ export function QueryPlannerPage() {
|
|
|
259
323
|
}
|
|
260
324
|
}, [commonDimensions, selectedDimensions]);
|
|
261
325
|
|
|
326
|
+
// Phase 1: When dimensions are selected, reactively find compatible metrics
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (selectedDimensions.length === 0) {
|
|
329
|
+
setCompatibleMetrics(null);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let cancelled = false;
|
|
333
|
+
djClient
|
|
334
|
+
.commonMetrics(selectedDimensions)
|
|
335
|
+
.then(result => {
|
|
336
|
+
if (cancelled) return;
|
|
337
|
+
// API returns array of node names (strings) or objects with a name field
|
|
338
|
+
if (Array.isArray(result)) {
|
|
339
|
+
const names = result.map(r => (typeof r === 'string' ? r : r.name));
|
|
340
|
+
setCompatibleMetrics(new Set(names));
|
|
341
|
+
} else {
|
|
342
|
+
setCompatibleMetrics(null);
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
.catch(() => {
|
|
346
|
+
if (!cancelled) setCompatibleMetrics(null);
|
|
347
|
+
});
|
|
348
|
+
return () => {
|
|
349
|
+
cancelled = true;
|
|
350
|
+
};
|
|
351
|
+
}, [selectedDimensions, djClient]);
|
|
352
|
+
|
|
262
353
|
// Fetch V3 measures and metrics SQL when selection, filters, or engine changes
|
|
263
354
|
useEffect(() => {
|
|
264
355
|
const fetchData = async () => {
|
|
@@ -1194,19 +1285,17 @@ export function QueryPlannerPage() {
|
|
|
1194
1285
|
|
|
1195
1286
|
return (
|
|
1196
1287
|
<div className="planner-page">
|
|
1197
|
-
{
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
<h1>Explore</h1>
|
|
1201
|
-
{/* <p>Explore metrics and dimensions and plan materializations</p> */}
|
|
1202
|
-
</div>
|
|
1203
|
-
{error && <div className="header-error">{error}</div>}
|
|
1204
|
-
</header>
|
|
1288
|
+
{error && (
|
|
1289
|
+
<div className="header-error planner-error-banner">{error}</div>
|
|
1290
|
+
)}
|
|
1205
1291
|
|
|
1206
1292
|
{/* Three-column layout */}
|
|
1207
|
-
<div className="planner-layout">
|
|
1293
|
+
<div className="planner-layout" ref={plannerLayoutRef}>
|
|
1208
1294
|
{/* Left: Selection Panel */}
|
|
1209
|
-
<aside
|
|
1295
|
+
<aside
|
|
1296
|
+
className="planner-selection"
|
|
1297
|
+
style={selectionWidth != null ? { width: selectionWidth } : undefined}
|
|
1298
|
+
>
|
|
1210
1299
|
<SelectionPanel
|
|
1211
1300
|
metrics={metrics}
|
|
1212
1301
|
selectedMetrics={selectedMetrics}
|
|
@@ -1228,9 +1317,17 @@ export function QueryPlannerPage() {
|
|
|
1228
1317
|
selectedMetrics.length > 0 && selectedDimensions.length > 0
|
|
1229
1318
|
}
|
|
1230
1319
|
queryLoading={queryLoading}
|
|
1320
|
+
compatibleMetrics={compatibleMetrics}
|
|
1231
1321
|
/>
|
|
1232
1322
|
</aside>
|
|
1233
1323
|
|
|
1324
|
+
{/* Vertical resizer between selection panel and main content */}
|
|
1325
|
+
<div
|
|
1326
|
+
className="vertical-resizer"
|
|
1327
|
+
onMouseDown={handleSelectionResizerMouseDown}
|
|
1328
|
+
title="Drag to resize"
|
|
1329
|
+
/>
|
|
1330
|
+
|
|
1234
1331
|
{/* Main Content Area - Either Results or Graph+Details */}
|
|
1235
1332
|
{showResults ? (
|
|
1236
1333
|
<ResultsView
|
|
@@ -1295,8 +1392,18 @@ export function QueryPlannerPage() {
|
|
|
1295
1392
|
)}
|
|
1296
1393
|
</main>
|
|
1297
1394
|
|
|
1395
|
+
{/* Vertical resizer between graph and details */}
|
|
1396
|
+
<div
|
|
1397
|
+
className="vertical-resizer"
|
|
1398
|
+
onMouseDown={handleDetailsResizerMouseDown}
|
|
1399
|
+
title="Drag to resize"
|
|
1400
|
+
/>
|
|
1401
|
+
|
|
1298
1402
|
{/* Right: Details Panel */}
|
|
1299
|
-
<aside
|
|
1403
|
+
<aside
|
|
1404
|
+
className="planner-details"
|
|
1405
|
+
style={detailsWidth != null ? { width: detailsWidth } : undefined}
|
|
1406
|
+
>
|
|
1300
1407
|
{selectedNode?.type === 'preagg' ||
|
|
1301
1408
|
selectedNode?.type === 'component' ? (
|
|
1302
1409
|
<PreAggDetailsPanel
|