datajunction-ui 0.0.23-rc.0 → 0.0.26
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 +8 -2
- package/src/app/index.tsx +6 -0
- package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
- package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
- package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
- package/src/app/pages/NamespacePage/index.jsx +489 -62
- package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
- package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
- package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
- package/src/app/pages/Root/index.tsx +5 -0
- package/src/app/services/DJService.js +61 -2
- package/src/styles/index.css +2 -2
- package/src/app/icons/FilterIcon.jsx +0 -7
- package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
- package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
- package/src/app/pages/NamespacePage/UserSelect.jsx +0 -47
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
3
|
+
import { atomOneLight } from 'react-syntax-highlighter/src/styles/hljs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to extract dimension node name from a dimension path
|
|
7
|
+
* e.g., "v3.customer.name" -> "v3.customer"
|
|
8
|
+
* e.g., "v3.date.month[order]" -> "v3.date"
|
|
9
|
+
*/
|
|
10
|
+
function getDimensionNodeName(dimPath) {
|
|
11
|
+
// Remove role suffix if present (e.g., "[order]")
|
|
12
|
+
const pathWithoutRole = dimPath.split('[')[0];
|
|
13
|
+
// Split by dot and remove the last segment (column name)
|
|
14
|
+
const parts = pathWithoutRole.split('.');
|
|
15
|
+
if (parts.length > 1) {
|
|
16
|
+
return parts.slice(0, -1).join('.');
|
|
17
|
+
}
|
|
18
|
+
return pathWithoutRole;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* QueryOverviewPanel - Default view showing metrics SQL and pre-agg summary
|
|
23
|
+
*
|
|
24
|
+
* Shown when no node is selected in the graph
|
|
25
|
+
*/
|
|
26
|
+
export function QueryOverviewPanel({
|
|
27
|
+
measuresResult,
|
|
28
|
+
metricsResult,
|
|
29
|
+
selectedMetrics,
|
|
30
|
+
selectedDimensions,
|
|
31
|
+
}) {
|
|
32
|
+
const copyToClipboard = text => {
|
|
33
|
+
navigator.clipboard.writeText(text);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// No selection yet
|
|
37
|
+
if (!selectedMetrics?.length || !selectedDimensions?.length) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="details-panel details-panel-empty">
|
|
40
|
+
<div className="empty-hint">
|
|
41
|
+
<div className="empty-icon">⊞</div>
|
|
42
|
+
<h4>Query Planner</h4>
|
|
43
|
+
<p>
|
|
44
|
+
Select metrics and dimensions from the left panel to see the
|
|
45
|
+
generated SQL and pre-aggregation plan
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Loading or no results yet
|
|
53
|
+
if (!measuresResult || !metricsResult) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="details-panel details-panel-empty">
|
|
56
|
+
<div className="empty-hint">
|
|
57
|
+
<div className="loading-spinner" />
|
|
58
|
+
<p>Building query plan...</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const grainGroups = measuresResult.grain_groups || [];
|
|
65
|
+
const metricFormulas = measuresResult.metric_formulas || [];
|
|
66
|
+
const sql = metricsResult.sql || '';
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="details-panel">
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="details-header">
|
|
72
|
+
<h2 className="details-title">Generated Query Overview</h2>
|
|
73
|
+
<p className="details-full-name">
|
|
74
|
+
{selectedMetrics.length} metric
|
|
75
|
+
{selectedMetrics.length !== 1 ? 's' : ''} ×{' '}
|
|
76
|
+
{selectedDimensions.length} dimension
|
|
77
|
+
{selectedDimensions.length !== 1 ? 's' : ''}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Pre-aggregations Summary */}
|
|
82
|
+
<div className="details-section">
|
|
83
|
+
<h3 className="section-title">
|
|
84
|
+
<span className="section-icon">◫</span>
|
|
85
|
+
Pre-Aggregations ({grainGroups.length})
|
|
86
|
+
</h3>
|
|
87
|
+
<div className="preagg-summary-list">
|
|
88
|
+
{grainGroups.map((gg, i) => {
|
|
89
|
+
const shortName = gg.parent_name?.split('.').pop() || 'Unknown';
|
|
90
|
+
const relatedMetrics = metricFormulas.filter(m =>
|
|
91
|
+
m.components?.some(comp =>
|
|
92
|
+
gg.components?.some(pc => pc.name === comp),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
return (
|
|
96
|
+
<div key={i} className="preagg-summary-card">
|
|
97
|
+
<div className="preagg-summary-header">
|
|
98
|
+
<span className="preagg-summary-name">{shortName}</span>
|
|
99
|
+
<span
|
|
100
|
+
className={`aggregability-pill aggregability-${gg.aggregability?.toLowerCase()}`}
|
|
101
|
+
>
|
|
102
|
+
{gg.aggregability}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="preagg-summary-details">
|
|
106
|
+
<div className="preagg-summary-row">
|
|
107
|
+
<span className="label">Grain:</span>
|
|
108
|
+
<span className="value">
|
|
109
|
+
{gg.grain?.join(', ') || 'None'}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="preagg-summary-row">
|
|
113
|
+
<span className="label">Measures:</span>
|
|
114
|
+
<span className="value">{gg.components?.length || 0}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="preagg-summary-row">
|
|
117
|
+
<span className="label">Metrics:</span>
|
|
118
|
+
<span className="value">
|
|
119
|
+
{relatedMetrics.map(m => m.short_name).join(', ') ||
|
|
120
|
+
'None'}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="preagg-summary-status">
|
|
125
|
+
<span className="status-indicator status-not-materialized">
|
|
126
|
+
○
|
|
127
|
+
</span>
|
|
128
|
+
<span>Not materialized</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Metrics & Dimensions Summary - Two columns */}
|
|
137
|
+
<div className="details-section">
|
|
138
|
+
<div className="selection-summary-grid">
|
|
139
|
+
{/* Metrics Column */}
|
|
140
|
+
<div className="selection-summary-column">
|
|
141
|
+
<h3 className="section-title">
|
|
142
|
+
<span className="section-icon">◈</span>
|
|
143
|
+
Metrics ({metricFormulas.length})
|
|
144
|
+
</h3>
|
|
145
|
+
<div className="selection-summary-list">
|
|
146
|
+
{metricFormulas.map((m, i) => (
|
|
147
|
+
<Link
|
|
148
|
+
key={i}
|
|
149
|
+
to={`/nodes/${m.name}`}
|
|
150
|
+
className="selection-summary-item metric clickable"
|
|
151
|
+
>
|
|
152
|
+
<span className="selection-summary-name">{m.short_name}</span>
|
|
153
|
+
{m.is_derived && (
|
|
154
|
+
<span className="compact-node-badge">Derived</span>
|
|
155
|
+
)}
|
|
156
|
+
</Link>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Dimensions Column */}
|
|
162
|
+
<div className="selection-summary-column">
|
|
163
|
+
<h3 className="section-title">
|
|
164
|
+
<span className="section-icon">⊞</span>
|
|
165
|
+
Dimensions ({selectedDimensions.length})
|
|
166
|
+
</h3>
|
|
167
|
+
<div className="selection-summary-list">
|
|
168
|
+
{selectedDimensions.map((dim, i) => {
|
|
169
|
+
const shortName = dim.split('.').pop().split('[')[0]; // Remove role suffix too
|
|
170
|
+
const nodeName = getDimensionNodeName(dim);
|
|
171
|
+
return (
|
|
172
|
+
<Link
|
|
173
|
+
key={i}
|
|
174
|
+
to={`/nodes/${nodeName}`}
|
|
175
|
+
className="selection-summary-item dimension clickable"
|
|
176
|
+
>
|
|
177
|
+
<span className="selection-summary-name">{shortName}</span>
|
|
178
|
+
</Link>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* SQL Section */}
|
|
187
|
+
{sql && (
|
|
188
|
+
<div className="details-section details-section-full details-sql-section">
|
|
189
|
+
<div className="section-header-row">
|
|
190
|
+
<h3 className="section-title">
|
|
191
|
+
<span className="section-icon">⌘</span>
|
|
192
|
+
Generated SQL
|
|
193
|
+
</h3>
|
|
194
|
+
<button
|
|
195
|
+
className="copy-sql-btn"
|
|
196
|
+
onClick={() => copyToClipboard(sql)}
|
|
197
|
+
type="button"
|
|
198
|
+
>
|
|
199
|
+
Copy SQL
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="sql-code-wrapper">
|
|
203
|
+
<SyntaxHighlighter
|
|
204
|
+
language="sql"
|
|
205
|
+
style={atomOneLight}
|
|
206
|
+
customStyle={{
|
|
207
|
+
margin: 0,
|
|
208
|
+
borderRadius: '6px',
|
|
209
|
+
fontSize: '11px',
|
|
210
|
+
background: '#f8fafc',
|
|
211
|
+
border: '1px solid #e2e8f0',
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{sql}
|
|
215
|
+
</SyntaxHighlighter>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* PreAggDetailsPanel - Detailed view of a selected pre-aggregation
|
|
225
|
+
*
|
|
226
|
+
* Shows comprehensive info when a preagg node is selected in the graph
|
|
227
|
+
*/
|
|
228
|
+
export function PreAggDetailsPanel({ preAgg, metricFormulas, onClose }) {
|
|
229
|
+
if (!preAgg) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Get friendly names
|
|
234
|
+
const sourceName = preAgg.parent_name || 'Pre-aggregation';
|
|
235
|
+
const shortName = sourceName.split('.').pop();
|
|
236
|
+
|
|
237
|
+
// Find metrics that use this preagg's components
|
|
238
|
+
const relatedMetrics =
|
|
239
|
+
metricFormulas?.filter(m =>
|
|
240
|
+
m.components?.some(comp =>
|
|
241
|
+
preAgg.components?.some(pc => pc.name === comp),
|
|
242
|
+
),
|
|
243
|
+
) || [];
|
|
244
|
+
|
|
245
|
+
const copyToClipboard = text => {
|
|
246
|
+
navigator.clipboard.writeText(text);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="details-panel">
|
|
251
|
+
{/* Header */}
|
|
252
|
+
<div className="details-header">
|
|
253
|
+
<div className="details-title-row">
|
|
254
|
+
<div className="details-type-badge preagg">Pre-aggregation</div>
|
|
255
|
+
<button
|
|
256
|
+
className="details-close"
|
|
257
|
+
onClick={onClose}
|
|
258
|
+
title="Close panel"
|
|
259
|
+
>
|
|
260
|
+
×
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
<h2 className="details-title">{shortName}</h2>
|
|
264
|
+
<p className="details-full-name">{sourceName}</p>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Grain Section */}
|
|
268
|
+
<div className="details-section">
|
|
269
|
+
<h3 className="section-title">
|
|
270
|
+
<span className="section-icon">⊞</span>
|
|
271
|
+
Grain (GROUP BY)
|
|
272
|
+
</h3>
|
|
273
|
+
<div className="grain-pills">
|
|
274
|
+
{preAgg.grain?.length > 0 ? (
|
|
275
|
+
preAgg.grain.map(g => (
|
|
276
|
+
<code key={g} className="grain-pill">
|
|
277
|
+
{g}
|
|
278
|
+
</code>
|
|
279
|
+
))
|
|
280
|
+
) : (
|
|
281
|
+
<span className="empty-text">No grain columns</span>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* Metrics Using This */}
|
|
287
|
+
<div className="details-section">
|
|
288
|
+
<h3 className="section-title">
|
|
289
|
+
<span className="section-icon">◈</span>
|
|
290
|
+
Metrics Using This
|
|
291
|
+
</h3>
|
|
292
|
+
<div className="metrics-list">
|
|
293
|
+
{relatedMetrics.length > 0 ? (
|
|
294
|
+
relatedMetrics.map((m, i) => (
|
|
295
|
+
<div key={i} className="related-metric">
|
|
296
|
+
<span className="metric-name">{m.short_name}</span>
|
|
297
|
+
{m.is_derived && <span className="derived-badge">Derived</span>}
|
|
298
|
+
</div>
|
|
299
|
+
))
|
|
300
|
+
) : (
|
|
301
|
+
<span className="empty-text">No metrics found</span>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Components Table */}
|
|
307
|
+
<div className="details-section details-section-full">
|
|
308
|
+
<h3 className="section-title">
|
|
309
|
+
<span className="section-icon">⚙</span>
|
|
310
|
+
Components ({preAgg.components?.length || 0})
|
|
311
|
+
</h3>
|
|
312
|
+
<div className="components-table-wrapper">
|
|
313
|
+
<table className="details-table">
|
|
314
|
+
<thead>
|
|
315
|
+
<tr>
|
|
316
|
+
<th>Name</th>
|
|
317
|
+
<th>Expression</th>
|
|
318
|
+
<th>Agg</th>
|
|
319
|
+
<th>Re-agg</th>
|
|
320
|
+
</tr>
|
|
321
|
+
</thead>
|
|
322
|
+
<tbody>
|
|
323
|
+
{preAgg.components?.map((comp, i) => (
|
|
324
|
+
<tr key={comp.name || i}>
|
|
325
|
+
<td className="comp-name-cell">
|
|
326
|
+
<code>{comp.name}</code>
|
|
327
|
+
</td>
|
|
328
|
+
<td className="comp-expr-cell">
|
|
329
|
+
<code>{comp.expression}</code>
|
|
330
|
+
</td>
|
|
331
|
+
<td className="comp-agg-cell">
|
|
332
|
+
<span className="agg-func">{comp.aggregation || '—'}</span>
|
|
333
|
+
</td>
|
|
334
|
+
<td className="comp-merge-cell">
|
|
335
|
+
<span className="merge-func">{comp.merge || '—'}</span>
|
|
336
|
+
</td>
|
|
337
|
+
</tr>
|
|
338
|
+
))}
|
|
339
|
+
</tbody>
|
|
340
|
+
</table>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* SQL Section */}
|
|
345
|
+
{preAgg.sql && (
|
|
346
|
+
<div className="details-section details-section-full details-sql-section">
|
|
347
|
+
<div className="section-header-row">
|
|
348
|
+
<h3 className="section-title">
|
|
349
|
+
<span className="section-icon">⌘</span>
|
|
350
|
+
Pre-Aggregation SQL
|
|
351
|
+
</h3>
|
|
352
|
+
<button
|
|
353
|
+
className="copy-sql-btn"
|
|
354
|
+
onClick={() => copyToClipboard(preAgg.sql)}
|
|
355
|
+
type="button"
|
|
356
|
+
>
|
|
357
|
+
Copy SQL
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="sql-code-wrapper">
|
|
361
|
+
<SyntaxHighlighter
|
|
362
|
+
language="sql"
|
|
363
|
+
style={atomOneLight}
|
|
364
|
+
customStyle={{
|
|
365
|
+
margin: 0,
|
|
366
|
+
borderRadius: '6px',
|
|
367
|
+
fontSize: '11px',
|
|
368
|
+
background: '#f8fafc',
|
|
369
|
+
border: '1px solid #e2e8f0',
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
{preAgg.sql}
|
|
373
|
+
</SyntaxHighlighter>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* MetricDetailsPanel - Detailed view of a selected metric
|
|
383
|
+
*/
|
|
384
|
+
export function MetricDetailsPanel({ metric, grainGroups, onClose }) {
|
|
385
|
+
if (!metric) return null;
|
|
386
|
+
|
|
387
|
+
// Find preaggs that this metric depends on
|
|
388
|
+
const relatedPreaggs =
|
|
389
|
+
grainGroups?.filter(gg =>
|
|
390
|
+
metric.components?.some(comp =>
|
|
391
|
+
gg.components?.some(pc => pc.name === comp),
|
|
392
|
+
),
|
|
393
|
+
) || [];
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="details-panel">
|
|
397
|
+
{/* Header */}
|
|
398
|
+
<div className="details-header">
|
|
399
|
+
<div className="details-title-row">
|
|
400
|
+
<div
|
|
401
|
+
className={`details-type-badge ${
|
|
402
|
+
metric.is_derived ? 'derived' : 'metric'
|
|
403
|
+
}`}
|
|
404
|
+
>
|
|
405
|
+
{metric.is_derived ? 'Derived Metric' : 'Metric'}
|
|
406
|
+
</div>
|
|
407
|
+
<button
|
|
408
|
+
className="details-close"
|
|
409
|
+
onClick={onClose}
|
|
410
|
+
title="Close panel"
|
|
411
|
+
>
|
|
412
|
+
×
|
|
413
|
+
</button>
|
|
414
|
+
</div>
|
|
415
|
+
<h2 className="details-title">{metric.short_name}</h2>
|
|
416
|
+
<p className="details-full-name">{metric.name}</p>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Formula */}
|
|
420
|
+
<div className="details-section">
|
|
421
|
+
<h3 className="section-title">
|
|
422
|
+
<span className="section-icon">∑</span>
|
|
423
|
+
Combiner Formula
|
|
424
|
+
</h3>
|
|
425
|
+
<div className="formula-display">
|
|
426
|
+
<code>{metric.combiner}</code>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Components Used */}
|
|
431
|
+
<div className="details-section">
|
|
432
|
+
<h3 className="section-title">
|
|
433
|
+
<span className="section-icon">⚙</span>
|
|
434
|
+
Components Used
|
|
435
|
+
</h3>
|
|
436
|
+
<div className="component-tags">
|
|
437
|
+
{metric.components?.map((comp, i) => (
|
|
438
|
+
<span key={i} className="component-tag">
|
|
439
|
+
{comp}
|
|
440
|
+
</span>
|
|
441
|
+
))}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
{/* Source Pre-aggregations */}
|
|
446
|
+
<div className="details-section">
|
|
447
|
+
<h3 className="section-title">
|
|
448
|
+
<span className="section-icon">◫</span>
|
|
449
|
+
Source Pre-aggregations
|
|
450
|
+
</h3>
|
|
451
|
+
<div className="preagg-sources">
|
|
452
|
+
{relatedPreaggs.length > 0 ? (
|
|
453
|
+
relatedPreaggs.map((gg, i) => (
|
|
454
|
+
<div key={i} className="preagg-source-item">
|
|
455
|
+
<span className="preagg-source-name">
|
|
456
|
+
{gg.parent_name?.split('.').pop()}
|
|
457
|
+
</span>
|
|
458
|
+
<span className="preagg-source-full">{gg.parent_name}</span>
|
|
459
|
+
</div>
|
|
460
|
+
))
|
|
461
|
+
) : (
|
|
462
|
+
<span className="empty-text">No source found</span>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export default PreAggDetailsPanel;
|