datajunction-ui 0.0.44 → 0.0.46
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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
3
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
4
|
+
import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql';
|
|
5
|
+
|
|
6
|
+
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ResultsView - Displays query results with SQL and data table
|
|
10
|
+
* Layout: SQL in top 1/3, results in bottom 2/3
|
|
11
|
+
*/
|
|
12
|
+
export function ResultsView({
|
|
13
|
+
sql: sqlQuery,
|
|
14
|
+
results,
|
|
15
|
+
loading,
|
|
16
|
+
error,
|
|
17
|
+
elapsedTime,
|
|
18
|
+
onBackToPlan,
|
|
19
|
+
selectedMetrics,
|
|
20
|
+
selectedDimensions,
|
|
21
|
+
filters,
|
|
22
|
+
dialect,
|
|
23
|
+
cubeName,
|
|
24
|
+
availability,
|
|
25
|
+
}) {
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const [sortColumn, setSortColumn] = useState(null);
|
|
28
|
+
const [sortDirection, setSortDirection] = useState('asc');
|
|
29
|
+
|
|
30
|
+
const handleCopySql = useCallback(() => {
|
|
31
|
+
if (sqlQuery) {
|
|
32
|
+
navigator.clipboard.writeText(sqlQuery);
|
|
33
|
+
setCopied(true);
|
|
34
|
+
setTimeout(() => setCopied(false), 2000);
|
|
35
|
+
}
|
|
36
|
+
}, [sqlQuery]);
|
|
37
|
+
|
|
38
|
+
// Parse results data - handle new v3 format
|
|
39
|
+
const columns = results?.results?.[0]?.columns || [];
|
|
40
|
+
const rows = results?.results?.[0]?.rows || [];
|
|
41
|
+
const rowCount = rows.length;
|
|
42
|
+
|
|
43
|
+
// Handle column header click for sorting
|
|
44
|
+
const handleSort = useCallback(
|
|
45
|
+
columnIndex => {
|
|
46
|
+
if (sortColumn === columnIndex) {
|
|
47
|
+
setSortDirection(d => (d === 'asc' ? 'desc' : 'asc'));
|
|
48
|
+
} else {
|
|
49
|
+
setSortColumn(columnIndex);
|
|
50
|
+
setSortDirection('asc');
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[sortColumn],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Sort rows based on current sort state
|
|
57
|
+
const sortedRows = useMemo(() => {
|
|
58
|
+
if (sortColumn === null) return rows;
|
|
59
|
+
return [...rows].sort((a, b) => {
|
|
60
|
+
const aVal = a[sortColumn];
|
|
61
|
+
const bVal = b[sortColumn];
|
|
62
|
+
// Handle nulls - nulls go last
|
|
63
|
+
if (aVal === null && bVal === null) return 0;
|
|
64
|
+
if (aVal === null) return 1;
|
|
65
|
+
if (bVal === null) return -1;
|
|
66
|
+
// Compare values
|
|
67
|
+
let cmp;
|
|
68
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
69
|
+
cmp = aVal - bVal;
|
|
70
|
+
} else {
|
|
71
|
+
cmp = String(aVal).localeCompare(String(bVal));
|
|
72
|
+
}
|
|
73
|
+
return sortDirection === 'asc' ? cmp : -cmp;
|
|
74
|
+
});
|
|
75
|
+
}, [rows, sortColumn, sortDirection]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="results-view">
|
|
79
|
+
{/* Header */}
|
|
80
|
+
<div className="results-header">
|
|
81
|
+
<button className="back-to-plan-btn" onClick={onBackToPlan}>
|
|
82
|
+
<span className="back-arrow">←</span>
|
|
83
|
+
<span>Back to Plan</span>
|
|
84
|
+
</button>
|
|
85
|
+
<div className="results-summary">
|
|
86
|
+
{loading ? (
|
|
87
|
+
<span className="results-loading-text">Running query...</span>
|
|
88
|
+
) : error ? (
|
|
89
|
+
<span className="results-error-text">Query failed</span>
|
|
90
|
+
) : (
|
|
91
|
+
<>
|
|
92
|
+
<span className="results-count">
|
|
93
|
+
{rowCount.toLocaleString()} rows
|
|
94
|
+
</span>
|
|
95
|
+
{elapsedTime != null && (
|
|
96
|
+
<span className="results-time">{elapsedTime.toFixed(2)}s</span>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Two-pane layout: SQL (top 1/3) + Results (bottom 2/3) */}
|
|
104
|
+
<div className="results-panes">
|
|
105
|
+
{/* SQL Pane - always visible, top 1/3 */}
|
|
106
|
+
<div className="sql-pane">
|
|
107
|
+
<div className="sql-pane-header">
|
|
108
|
+
<span className="sql-pane-title">SQL Query</span>
|
|
109
|
+
{cubeName && (
|
|
110
|
+
<span
|
|
111
|
+
className="sql-pane-info"
|
|
112
|
+
title={
|
|
113
|
+
availability
|
|
114
|
+
? `Querying materialized dataset ${[
|
|
115
|
+
availability.catalog,
|
|
116
|
+
availability.schema_,
|
|
117
|
+
availability.table,
|
|
118
|
+
]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join('.')}, last refreshed for data through ${new Date(
|
|
121
|
+
availability.validThroughTs,
|
|
122
|
+
).toLocaleDateString()}`
|
|
123
|
+
: undefined
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
<span className="info-materialized">
|
|
127
|
+
<span style={{ fontFamily: 'sans-serif' }}>⚡</span> Using
|
|
128
|
+
materialized cube
|
|
129
|
+
</span>
|
|
130
|
+
{availability?.validThroughTs && (
|
|
131
|
+
<>
|
|
132
|
+
{' · Valid thru '}
|
|
133
|
+
{new Date(availability.validThroughTs).toLocaleDateString()}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
<button
|
|
139
|
+
className={`copy-btn ${copied ? 'copied' : ''}`}
|
|
140
|
+
onClick={handleCopySql}
|
|
141
|
+
disabled={!sqlQuery}
|
|
142
|
+
>
|
|
143
|
+
{copied ? '✓ Copied' : 'Copy'}
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="sql-pane-content">
|
|
147
|
+
{sqlQuery ? (
|
|
148
|
+
<SyntaxHighlighter
|
|
149
|
+
language="sql"
|
|
150
|
+
style={foundation}
|
|
151
|
+
wrapLongLines={true}
|
|
152
|
+
customStyle={{
|
|
153
|
+
margin: 0,
|
|
154
|
+
padding: '12px 16px',
|
|
155
|
+
background: '#f8fafc',
|
|
156
|
+
fontSize: '12px',
|
|
157
|
+
height: '100%',
|
|
158
|
+
overflow: 'auto',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{sqlQuery}
|
|
162
|
+
</SyntaxHighlighter>
|
|
163
|
+
) : (
|
|
164
|
+
<div className="sql-pane-empty">Generating SQL...</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Results Pane - bottom 2/3 */}
|
|
170
|
+
<div className="results-pane">
|
|
171
|
+
{loading ? (
|
|
172
|
+
<div className="results-loading">
|
|
173
|
+
<div className="loading-spinner large" />
|
|
174
|
+
<span>Executing query...</span>
|
|
175
|
+
<span className="loading-hint">
|
|
176
|
+
Querying {selectedMetrics.length} metric(s) with{' '}
|
|
177
|
+
{selectedDimensions.length} dimension(s)
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
) : error ? (
|
|
181
|
+
<div className="results-error">
|
|
182
|
+
<div className="error-icon">⚠</div>
|
|
183
|
+
<h3>Query Failed</h3>
|
|
184
|
+
<p className="error-message">{error}</p>
|
|
185
|
+
<button
|
|
186
|
+
className="action-btn action-btn-primary"
|
|
187
|
+
onClick={onBackToPlan}
|
|
188
|
+
>
|
|
189
|
+
Back to Plan
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
) : (
|
|
193
|
+
<div className="results-table-section">
|
|
194
|
+
<div className="table-header">
|
|
195
|
+
<span className="table-title">Results</span>
|
|
196
|
+
<span className="table-count">
|
|
197
|
+
{rowCount.toLocaleString()} rows
|
|
198
|
+
</span>
|
|
199
|
+
{filters && filters.length > 0 && (
|
|
200
|
+
<div className="table-filters">
|
|
201
|
+
{filters.map((filter, idx) => (
|
|
202
|
+
<span key={idx} className="filter-chip small">
|
|
203
|
+
{filter}
|
|
204
|
+
</span>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
<div className="results-table-wrapper">
|
|
210
|
+
{rowCount === 0 ? (
|
|
211
|
+
<div className="table-empty">
|
|
212
|
+
<p>No results returned</p>
|
|
213
|
+
</div>
|
|
214
|
+
) : (
|
|
215
|
+
<table className="results-table">
|
|
216
|
+
<thead>
|
|
217
|
+
<tr>
|
|
218
|
+
{columns.map((col, idx) => (
|
|
219
|
+
<th
|
|
220
|
+
key={idx}
|
|
221
|
+
title={col.semantic_name || col.name}
|
|
222
|
+
onClick={() => handleSort(idx)}
|
|
223
|
+
className={sortColumn === idx ? 'sorted' : ''}
|
|
224
|
+
>
|
|
225
|
+
<span className="col-header-content">
|
|
226
|
+
{col.name}
|
|
227
|
+
<span className="sort-arrows">
|
|
228
|
+
<span
|
|
229
|
+
className={`sort-arrow up ${
|
|
230
|
+
sortColumn === idx &&
|
|
231
|
+
sortDirection === 'asc'
|
|
232
|
+
? 'active'
|
|
233
|
+
: ''
|
|
234
|
+
}`}
|
|
235
|
+
>
|
|
236
|
+
▲
|
|
237
|
+
</span>
|
|
238
|
+
<span
|
|
239
|
+
className={`sort-arrow down ${
|
|
240
|
+
sortColumn === idx &&
|
|
241
|
+
sortDirection === 'desc'
|
|
242
|
+
? 'active'
|
|
243
|
+
: ''
|
|
244
|
+
}`}
|
|
245
|
+
>
|
|
246
|
+
▼
|
|
247
|
+
</span>
|
|
248
|
+
</span>
|
|
249
|
+
</span>
|
|
250
|
+
<span className="col-type">{col.type}</span>
|
|
251
|
+
</th>
|
|
252
|
+
))}
|
|
253
|
+
</tr>
|
|
254
|
+
</thead>
|
|
255
|
+
<tbody>
|
|
256
|
+
{sortedRows.map((row, rowIdx) => (
|
|
257
|
+
<tr key={rowIdx}>
|
|
258
|
+
{row.map((cell, cellIdx) => (
|
|
259
|
+
<td key={cellIdx}>
|
|
260
|
+
{cell === null ? (
|
|
261
|
+
<span className="null-value">NULL</span>
|
|
262
|
+
) : (
|
|
263
|
+
String(cell)
|
|
264
|
+
)}
|
|
265
|
+
</td>
|
|
266
|
+
))}
|
|
267
|
+
</tr>
|
|
268
|
+
))}
|
|
269
|
+
</tbody>
|
|
270
|
+
</table>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export default ResultsView;
|
|
@@ -17,6 +17,11 @@ export function SelectionPanel({
|
|
|
17
17
|
onLoadCubePreset,
|
|
18
18
|
loadedCubeName = null, // Managed by parent for URL persistence
|
|
19
19
|
onClearSelection,
|
|
20
|
+
filters = [],
|
|
21
|
+
onFiltersChange,
|
|
22
|
+
onRunQuery,
|
|
23
|
+
canRunQuery = false,
|
|
24
|
+
queryLoading = false,
|
|
20
25
|
}) {
|
|
21
26
|
const [metricsSearch, setMetricsSearch] = useState('');
|
|
22
27
|
const [dimensionsSearch, setDimensionsSearch] = useState('');
|
|
@@ -25,8 +30,11 @@ export function SelectionPanel({
|
|
|
25
30
|
const [cubeSearch, setCubeSearch] = useState('');
|
|
26
31
|
const [metricsChipsExpanded, setMetricsChipsExpanded] = useState(false);
|
|
27
32
|
const [dimensionsChipsExpanded, setDimensionsChipsExpanded] = useState(false);
|
|
33
|
+
const [filterInput, setFilterInput] = useState('');
|
|
28
34
|
const prevSearchRef = useRef('');
|
|
29
35
|
const cubeDropdownRef = useRef(null);
|
|
36
|
+
const metricsSearchRef = useRef(null);
|
|
37
|
+
const dimensionsSearchRef = useRef(null);
|
|
30
38
|
|
|
31
39
|
// Threshold for showing expand/collapse button
|
|
32
40
|
const CHIPS_COLLAPSE_THRESHOLD = 8;
|
|
@@ -261,6 +269,27 @@ export function SelectionPanel({
|
|
|
261
269
|
}
|
|
262
270
|
};
|
|
263
271
|
|
|
272
|
+
const handleAddFilter = () => {
|
|
273
|
+
const trimmed = filterInput.trim();
|
|
274
|
+
if (trimmed && !filters.includes(trimmed) && onFiltersChange) {
|
|
275
|
+
onFiltersChange([...filters, trimmed]);
|
|
276
|
+
setFilterInput('');
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleFilterKeyDown = e => {
|
|
281
|
+
if (e.key === 'Enter') {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
handleAddFilter();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const handleRemoveFilter = filterToRemove => {
|
|
288
|
+
if (onFiltersChange) {
|
|
289
|
+
onFiltersChange(filters.filter(f => f !== filterToRemove));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
264
293
|
return (
|
|
265
294
|
<div className="selection-panel">
|
|
266
295
|
{/* Cube Preset Dropdown */}
|
|
@@ -286,7 +315,7 @@ export function SelectionPanel({
|
|
|
286
315
|
</button>
|
|
287
316
|
{(selectedMetrics.length > 0 || selectedDimensions.length > 0) && (
|
|
288
317
|
<button className="clear-all-btn" onClick={clearSelection}>
|
|
289
|
-
Clear
|
|
318
|
+
Clear
|
|
290
319
|
</button>
|
|
291
320
|
)}
|
|
292
321
|
</div>
|
|
@@ -312,7 +341,9 @@ export function SelectionPanel({
|
|
|
312
341
|
filteredCubes.map(cube => (
|
|
313
342
|
<button
|
|
314
343
|
key={cube.name}
|
|
315
|
-
className=
|
|
344
|
+
className={`cube-option ${
|
|
345
|
+
loadedCubeName === cube.name ? 'selected' : ''
|
|
346
|
+
}`}
|
|
316
347
|
onClick={() => handleCubeSelect(cube)}
|
|
317
348
|
>
|
|
318
349
|
<span className="cube-name">
|
|
@@ -320,6 +351,9 @@ export function SelectionPanel({
|
|
|
320
351
|
(cube.name ? cube.name.split('.').pop() : 'Unknown')}
|
|
321
352
|
</span>
|
|
322
353
|
<span className="cube-info">{cube.name}</span>
|
|
354
|
+
{loadedCubeName === cube.name && (
|
|
355
|
+
<span className="cube-selected-icon">✓</span>
|
|
356
|
+
)}
|
|
323
357
|
</button>
|
|
324
358
|
))
|
|
325
359
|
)}
|
|
@@ -338,62 +372,71 @@ export function SelectionPanel({
|
|
|
338
372
|
</span>
|
|
339
373
|
</div>
|
|
340
374
|
|
|
341
|
-
{/*
|
|
342
|
-
|
|
343
|
-
|
|
375
|
+
{/* Combined Chips + Search Input */}
|
|
376
|
+
<div
|
|
377
|
+
className="combobox-input"
|
|
378
|
+
onClick={() => metricsSearchRef.current?.focus()}
|
|
379
|
+
>
|
|
380
|
+
{selectedMetrics.length > 0 && (
|
|
344
381
|
<div
|
|
345
|
-
className={`
|
|
346
|
-
|
|
382
|
+
className={`combobox-chips ${
|
|
383
|
+
selectedMetrics.length > CHIPS_COLLAPSE_THRESHOLD
|
|
384
|
+
? metricsChipsExpanded
|
|
385
|
+
? 'expanded'
|
|
386
|
+
: 'collapsed'
|
|
387
|
+
: ''
|
|
347
388
|
}`}
|
|
348
389
|
>
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
390
|
+
{selectedMetrics.map(metric => (
|
|
391
|
+
<span key={metric} className="selected-chip metric-chip">
|
|
392
|
+
{getShortName(metric)}
|
|
393
|
+
<button
|
|
394
|
+
className="chip-remove"
|
|
395
|
+
onClick={e => {
|
|
396
|
+
e.stopPropagation();
|
|
397
|
+
removeMetric(metric);
|
|
398
|
+
}}
|
|
399
|
+
title={`Remove ${getShortName(metric)}`}
|
|
400
|
+
>
|
|
401
|
+
×
|
|
402
|
+
</button>
|
|
403
|
+
</span>
|
|
404
|
+
))}
|
|
363
405
|
</div>
|
|
406
|
+
)}
|
|
407
|
+
<div className="combobox-input-row">
|
|
408
|
+
<input
|
|
409
|
+
ref={metricsSearchRef}
|
|
410
|
+
type="text"
|
|
411
|
+
className="combobox-search"
|
|
412
|
+
placeholder="Search metrics..."
|
|
413
|
+
value={metricsSearch}
|
|
414
|
+
onChange={e => setMetricsSearch(e.target.value)}
|
|
415
|
+
onClick={e => e.stopPropagation()}
|
|
416
|
+
/>
|
|
364
417
|
{selectedMetrics.length > CHIPS_COLLAPSE_THRESHOLD && (
|
|
365
418
|
<button
|
|
366
|
-
className="
|
|
367
|
-
onClick={
|
|
419
|
+
className="combobox-action"
|
|
420
|
+
onClick={e => {
|
|
421
|
+
e.stopPropagation();
|
|
422
|
+
setMetricsChipsExpanded(!metricsChipsExpanded);
|
|
423
|
+
}}
|
|
368
424
|
>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
425
|
+
{metricsChipsExpanded ? 'Show less' : 'Show all'}
|
|
426
|
+
</button>
|
|
427
|
+
)}
|
|
428
|
+
{selectedMetrics.length > 0 && (
|
|
429
|
+
<button
|
|
430
|
+
className="combobox-action"
|
|
431
|
+
onClick={e => {
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
onMetricsChange([]);
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
436
|
+
Clear
|
|
377
437
|
</button>
|
|
378
438
|
)}
|
|
379
439
|
</div>
|
|
380
|
-
)}
|
|
381
|
-
|
|
382
|
-
<div className="search-box">
|
|
383
|
-
<input
|
|
384
|
-
type="text"
|
|
385
|
-
placeholder="Search metrics..."
|
|
386
|
-
value={metricsSearch}
|
|
387
|
-
onChange={e => setMetricsSearch(e.target.value)}
|
|
388
|
-
/>
|
|
389
|
-
{metricsSearch && (
|
|
390
|
-
<button
|
|
391
|
-
className="clear-search"
|
|
392
|
-
onClick={() => setMetricsSearch('')}
|
|
393
|
-
>
|
|
394
|
-
×
|
|
395
|
-
</button>
|
|
396
|
-
)}
|
|
397
440
|
</div>
|
|
398
441
|
|
|
399
442
|
<div className="selection-list">
|
|
@@ -489,69 +532,74 @@ export function SelectionPanel({
|
|
|
489
532
|
<div className="empty-list">Loading dimensions...</div>
|
|
490
533
|
) : (
|
|
491
534
|
<>
|
|
492
|
-
{/*
|
|
493
|
-
|
|
494
|
-
|
|
535
|
+
{/* Combined Chips + Search Input */}
|
|
536
|
+
<div
|
|
537
|
+
className="combobox-input"
|
|
538
|
+
onClick={() => dimensionsSearchRef.current?.focus()}
|
|
539
|
+
>
|
|
540
|
+
{selectedDimensions.length > 0 && (
|
|
495
541
|
<div
|
|
496
|
-
className={`
|
|
497
|
-
|
|
542
|
+
className={`combobox-chips ${
|
|
543
|
+
selectedDimensions.length > CHIPS_COLLAPSE_THRESHOLD
|
|
544
|
+
? dimensionsChipsExpanded
|
|
545
|
+
? 'expanded'
|
|
546
|
+
: 'collapsed'
|
|
547
|
+
: ''
|
|
498
548
|
}`}
|
|
499
549
|
>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
550
|
+
{selectedDimensions.map(dimName => (
|
|
551
|
+
<span
|
|
552
|
+
key={dimName}
|
|
553
|
+
className="selected-chip dimension-chip"
|
|
554
|
+
>
|
|
555
|
+
{getDimDisplayName(dimName)}
|
|
556
|
+
<button
|
|
557
|
+
className="chip-remove"
|
|
558
|
+
onClick={e => {
|
|
559
|
+
e.stopPropagation();
|
|
560
|
+
removeDimension(dimName);
|
|
561
|
+
}}
|
|
562
|
+
title={`Remove ${getDimDisplayName(dimName)}`}
|
|
505
563
|
>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
className="chip-remove"
|
|
511
|
-
onClick={() => removeDimension(dimName)}
|
|
512
|
-
title={`Remove ${getDimDisplayName(dimName)}`}
|
|
513
|
-
>
|
|
514
|
-
×
|
|
515
|
-
</button>
|
|
516
|
-
</span>
|
|
517
|
-
))}
|
|
518
|
-
</div>
|
|
564
|
+
×
|
|
565
|
+
</button>
|
|
566
|
+
</span>
|
|
567
|
+
))}
|
|
519
568
|
</div>
|
|
569
|
+
)}
|
|
570
|
+
<div className="combobox-input-row">
|
|
571
|
+
<input
|
|
572
|
+
ref={dimensionsSearchRef}
|
|
573
|
+
type="text"
|
|
574
|
+
className="combobox-search"
|
|
575
|
+
placeholder="Search dimensions..."
|
|
576
|
+
value={dimensionsSearch}
|
|
577
|
+
onChange={e => setDimensionsSearch(e.target.value)}
|
|
578
|
+
onClick={e => e.stopPropagation()}
|
|
579
|
+
/>
|
|
520
580
|
{selectedDimensions.length > CHIPS_COLLAPSE_THRESHOLD && (
|
|
521
581
|
<button
|
|
522
|
-
className="
|
|
523
|
-
onClick={
|
|
524
|
-
|
|
525
|
-
|
|
582
|
+
className="combobox-action"
|
|
583
|
+
onClick={e => {
|
|
584
|
+
e.stopPropagation();
|
|
585
|
+
setDimensionsChipsExpanded(!dimensionsChipsExpanded);
|
|
586
|
+
}}
|
|
526
587
|
>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
588
|
+
{dimensionsChipsExpanded ? 'Show less' : 'Show all'}
|
|
589
|
+
</button>
|
|
590
|
+
)}
|
|
591
|
+
{selectedDimensions.length > 0 && (
|
|
592
|
+
<button
|
|
593
|
+
className="combobox-action"
|
|
594
|
+
onClick={e => {
|
|
595
|
+
e.stopPropagation();
|
|
596
|
+
onDimensionsChange([]);
|
|
597
|
+
}}
|
|
598
|
+
>
|
|
599
|
+
Clear
|
|
535
600
|
</button>
|
|
536
601
|
)}
|
|
537
602
|
</div>
|
|
538
|
-
)}
|
|
539
|
-
|
|
540
|
-
<div className="search-box">
|
|
541
|
-
<input
|
|
542
|
-
type="text"
|
|
543
|
-
placeholder="Search dimensions..."
|
|
544
|
-
value={dimensionsSearch}
|
|
545
|
-
onChange={e => setDimensionsSearch(e.target.value)}
|
|
546
|
-
/>
|
|
547
|
-
{dimensionsSearch && (
|
|
548
|
-
<button
|
|
549
|
-
className="clear-search"
|
|
550
|
-
onClick={() => setDimensionsSearch('')}
|
|
551
|
-
>
|
|
552
|
-
×
|
|
553
|
-
</button>
|
|
554
|
-
)}
|
|
555
603
|
</div>
|
|
556
604
|
|
|
557
605
|
<div className="selection-list dimensions-list">
|
|
@@ -586,6 +634,83 @@ export function SelectionPanel({
|
|
|
586
634
|
</>
|
|
587
635
|
)}
|
|
588
636
|
</div>
|
|
637
|
+
|
|
638
|
+
{/* Divider */}
|
|
639
|
+
<div className="section-divider" />
|
|
640
|
+
|
|
641
|
+
{/* Filters Section */}
|
|
642
|
+
<div className="selection-section filters-section">
|
|
643
|
+
<div className="section-header">
|
|
644
|
+
<h3>Filters</h3>
|
|
645
|
+
<span className="selection-count">{filters.length} applied</span>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
{/* Filter chips */}
|
|
649
|
+
{filters.length > 0 && (
|
|
650
|
+
<div className="filter-chips-container">
|
|
651
|
+
{filters.map((filter, idx) => (
|
|
652
|
+
<span key={idx} className="filter-chip">
|
|
653
|
+
<span className="filter-chip-text">{filter}</span>
|
|
654
|
+
<button
|
|
655
|
+
className="filter-chip-remove"
|
|
656
|
+
onClick={() => handleRemoveFilter(filter)}
|
|
657
|
+
title="Remove filter"
|
|
658
|
+
>
|
|
659
|
+
×
|
|
660
|
+
</button>
|
|
661
|
+
</span>
|
|
662
|
+
))}
|
|
663
|
+
</div>
|
|
664
|
+
)}
|
|
665
|
+
|
|
666
|
+
{/* Filter input */}
|
|
667
|
+
<div className="filter-input-container">
|
|
668
|
+
<input
|
|
669
|
+
type="text"
|
|
670
|
+
className="filter-input"
|
|
671
|
+
placeholder="e.g. v3.date.date_id >= '2024-01-01'"
|
|
672
|
+
value={filterInput}
|
|
673
|
+
onChange={e => setFilterInput(e.target.value)}
|
|
674
|
+
onKeyDown={handleFilterKeyDown}
|
|
675
|
+
/>
|
|
676
|
+
<button
|
|
677
|
+
className="filter-add-btn"
|
|
678
|
+
onClick={handleAddFilter}
|
|
679
|
+
disabled={!filterInput.trim()}
|
|
680
|
+
>
|
|
681
|
+
Add
|
|
682
|
+
</button>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
{/* Run Query Section */}
|
|
687
|
+
<div className="run-query-section">
|
|
688
|
+
<button
|
|
689
|
+
className="run-query-btn"
|
|
690
|
+
onClick={onRunQuery}
|
|
691
|
+
disabled={!canRunQuery || queryLoading}
|
|
692
|
+
>
|
|
693
|
+
{queryLoading ? (
|
|
694
|
+
<>
|
|
695
|
+
<span className="spinner small" />
|
|
696
|
+
Running...
|
|
697
|
+
</>
|
|
698
|
+
) : (
|
|
699
|
+
<>
|
|
700
|
+
<span className="run-icon">▶</span>
|
|
701
|
+
Run Query
|
|
702
|
+
</>
|
|
703
|
+
)}
|
|
704
|
+
</button>
|
|
705
|
+
{!canRunQuery && selectedMetrics.length > 0 && (
|
|
706
|
+
<span className="run-hint">Select at least one dimension</span>
|
|
707
|
+
)}
|
|
708
|
+
{!canRunQuery && selectedMetrics.length === 0 && (
|
|
709
|
+
<span className="run-hint">
|
|
710
|
+
Select metrics and dimensions to run a query
|
|
711
|
+
</span>
|
|
712
|
+
)}
|
|
713
|
+
</div>
|
|
589
714
|
</div>
|
|
590
715
|
);
|
|
591
716
|
}
|