datajunction-ui 0.0.23 → 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.
Files changed (25) hide show
  1. package/package.json +11 -4
  2. package/src/app/index.tsx +6 -0
  3. package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
  4. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
  5. package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
  6. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
  7. package/src/app/pages/NamespacePage/index.jsx +489 -62
  8. package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
  9. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
  10. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
  11. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
  12. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
  13. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
  14. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
  15. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
  16. package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
  17. package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
  18. package/src/app/pages/Root/index.tsx +5 -0
  19. package/src/app/services/DJService.js +61 -2
  20. package/src/styles/index.css +2 -2
  21. package/src/app/icons/FilterIcon.jsx +0 -7
  22. package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
  23. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
  24. package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
  25. 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;