datajunction-ui 0.0.93 → 0.0.95

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NodeComponents.jsx +4 -0
  3. package/src/app/components/Tab.jsx +11 -16
  4. package/src/app/components/__tests__/Tab.test.jsx +4 -2
  5. package/src/app/hooks/useWorkspaceData.js +226 -0
  6. package/src/app/index.tsx +17 -1
  7. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
  8. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
  9. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
  10. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
  11. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
  12. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
  13. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
  14. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
  15. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
  16. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
  17. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
  18. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
  20. package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
  21. package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
  22. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
  23. package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
  24. package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
  25. package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
  26. package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
  27. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
  28. package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +412 -0
  29. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
  30. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
  31. package/src/app/pages/NodePage/index.jsx +15 -8
  32. package/src/app/pages/QueryPlannerPage/ResultsView.jsx +420 -86
  33. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +32 -1
  34. package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +322 -0
  35. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +431 -2
  36. package/src/app/pages/QueryPlannerPage/index.jsx +31 -5
  37. package/src/app/pages/QueryPlannerPage/styles.css +211 -2
  38. package/src/app/pages/Root/__tests__/index.test.jsx +2 -3
  39. package/src/app/pages/Root/index.tsx +1 -1
  40. package/src/app/services/DJService.js +133 -23
  41. package/src/app/services/__tests__/DJService.test.jsx +600 -11
  42. package/src/styles/index.css +32 -0
@@ -1,13 +1,269 @@
1
- import { useState, useCallback, useMemo } from 'react';
1
+ import { useState, useCallback, useMemo, useEffect } from 'react';
2
2
  import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
3
3
  import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
4
4
  import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql';
5
+ import {
6
+ LineChart,
7
+ Line,
8
+ BarChart,
9
+ Bar,
10
+ XAxis,
11
+ YAxis,
12
+ CartesianGrid,
13
+ Tooltip,
14
+ ResponsiveContainer,
15
+ } from 'recharts';
5
16
 
6
17
  SyntaxHighlighter.registerLanguage('sql', sql);
7
18
 
19
+ const SERIES_COLORS = [
20
+ '#3b82f6',
21
+ '#a2283e',
22
+ '#059669',
23
+ '#d97706',
24
+ '#8b5cf6',
25
+ '#0ea5e9',
26
+ '#ec4899',
27
+ ];
28
+
29
+ // Threshold for switching from multi-series to small multiples
30
+ const SMALL_MULTIPLES_THRESHOLD = 2;
31
+
32
+ function isTimeColumn(col) {
33
+ const name = col.name.toLowerCase();
34
+ const type = (col.type || '').toLowerCase();
35
+ return (
36
+ type.includes('date') ||
37
+ type.includes('timestamp') ||
38
+ type.includes('time') ||
39
+ name === 'date' ||
40
+ name === 'day' ||
41
+ name === 'week' ||
42
+ name === 'month' ||
43
+ name === 'year' ||
44
+ name === 'quarter' ||
45
+ name.endsWith('_date') ||
46
+ name.endsWith('_day') ||
47
+ name.endsWith('_week') ||
48
+ name.endsWith('_month') ||
49
+ name.endsWith('_year') ||
50
+ name.endsWith('_at') ||
51
+ name.startsWith('date') ||
52
+ name.startsWith('ds')
53
+ );
54
+ }
55
+
56
+ function isNumericColumn(col) {
57
+ const type = (col.type || '').toLowerCase();
58
+ return (
59
+ type.includes('int') ||
60
+ type.includes('float') ||
61
+ type.includes('double') ||
62
+ type.includes('decimal') ||
63
+ type.includes('numeric') ||
64
+ type.includes('real') ||
65
+ type.includes('number')
66
+ );
67
+ }
68
+
69
+ function detectChartConfig(columns, rows) {
70
+ if (!columns.length || !rows.length) return null;
71
+
72
+ const tagged = columns.map((c, i) => ({ ...c, idx: i }));
73
+ const timeCols = tagged.filter(c => isTimeColumn(c));
74
+ const numericCols = tagged.filter(c => isNumericColumn(c));
75
+ const nonNumericCols = tagged.filter(c => !isNumericColumn(c));
76
+
77
+ // Time dimension present → line chart (handles integer time cols like week/year)
78
+ if (timeCols.length > 0) {
79
+ const xCol = timeCols[0];
80
+ const metricCols = numericCols.filter(c => c.idx !== xCol.idx);
81
+ if (metricCols.length > 0) return { type: 'line', xCol, metricCols };
82
+ }
83
+
84
+ // String/categorical dimension → bar chart
85
+ if (nonNumericCols.length > 0 && numericCols.length > 0) {
86
+ const xCol = nonNumericCols[0];
87
+ const metricCols = numericCols.filter(c => c.idx !== xCol.idx);
88
+ if (metricCols.length > 0) return { type: 'bar', xCol, metricCols };
89
+ }
90
+
91
+ // Multiple numeric columns, no string/time dim → treat first as x-axis (line)
92
+ if (numericCols.length > 1) {
93
+ const xCol = numericCols[0];
94
+ const metricCols = numericCols.slice(1);
95
+ return { type: 'line', xCol, metricCols };
96
+ }
97
+
98
+ // Scalar result → KPI cards
99
+ if (numericCols.length > 0) {
100
+ return { type: 'kpi', metricCols: numericCols };
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ function buildChartData(columns, rows, xCol) {
107
+ const data = rows.map(row => {
108
+ const obj = {};
109
+ columns.forEach((col, i) => {
110
+ obj[col.name] = row[i];
111
+ });
112
+ return obj;
113
+ });
114
+ const key = xCol.name;
115
+ data.sort((a, b) => {
116
+ const av = a[key];
117
+ const bv = b[key];
118
+ if (av === null && bv === null) return 0;
119
+ if (av === null) return 1;
120
+ if (bv === null) return -1;
121
+ if (typeof av === 'number' && typeof bv === 'number') return av - bv;
122
+ return String(av).localeCompare(String(bv));
123
+ });
124
+ return data;
125
+ }
126
+
127
+ function formatYAxis(value) {
128
+ if (Math.abs(value) >= 1_000_000_000)
129
+ return (value / 1_000_000_000).toFixed(1) + 'B';
130
+ if (Math.abs(value) >= 1_000_000) return (value / 1_000_000).toFixed(1) + 'M';
131
+ if (Math.abs(value) >= 1_000) return (value / 1_000).toFixed(1) + 'K';
132
+ return value;
133
+ }
134
+
135
+ function KpiCards({ rows, metricCols }) {
136
+ const row = rows[0] || [];
137
+ return (
138
+ <div className="kpi-cards">
139
+ {metricCols.map(col => {
140
+ const val = row[col.idx];
141
+ const formatted =
142
+ val == null
143
+ ? '—'
144
+ : typeof val === 'number'
145
+ ? val.toLocaleString(undefined, { maximumFractionDigits: 4 })
146
+ : String(val);
147
+ return (
148
+ <div key={col.idx} className="kpi-card">
149
+ <div className="kpi-label">{col.name}</div>
150
+ <div className="kpi-value">{formatted}</div>
151
+ {col.type && <div className="kpi-type">{col.type}</div>}
152
+ </div>
153
+ );
154
+ })}
155
+ </div>
156
+ );
157
+ }
158
+
159
+ const CHART_MARGIN = { top: 8, right: 24, left: 8, bottom: 40 };
160
+ const AXIS_TICK = { fontSize: 11, fill: '#64748b' };
161
+ const TOOLTIP_STYLE = { fontSize: 12, border: '1px solid #e2e8f0' };
162
+
163
+ function Chart({
164
+ type,
165
+ xCol,
166
+ metricCols,
167
+ chartData,
168
+ seriesColors = SERIES_COLORS,
169
+ }) {
170
+ const showDots = chartData.length <= 60;
171
+ const xInterval = type === 'line' ? 'preserveStartEnd' : 0;
172
+ if (type === 'line') {
173
+ return (
174
+ <ResponsiveContainer width="100%" height="100%">
175
+ <LineChart data={chartData} margin={CHART_MARGIN}>
176
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
177
+ <XAxis
178
+ dataKey={xCol.name}
179
+ tick={AXIS_TICK}
180
+ angle={-35}
181
+ textAnchor="end"
182
+ interval={xInterval}
183
+ />
184
+ <YAxis tickFormatter={formatYAxis} tick={AXIS_TICK} width={60} />
185
+ <Tooltip contentStyle={TOOLTIP_STYLE} />
186
+ {metricCols.map((col, i) => (
187
+ <Line
188
+ key={col.idx}
189
+ type="monotone"
190
+ dataKey={col.name}
191
+ stroke={seriesColors[i % seriesColors.length]}
192
+ dot={showDots}
193
+ strokeWidth={2}
194
+ />
195
+ ))}
196
+ </LineChart>
197
+ </ResponsiveContainer>
198
+ );
199
+ }
200
+ return (
201
+ <ResponsiveContainer width="100%" height="100%">
202
+ <BarChart data={chartData} margin={CHART_MARGIN}>
203
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
204
+ <XAxis
205
+ dataKey={xCol.name}
206
+ tick={AXIS_TICK}
207
+ angle={-35}
208
+ textAnchor="end"
209
+ interval={xInterval}
210
+ />
211
+ <YAxis tickFormatter={formatYAxis} tick={AXIS_TICK} width={60} />
212
+ <Tooltip contentStyle={TOOLTIP_STYLE} />
213
+ {metricCols.map((col, i) => (
214
+ <Bar
215
+ key={col.idx}
216
+ dataKey={col.name}
217
+ fill={seriesColors[i % seriesColors.length]}
218
+ />
219
+ ))}
220
+ </BarChart>
221
+ </ResponsiveContainer>
222
+ );
223
+ }
224
+
225
+ function ChartView({ chartConfig, chartData, rows, columns }) {
226
+ if (chartConfig.type === 'kpi') {
227
+ return <KpiCards rows={rows} metricCols={chartConfig.metricCols} />;
228
+ }
229
+
230
+ const { type, xCol, metricCols } = chartConfig;
231
+ const useSmallMultiples = metricCols.length > SMALL_MULTIPLES_THRESHOLD;
232
+
233
+ if (useSmallMultiples) {
234
+ return (
235
+ <div className="small-multiples">
236
+ {metricCols.map((col, i) => (
237
+ <div key={col.idx} className="small-multiple">
238
+ <div className="small-multiple-label">{col.name}</div>
239
+ <div className="small-multiple-chart">
240
+ <Chart
241
+ type={type}
242
+ xCol={xCol}
243
+ metricCols={[col]}
244
+ chartData={chartData}
245
+ seriesColors={[SERIES_COLORS[i % SERIES_COLORS.length]]}
246
+ />
247
+ </div>
248
+ </div>
249
+ ))}
250
+ </div>
251
+ );
252
+ }
253
+
254
+ return (
255
+ <Chart
256
+ type={type}
257
+ xCol={xCol}
258
+ metricCols={metricCols}
259
+ chartData={chartData}
260
+ />
261
+ );
262
+ }
263
+
8
264
  /**
9
265
  * ResultsView - Displays query results with SQL and data table
10
- * Layout: SQL in top 1/3, results in bottom 2/3
266
+ * Layout: SQL in top ~25%, results in bottom ~75% with Table/Chart tabs
11
267
  */
12
268
  export function ResultsView({
13
269
  sql: sqlQuery,
@@ -22,10 +278,12 @@ export function ResultsView({
22
278
  dialect,
23
279
  cubeName,
24
280
  availability,
281
+ links,
25
282
  }) {
26
283
  const [copied, setCopied] = useState(false);
27
284
  const [sortColumn, setSortColumn] = useState(null);
28
285
  const [sortDirection, setSortDirection] = useState('asc');
286
+ const [activeTab, setActiveTab] = useState('table');
29
287
 
30
288
  const handleCopySql = useCallback(() => {
31
289
  if (sqlQuery) {
@@ -35,12 +293,13 @@ export function ResultsView({
35
293
  }
36
294
  }, [sqlQuery]);
37
295
 
38
- // Parse results data - handle new v3 format
39
- const columns = results?.results?.[0]?.columns || [];
40
- const rows = results?.results?.[0]?.rows || [];
296
+ const columns = useMemo(
297
+ () => results?.results?.[0]?.columns || [],
298
+ [results],
299
+ );
300
+ const rows = useMemo(() => results?.results?.[0]?.rows || [], [results]);
41
301
  const rowCount = rows.length;
42
302
 
43
- // Handle column header click for sorting
44
303
  const handleSort = useCallback(
45
304
  columnIndex => {
46
305
  if (sortColumn === columnIndex) {
@@ -53,17 +312,14 @@ export function ResultsView({
53
312
  [sortColumn],
54
313
  );
55
314
 
56
- // Sort rows based on current sort state
57
315
  const sortedRows = useMemo(() => {
58
316
  if (sortColumn === null) return rows;
59
317
  return [...rows].sort((a, b) => {
60
318
  const aVal = a[sortColumn];
61
319
  const bVal = b[sortColumn];
62
- // Handle nulls - nulls go last
63
320
  if (aVal === null && bVal === null) return 0;
64
321
  if (aVal === null) return 1;
65
322
  if (bVal === null) return -1;
66
- // Compare values
67
323
  let cmp;
68
324
  if (typeof aVal === 'number' && typeof bVal === 'number') {
69
325
  cmp = aVal - bVal;
@@ -74,6 +330,25 @@ export function ResultsView({
74
330
  });
75
331
  }, [rows, sortColumn, sortDirection]);
76
332
 
333
+ const chartConfig = useMemo(
334
+ () => detectChartConfig(columns, rows),
335
+ [columns, rows],
336
+ );
337
+ const chartData = useMemo(
338
+ () =>
339
+ chartConfig && chartConfig.xCol
340
+ ? buildChartData(columns, rows, chartConfig.xCol)
341
+ : [],
342
+ [columns, rows, chartConfig],
343
+ );
344
+
345
+ const canChart = chartConfig !== null && rowCount > 0;
346
+
347
+ // Reset to table view if new results can't be charted
348
+ useEffect(() => {
349
+ if (!canChart && activeTab === 'chart') setActiveTab('table');
350
+ }, [canChart, activeTab]);
351
+
77
352
  return (
78
353
  <div className="results-view">
79
354
  {/* Header */}
@@ -100,9 +375,9 @@ export function ResultsView({
100
375
  </div>
101
376
  </div>
102
377
 
103
- {/* Two-pane layout: SQL (top 1/3) + Results (bottom 2/3) */}
378
+ {/* Two-pane layout: SQL (top) + Results (bottom) */}
104
379
  <div className="results-panes">
105
- {/* SQL Pane - always visible, top 1/3 */}
380
+ {/* SQL Pane */}
106
381
  <div className="sql-pane">
107
382
  <div className="sql-pane-header">
108
383
  <span className="sql-pane-title">SQL Query</span>
@@ -166,7 +441,7 @@ export function ResultsView({
166
441
  </div>
167
442
  </div>
168
443
 
169
- {/* Results Pane - bottom 2/3 */}
444
+ {/* Results Pane */}
170
445
  <div className="results-pane">
171
446
  {loading ? (
172
447
  <div className="results-loading">
@@ -176,6 +451,21 @@ export function ResultsView({
176
451
  Querying {selectedMetrics.length} metric(s) with{' '}
177
452
  {selectedDimensions.length} dimension(s)
178
453
  </span>
454
+ {links && links.length > 0 && (
455
+ <span className="results-links">
456
+ {links.map((link, idx) => (
457
+ <a
458
+ key={idx}
459
+ href={link}
460
+ target="_blank"
461
+ rel="noopener noreferrer"
462
+ className="results-link"
463
+ >
464
+ View query ↗
465
+ </a>
466
+ ))}
467
+ </span>
468
+ )}
179
469
  </div>
180
470
  ) : error ? (
181
471
  <div className="results-error">
@@ -191,85 +481,129 @@ export function ResultsView({
191
481
  </div>
192
482
  ) : (
193
483
  <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
- )}
484
+ {/* Tab bar */}
485
+ <div className="results-tabs-bar">
486
+ <div className="results-tabs">
487
+ <button
488
+ className={`results-tab ${
489
+ activeTab === 'table' ? 'active' : ''
490
+ }`}
491
+ onClick={() => setActiveTab('table')}
492
+ >
493
+ Table
494
+ </button>
495
+ <button
496
+ className={`results-tab ${
497
+ activeTab === 'chart' ? 'active' : ''
498
+ } ${!canChart ? 'disabled' : ''}`}
499
+ onClick={() => canChart && setActiveTab('chart')}
500
+ title={
501
+ !canChart
502
+ ? 'No chartable data (need at least one numeric column)'
503
+ : undefined
504
+ }
505
+ >
506
+ Chart
507
+ </button>
508
+ </div>
509
+ <div className="results-tabs-meta">
510
+ <span className="table-count">
511
+ {rowCount.toLocaleString()} rows
512
+ </span>
513
+ {filters && filters.length > 0 && (
514
+ <div className="table-filters">
515
+ {filters.map((filter, idx) => (
516
+ <span key={idx} className="filter-chip small">
517
+ {filter}
518
+ </span>
519
+ ))}
520
+ </div>
521
+ )}
522
+ </div>
208
523
  </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
-
524
+
525
+ {/* Content */}
526
+ {activeTab === 'table' ? (
527
+ <div className="results-table-wrapper">
528
+ {rowCount === 0 ? (
529
+ <div className="table-empty">
530
+ <p>No results returned</p>
531
+ </div>
532
+ ) : (
533
+ <table className="results-table">
534
+ <thead>
535
+ <tr>
536
+ {columns.map((col, idx) => (
537
+ <th
538
+ key={idx}
539
+ title={col.semantic_name || col.name}
540
+ onClick={() => handleSort(idx)}
541
+ className={sortColumn === idx ? 'sorted' : ''}
542
+ >
543
+ <span className="col-header-content">
544
+ {col.name}
545
+ <span className="sort-arrows">
546
+ <span
547
+ className={`sort-arrow up ${
548
+ sortColumn === idx &&
549
+ sortDirection === 'asc'
550
+ ? 'active'
551
+ : ''
552
+ }`}
553
+ >
554
+
555
+ </span>
556
+ <span
557
+ className={`sort-arrow down ${
558
+ sortColumn === idx &&
559
+ sortDirection === 'desc'
560
+ ? 'active'
561
+ : ''
562
+ }`}
563
+ >
564
+
565
+ </span>
247
566
  </span>
248
567
  </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>
568
+ <span className="col-type">{col.type}</span>
569
+ </th>
266
570
  ))}
267
571
  </tr>
268
- ))}
269
- </tbody>
270
- </table>
271
- )}
272
- </div>
572
+ </thead>
573
+ <tbody>
574
+ {sortedRows.map((row, rowIdx) => (
575
+ <tr key={rowIdx}>
576
+ {row.map((cell, cellIdx) => (
577
+ <td key={cellIdx}>
578
+ {cell === null ? (
579
+ <span className="null-value">NULL</span>
580
+ ) : (
581
+ String(cell)
582
+ )}
583
+ </td>
584
+ ))}
585
+ </tr>
586
+ ))}
587
+ </tbody>
588
+ </table>
589
+ )}
590
+ </div>
591
+ ) : (
592
+ <div className="results-chart-wrapper">
593
+ {canChart ? (
594
+ <ChartView
595
+ chartConfig={chartConfig}
596
+ chartData={chartData}
597
+ rows={rows}
598
+ columns={columns}
599
+ />
600
+ ) : (
601
+ <div className="chart-no-data">
602
+ No chartable data detected
603
+ </div>
604
+ )}
605
+ </div>
606
+ )}
273
607
  </div>
274
608
  )}
275
609
  </div>
@@ -1,5 +1,11 @@
1
1
  import { useState, useMemo, useEffect, useRef } from 'react';
2
2
 
3
+ const ENGINE_OPTIONS = [
4
+ { value: null, label: 'Auto' },
5
+ { value: 'druid', label: 'Druid' },
6
+ { value: 'trino', label: 'Trino' },
7
+ ];
8
+
3
9
  /**
4
10
  * SelectionPanel - Browse and select metrics and dimensions
5
11
  * Features selected items as chips at the top for visibility
@@ -19,6 +25,8 @@ export function SelectionPanel({
19
25
  onClearSelection,
20
26
  filters = [],
21
27
  onFiltersChange,
28
+ selectedEngine = null,
29
+ onEngineChange,
22
30
  onRunQuery,
23
31
  canRunQuery = false,
24
32
  queryLoading = false,
@@ -604,7 +612,11 @@ export function SelectionPanel({
604
612
 
605
613
  <div className="selection-list dimensions-list">
606
614
  {filteredDimensions.map(dim => (
607
- <label key={dim.name} className="selection-item dimension-item">
615
+ <label
616
+ key={dim.name}
617
+ className="selection-item dimension-item"
618
+ title={dim.name}
619
+ >
608
620
  <input
609
621
  type="checkbox"
610
622
  checked={selectedDimensions.includes(dim.name)}
@@ -614,6 +626,7 @@ export function SelectionPanel({
614
626
  <span className="item-name">
615
627
  {getDimDisplayName(dim.name)}
616
628
  </span>
629
+ <span className="dimension-full-name">{dim.name}</span>
617
630
  {dim.path && dim.path.length > 1 && (
618
631
  <span className="dimension-path">
619
632
  {dim.path.slice(1).join(' ▶ ')}
@@ -683,6 +696,24 @@ export function SelectionPanel({
683
696
  </div>
684
697
  </div>
685
698
 
699
+ {/* Engine Selection */}
700
+ <div className="engine-section">
701
+ <span className="engine-label">Engine</span>
702
+ <div className="engine-pills">
703
+ {ENGINE_OPTIONS.map(({ value, label }) => (
704
+ <button
705
+ key={label}
706
+ className={`engine-pill${
707
+ selectedEngine === value ? ' active' : ''
708
+ }`}
709
+ onClick={() => onEngineChange && onEngineChange(value)}
710
+ >
711
+ {label}
712
+ </button>
713
+ ))}
714
+ </div>
715
+ </div>
716
+
686
717
  {/* Run Query Section */}
687
718
  <div className="run-query-section">
688
719
  <button