datajunction-ui 0.0.44 → 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.
@@ -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
- if (cubeMat) {
180
- setCubeMaterialization(cubeMat);
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 state since user is manually changing selection
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
- if (cubeMat) {
391
- setCubeMaterialization(cubeMat);
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 state since user is manually changing selection
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
- {/* Center: Graph */}
1112
- <main className="planner-graph">
1113
- {loading ? (
1114
- <div className="graph-loading">
1115
- <div className="loading-spinner" />
1116
- <span>Building data flow...</span>
1117
- </div>
1118
- ) : measuresResult ? (
1119
- <>
1120
- <div className="graph-header">
1121
- <span className="graph-stats">
1122
- {measuresResult.grain_groups?.length || 0} pre-aggregations →{' '}
1123
- {measuresResult.metric_formulas?.length || 0} metrics
1124
- </span>
1125
- </div>
1126
- {/* Suspense boundary for lazy-loaded ReactFlow graph */}
1127
- <Suspense
1128
- fallback={
1129
- <div className="graph-loading">
1130
- <div className="loading-spinner" />
1131
- <span>Loading graph...</span>
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
- <MetricFlowGraph
1136
- grainGroups={measuresResult.grain_groups}
1137
- metricFormulas={measuresResult.metric_formulas}
1138
- selectedNode={selectedNode}
1139
- onNodeSelect={handleNodeSelect}
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
- </Suspense>
1142
- </>
1143
- ) : (
1144
- <div className="graph-empty">
1145
- <div className="empty-icon">⊞</div>
1146
- <h3>Select Metrics & Dimensions</h3>
1147
- <p>
1148
- Choose metrics from the left panel, then select dimensions to
1149
- see how they decompose into pre-aggregations.
1150
- </p>
1151
- </div>
1152
- )}
1153
- </main>
1154
-
1155
- {/* Right: Details Panel */}
1156
- <aside className="planner-details">
1157
- {selectedNode?.type === 'preagg' ||
1158
- selectedNode?.type === 'component' ? (
1159
- <PreAggDetailsPanel
1160
- preAgg={
1161
- selectedNode?.type === 'component'
1162
- ? measuresResult?.grain_groups?.[
1163
- selectedNode.data?.grainGroupIndex
1164
- ]
1165
- : selectedNode.data
1166
- }
1167
- metricFormulas={measuresResult?.metric_formulas}
1168
- onClose={handleClosePanel}
1169
- highlightedComponent={
1170
- selectedNode?.type === 'component'
1171
- ? selectedNode.data?.name
1172
- : null
1173
- }
1174
- />
1175
- ) : selectedNode?.type === 'metric' ? (
1176
- <MetricDetailsPanel
1177
- metric={selectedNode.data}
1178
- grainGroups={measuresResult?.grain_groups}
1179
- onClose={handleClosePanel}
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
  );