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.
- package/package.json +1 -1
- package/src/app/components/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +412 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +420 -86
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +32 -1
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +322 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +431 -2
- package/src/app/pages/QueryPlannerPage/index.jsx +31 -5
- package/src/app/pages/QueryPlannerPage/styles.css +211 -2
- package/src/app/pages/Root/__tests__/index.test.jsx +2 -3
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/services/DJService.js +133 -23
- package/src/app/services/__tests__/DJService.test.jsx +600 -11
- 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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
378
|
+
{/* Two-pane layout: SQL (top) + Results (bottom) */}
|
|
104
379
|
<div className="results-panes">
|
|
105
|
-
{/* SQL Pane
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|