datajunction-ui 0.0.43 → 0.0.45
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/NodePage/NodeInfoTab.jsx +38 -40
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +0 -133
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +9 -7
- package/src/app/pages/NodePage/index.jsx +12 -11
- 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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MetricDetailsPanel,
|
|
16
16
|
QueryOverviewPanel,
|
|
17
17
|
} from './PreAggDetailsPanel';
|
|
18
|
+
import ResultsView from './ResultsView';
|
|
18
19
|
import './styles.css';
|
|
19
20
|
|
|
20
21
|
// Lazy load the graph component - ReactFlow and dagre are heavy (~500KB)
|
|
@@ -59,6 +60,20 @@ export function QueryPlannerPage() {
|
|
|
59
60
|
const [dimensionsLoading, setDimensionsLoading] = useState(false);
|
|
60
61
|
const [error, setError] = useState(null);
|
|
61
62
|
|
|
63
|
+
// Query execution state
|
|
64
|
+
const [showResults, setShowResults] = useState(false);
|
|
65
|
+
const [queryResults, setQueryResults] = useState(null);
|
|
66
|
+
const [queryLoading, setQueryLoading] = useState(false);
|
|
67
|
+
const [queryError, setQueryError] = useState(null);
|
|
68
|
+
const [queryStartTime, setQueryStartTime] = useState(null);
|
|
69
|
+
const [queryElapsedTime, setQueryElapsedTime] = useState(null);
|
|
70
|
+
|
|
71
|
+
// Filters state
|
|
72
|
+
const [filters, setFilters] = useState([]);
|
|
73
|
+
|
|
74
|
+
// Cube availability state (for displaying freshness info)
|
|
75
|
+
const [cubeAvailability, setCubeAvailability] = useState(null);
|
|
76
|
+
|
|
62
77
|
// Node selection for details panel
|
|
63
78
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
64
79
|
|
|
@@ -176,12 +191,8 @@ export function QueryPlannerPage() {
|
|
|
176
191
|
|
|
177
192
|
// Materialization info is included in the GraphQL response
|
|
178
193
|
const cubeMat = cubeData.cubeMaterialization;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (cubeMat.workflowUrls?.length > 0) {
|
|
182
|
-
setWorkflowUrls(cubeMat.workflowUrls);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
194
|
+
setCubeMaterialization(cubeMat || null);
|
|
195
|
+
setWorkflowUrls(cubeMat?.workflowUrls || []);
|
|
185
196
|
} else {
|
|
186
197
|
console.error('Invalid cube data from URL:', cubeData);
|
|
187
198
|
}
|
|
@@ -354,13 +365,52 @@ export function QueryPlannerPage() {
|
|
|
354
365
|
fetchExistingPreaggs();
|
|
355
366
|
}, [measuresResult, djClient]);
|
|
356
367
|
|
|
368
|
+
// Auto-detect cube info when metricsResult has cube_name (backend found a matching cube)
|
|
369
|
+
// This effect manages cubeAvailability, cubeMaterialization, and workflowUrls based on
|
|
370
|
+
// either: 1) explicit user selection (loadedCubeName), or 2) auto-detected cube (metricsResult.cube_name)
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
const fetchCubeInfo = async () => {
|
|
373
|
+
// Determine which cube to use: explicit selection takes precedence over auto-detection
|
|
374
|
+
const cubeName = loadedCubeName || metricsResult?.cube_name;
|
|
375
|
+
|
|
376
|
+
if (!cubeName) {
|
|
377
|
+
// No cube - clear state
|
|
378
|
+
setCubeAvailability(null);
|
|
379
|
+
setCubeMaterialization(null);
|
|
380
|
+
setWorkflowUrls([]);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
// Fetch full cube info including materialization
|
|
386
|
+
const cubeData = await djClient.cubeForPlanner(cubeName);
|
|
387
|
+
setCubeAvailability(cubeData?.availability || null);
|
|
388
|
+
|
|
389
|
+
if (cubeData) {
|
|
390
|
+
const cubeMat = cubeData.cubeMaterialization;
|
|
391
|
+
setCubeMaterialization(cubeMat || null);
|
|
392
|
+
setWorkflowUrls(cubeMat?.workflowUrls || []);
|
|
393
|
+
} else {
|
|
394
|
+
setCubeMaterialization(null);
|
|
395
|
+
setWorkflowUrls([]);
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.error('Failed to fetch cube info:', err);
|
|
399
|
+
setCubeAvailability(null);
|
|
400
|
+
setCubeMaterialization(null);
|
|
401
|
+
setWorkflowUrls([]);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
fetchCubeInfo();
|
|
406
|
+
}, [metricsResult?.cube_name, loadedCubeName, djClient]);
|
|
407
|
+
|
|
357
408
|
const handleMetricsChange = useCallback(newMetrics => {
|
|
358
409
|
setSelectedMetrics(newMetrics);
|
|
359
410
|
setSelectedNode(null);
|
|
360
|
-
// Clear cube
|
|
411
|
+
// Clear loaded cube name to indicate user is manually changing selection
|
|
412
|
+
// (workflowUrls and cubeMaterialization will be updated by the effect when metricsResult changes)
|
|
361
413
|
setLoadedCubeName(null);
|
|
362
|
-
setWorkflowUrls([]);
|
|
363
|
-
setCubeMaterialization(null);
|
|
364
414
|
}, []);
|
|
365
415
|
|
|
366
416
|
// Load a cube preset - sets both metrics and dimensions from the cube definition
|
|
@@ -385,14 +435,11 @@ export function QueryPlannerPage() {
|
|
|
385
435
|
pendingDimensionsFromUrl.current = cubeDimensions;
|
|
386
436
|
setSelectedNode(null);
|
|
387
437
|
|
|
388
|
-
// Materialization info is included in the GraphQL response
|
|
438
|
+
// Materialization and availability info is included in the GraphQL response
|
|
439
|
+
setCubeAvailability(cubeData.availability || null);
|
|
389
440
|
const cubeMat = cubeData.cubeMaterialization;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (cubeMat.workflowUrls?.length > 0) {
|
|
393
|
-
setWorkflowUrls(cubeMat.workflowUrls);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
441
|
+
setCubeMaterialization(cubeMat || null);
|
|
442
|
+
setWorkflowUrls(cubeMat?.workflowUrls || []);
|
|
396
443
|
} else {
|
|
397
444
|
console.error('Invalid cube data received:', cubeData);
|
|
398
445
|
}
|
|
@@ -415,10 +462,9 @@ export function QueryPlannerPage() {
|
|
|
415
462
|
const handleDimensionsChange = useCallback(newDimensions => {
|
|
416
463
|
setSelectedDimensions(newDimensions);
|
|
417
464
|
setSelectedNode(null);
|
|
418
|
-
// Clear cube
|
|
465
|
+
// Clear loaded cube name to indicate user is manually changing selection
|
|
466
|
+
// (workflowUrls and cubeMaterialization will be updated by the effect when metricsResult changes)
|
|
419
467
|
setLoadedCubeName(null);
|
|
420
|
-
setWorkflowUrls([]);
|
|
421
|
-
setCubeMaterialization(null);
|
|
422
468
|
}, []);
|
|
423
469
|
|
|
424
470
|
const handleNodeSelect = useCallback(node => {
|
|
@@ -1078,6 +1124,51 @@ export function QueryPlannerPage() {
|
|
|
1078
1124
|
[djClient],
|
|
1079
1125
|
);
|
|
1080
1126
|
|
|
1127
|
+
// Run query and fetch results
|
|
1128
|
+
const handleRunQuery = useCallback(async () => {
|
|
1129
|
+
if (selectedMetrics.length === 0 || selectedDimensions.length === 0) {
|
|
1130
|
+
setQueryError('Select at least one metric and one dimension');
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
setQueryLoading(true);
|
|
1135
|
+
setQueryError(null);
|
|
1136
|
+
setQueryResults(null);
|
|
1137
|
+
setShowResults(true);
|
|
1138
|
+
const startTime = Date.now();
|
|
1139
|
+
setQueryStartTime(startTime);
|
|
1140
|
+
setQueryElapsedTime(null);
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
// Fetch data from /data/ endpoint
|
|
1144
|
+
const results = await djClient.data(
|
|
1145
|
+
selectedMetrics,
|
|
1146
|
+
selectedDimensions,
|
|
1147
|
+
filters,
|
|
1148
|
+
);
|
|
1149
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
1150
|
+
setQueryElapsedTime(elapsed);
|
|
1151
|
+
setQueryResults(results);
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
console.error('Query failed:', err);
|
|
1154
|
+
setQueryError(err.message || 'Failed to execute query');
|
|
1155
|
+
} finally {
|
|
1156
|
+
setQueryLoading(false);
|
|
1157
|
+
}
|
|
1158
|
+
}, [djClient, selectedMetrics, selectedDimensions, filters]);
|
|
1159
|
+
|
|
1160
|
+
// Handle back to plan view
|
|
1161
|
+
const handleBackToPlan = useCallback(() => {
|
|
1162
|
+
setShowResults(false);
|
|
1163
|
+
setQueryResults(null);
|
|
1164
|
+
setQueryError(null);
|
|
1165
|
+
}, []);
|
|
1166
|
+
|
|
1167
|
+
// Handle filter changes
|
|
1168
|
+
const handleFiltersChange = useCallback(newFilters => {
|
|
1169
|
+
setFilters(newFilters);
|
|
1170
|
+
}, []);
|
|
1171
|
+
|
|
1081
1172
|
return (
|
|
1082
1173
|
<div className="planner-page">
|
|
1083
1174
|
{/* Header */}
|
|
@@ -1105,109 +1196,138 @@ export function QueryPlannerPage() {
|
|
|
1105
1196
|
onLoadCubePreset={handleLoadCubePreset}
|
|
1106
1197
|
loadedCubeName={loadedCubeName}
|
|
1107
1198
|
onClearSelection={handleClearSelection}
|
|
1199
|
+
filters={filters}
|
|
1200
|
+
onFiltersChange={handleFiltersChange}
|
|
1201
|
+
onRunQuery={handleRunQuery}
|
|
1202
|
+
canRunQuery={
|
|
1203
|
+
selectedMetrics.length > 0 && selectedDimensions.length > 0
|
|
1204
|
+
}
|
|
1205
|
+
queryLoading={queryLoading}
|
|
1108
1206
|
/>
|
|
1109
1207
|
</aside>
|
|
1110
1208
|
|
|
1111
|
-
{/*
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1209
|
+
{/* Main Content Area - Either Results or Graph+Details */}
|
|
1210
|
+
{showResults ? (
|
|
1211
|
+
<ResultsView
|
|
1212
|
+
sql={metricsResult?.sql}
|
|
1213
|
+
results={queryResults}
|
|
1214
|
+
loading={queryLoading}
|
|
1215
|
+
error={queryError}
|
|
1216
|
+
elapsedTime={queryElapsedTime}
|
|
1217
|
+
onBackToPlan={handleBackToPlan}
|
|
1218
|
+
selectedMetrics={selectedMetrics}
|
|
1219
|
+
selectedDimensions={selectedDimensions}
|
|
1220
|
+
filters={filters}
|
|
1221
|
+
dialect={metricsResult?.dialect}
|
|
1222
|
+
cubeName={metricsResult?.cube_name}
|
|
1223
|
+
availability={cubeAvailability}
|
|
1224
|
+
/>
|
|
1225
|
+
) : (
|
|
1226
|
+
<>
|
|
1227
|
+
{/* Center: Graph */}
|
|
1228
|
+
<main className="planner-graph">
|
|
1229
|
+
{loading ? (
|
|
1230
|
+
<div className="graph-loading">
|
|
1231
|
+
<div className="loading-spinner" />
|
|
1232
|
+
<span>Building data flow...</span>
|
|
1233
|
+
</div>
|
|
1234
|
+
) : measuresResult ? (
|
|
1235
|
+
<>
|
|
1236
|
+
<div className="graph-header">
|
|
1237
|
+
<span className="graph-stats">
|
|
1238
|
+
{measuresResult.grain_groups?.length || 0}{' '}
|
|
1239
|
+
pre-aggregations →{' '}
|
|
1240
|
+
{measuresResult.metric_formulas?.length || 0} metrics
|
|
1241
|
+
</span>
|
|
1132
1242
|
</div>
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1243
|
+
{/* Suspense boundary for lazy-loaded ReactFlow graph */}
|
|
1244
|
+
<Suspense
|
|
1245
|
+
fallback={
|
|
1246
|
+
<div className="graph-loading">
|
|
1247
|
+
<div className="loading-spinner" />
|
|
1248
|
+
<span>Loading graph...</span>
|
|
1249
|
+
</div>
|
|
1250
|
+
}
|
|
1251
|
+
>
|
|
1252
|
+
<MetricFlowGraph
|
|
1253
|
+
grainGroups={measuresResult.grain_groups}
|
|
1254
|
+
metricFormulas={measuresResult.metric_formulas}
|
|
1255
|
+
selectedNode={selectedNode}
|
|
1256
|
+
onNodeSelect={handleNodeSelect}
|
|
1257
|
+
/>
|
|
1258
|
+
</Suspense>
|
|
1259
|
+
</>
|
|
1260
|
+
) : (
|
|
1261
|
+
<div className="graph-empty">
|
|
1262
|
+
<div className="empty-icon">⊞</div>
|
|
1263
|
+
<h3>Select Metrics & Dimensions</h3>
|
|
1264
|
+
<p>
|
|
1265
|
+
Choose metrics from the left panel, then select dimensions
|
|
1266
|
+
to see how they decompose into pre-aggregations.
|
|
1267
|
+
</p>
|
|
1268
|
+
</div>
|
|
1269
|
+
)}
|
|
1270
|
+
</main>
|
|
1271
|
+
|
|
1272
|
+
{/* Right: Details Panel */}
|
|
1273
|
+
<aside className="planner-details">
|
|
1274
|
+
{selectedNode?.type === 'preagg' ||
|
|
1275
|
+
selectedNode?.type === 'component' ? (
|
|
1276
|
+
<PreAggDetailsPanel
|
|
1277
|
+
preAgg={
|
|
1278
|
+
selectedNode?.type === 'component'
|
|
1279
|
+
? measuresResult?.grain_groups?.[
|
|
1280
|
+
selectedNode.data?.grainGroupIndex
|
|
1281
|
+
]
|
|
1282
|
+
: selectedNode.data
|
|
1283
|
+
}
|
|
1284
|
+
metricFormulas={measuresResult?.metric_formulas}
|
|
1285
|
+
onClose={handleClosePanel}
|
|
1286
|
+
highlightedComponent={
|
|
1287
|
+
selectedNode?.type === 'component'
|
|
1288
|
+
? selectedNode.data?.name
|
|
1289
|
+
: null
|
|
1290
|
+
}
|
|
1140
1291
|
/>
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
/>
|
|
1181
|
-
) : (
|
|
1182
|
-
<QueryOverviewPanel
|
|
1183
|
-
measuresResult={measuresResult}
|
|
1184
|
-
metricsResult={metricsResult}
|
|
1185
|
-
selectedMetrics={selectedMetrics}
|
|
1186
|
-
selectedDimensions={selectedDimensions}
|
|
1187
|
-
plannedPreaggs={plannedPreaggs}
|
|
1188
|
-
onPlanMaterialization={handlePlanMaterialization}
|
|
1189
|
-
onUpdateConfig={handleUpdateConfig}
|
|
1190
|
-
onCreateWorkflow={handleCreateWorkflow}
|
|
1191
|
-
onRunBackfill={handleRunBackfill}
|
|
1192
|
-
onRunAdhoc={handleRunAdhoc}
|
|
1193
|
-
onFetchRawSql={handleFetchRawSql}
|
|
1194
|
-
onSetPartition={handleSetPartition}
|
|
1195
|
-
onRefreshMeasures={handleRefreshMeasures}
|
|
1196
|
-
onFetchNodePartitions={handleFetchNodePartitions}
|
|
1197
|
-
materializationError={materializationError}
|
|
1198
|
-
onClearError={() => setMaterializationError(null)}
|
|
1199
|
-
workflowUrls={workflowUrls}
|
|
1200
|
-
onClearWorkflowUrls={() => setWorkflowUrls([])}
|
|
1201
|
-
loadedCubeName={loadedCubeName}
|
|
1202
|
-
cubeMaterialization={cubeMaterialization}
|
|
1203
|
-
onUpdateCubeConfig={handleUpdateCubeConfig}
|
|
1204
|
-
onRefreshCubeWorkflow={handleRefreshCubeWorkflow}
|
|
1205
|
-
onRunCubeBackfill={handleRunCubeBackfill}
|
|
1206
|
-
onDeactivatePreaggWorkflow={handleDeactivatePreaggWorkflow}
|
|
1207
|
-
onDeactivateCubeWorkflow={handleDeactivateCubeWorkflow}
|
|
1208
|
-
/>
|
|
1209
|
-
)}
|
|
1210
|
-
</aside>
|
|
1292
|
+
) : selectedNode?.type === 'metric' ? (
|
|
1293
|
+
<MetricDetailsPanel
|
|
1294
|
+
metric={selectedNode.data}
|
|
1295
|
+
grainGroups={measuresResult?.grain_groups}
|
|
1296
|
+
onClose={handleClosePanel}
|
|
1297
|
+
/>
|
|
1298
|
+
) : (
|
|
1299
|
+
<QueryOverviewPanel
|
|
1300
|
+
measuresResult={measuresResult}
|
|
1301
|
+
metricsResult={metricsResult}
|
|
1302
|
+
selectedMetrics={selectedMetrics}
|
|
1303
|
+
selectedDimensions={selectedDimensions}
|
|
1304
|
+
plannedPreaggs={plannedPreaggs}
|
|
1305
|
+
onPlanMaterialization={handlePlanMaterialization}
|
|
1306
|
+
onUpdateConfig={handleUpdateConfig}
|
|
1307
|
+
onCreateWorkflow={handleCreateWorkflow}
|
|
1308
|
+
onRunBackfill={handleRunBackfill}
|
|
1309
|
+
onRunAdhoc={handleRunAdhoc}
|
|
1310
|
+
onFetchRawSql={handleFetchRawSql}
|
|
1311
|
+
onSetPartition={handleSetPartition}
|
|
1312
|
+
onRefreshMeasures={handleRefreshMeasures}
|
|
1313
|
+
onFetchNodePartitions={handleFetchNodePartitions}
|
|
1314
|
+
materializationError={materializationError}
|
|
1315
|
+
onClearError={() => setMaterializationError(null)}
|
|
1316
|
+
workflowUrls={workflowUrls}
|
|
1317
|
+
onClearWorkflowUrls={() => setWorkflowUrls([])}
|
|
1318
|
+
loadedCubeName={loadedCubeName}
|
|
1319
|
+
cubeMaterialization={cubeMaterialization}
|
|
1320
|
+
cubeAvailability={cubeAvailability}
|
|
1321
|
+
onUpdateCubeConfig={handleUpdateCubeConfig}
|
|
1322
|
+
onRefreshCubeWorkflow={handleRefreshCubeWorkflow}
|
|
1323
|
+
onRunCubeBackfill={handleRunCubeBackfill}
|
|
1324
|
+
onDeactivatePreaggWorkflow={handleDeactivatePreaggWorkflow}
|
|
1325
|
+
onDeactivateCubeWorkflow={handleDeactivateCubeWorkflow}
|
|
1326
|
+
/>
|
|
1327
|
+
)}
|
|
1328
|
+
</aside>
|
|
1329
|
+
</>
|
|
1330
|
+
)}
|
|
1211
1331
|
</div>
|
|
1212
1332
|
</div>
|
|
1213
1333
|
);
|