datajunction-ui 0.0.94 → 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/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +50 -0
- 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 +60 -17
- package/src/app/services/__tests__/DJService.test.jsx +13 -15
|
@@ -857,6 +857,13 @@
|
|
|
857
857
|
gap: 2px;
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
+
.dimension-full-name {
|
|
861
|
+
font-size: 10px;
|
|
862
|
+
color: var(--planner-text-muted);
|
|
863
|
+
font-family: var(--font-display);
|
|
864
|
+
word-break: break-all;
|
|
865
|
+
}
|
|
866
|
+
|
|
860
867
|
.dimension-path {
|
|
861
868
|
font-size: 10px;
|
|
862
869
|
color: var(--planner-text-dim);
|
|
@@ -3557,6 +3564,55 @@ a.action-btn {
|
|
|
3557
3564
|
cursor: not-allowed;
|
|
3558
3565
|
}
|
|
3559
3566
|
|
|
3567
|
+
/* =================================
|
|
3568
|
+
Engine Selection
|
|
3569
|
+
================================= */
|
|
3570
|
+
|
|
3571
|
+
.engine-section {
|
|
3572
|
+
display: flex;
|
|
3573
|
+
align-items: center;
|
|
3574
|
+
gap: 8px;
|
|
3575
|
+
padding: 10px 12px;
|
|
3576
|
+
border-top: 1px solid var(--planner-border);
|
|
3577
|
+
background: var(--planner-bg);
|
|
3578
|
+
flex-shrink: 0;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
.engine-label {
|
|
3582
|
+
font-size: 12px;
|
|
3583
|
+
font-weight: 500;
|
|
3584
|
+
color: var(--text-secondary);
|
|
3585
|
+
white-space: nowrap;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
.engine-pills {
|
|
3589
|
+
display: flex;
|
|
3590
|
+
gap: 4px;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
.engine-pill {
|
|
3594
|
+
padding: 4px 10px;
|
|
3595
|
+
border: 1px solid var(--planner-border);
|
|
3596
|
+
border-radius: 12px;
|
|
3597
|
+
background: transparent;
|
|
3598
|
+
font-size: 12px;
|
|
3599
|
+
color: var(--text-secondary);
|
|
3600
|
+
cursor: pointer;
|
|
3601
|
+
transition: all 0.12s ease;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
.engine-pill:hover {
|
|
3605
|
+
border-color: #059669;
|
|
3606
|
+
color: #059669;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
.engine-pill.active {
|
|
3610
|
+
background: #059669;
|
|
3611
|
+
border-color: #059669;
|
|
3612
|
+
color: white;
|
|
3613
|
+
font-weight: 500;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3560
3616
|
/* =================================
|
|
3561
3617
|
Run Query Section
|
|
3562
3618
|
================================= */
|
|
@@ -3950,8 +4006,9 @@ a.action-btn {
|
|
|
3950
4006
|
.results-table th {
|
|
3951
4007
|
position: sticky;
|
|
3952
4008
|
top: 0;
|
|
4009
|
+
z-index: 1;
|
|
3953
4010
|
padding: 10px 12px;
|
|
3954
|
-
background:
|
|
4011
|
+
background: #f8fafc;
|
|
3955
4012
|
color: var(--planner-text);
|
|
3956
4013
|
font-weight: 600;
|
|
3957
4014
|
text-align: left;
|
|
@@ -3962,7 +4019,7 @@ a.action-btn {
|
|
|
3962
4019
|
}
|
|
3963
4020
|
|
|
3964
4021
|
.results-table th:hover {
|
|
3965
|
-
background:
|
|
4022
|
+
background: #f1f5f9;
|
|
3966
4023
|
}
|
|
3967
4024
|
|
|
3968
4025
|
.results-table th.sorted {
|
|
@@ -4051,3 +4108,155 @@ a.action-btn {
|
|
|
4051
4108
|
.filters-list .filter-chip {
|
|
4052
4109
|
padding: 4px 10px;
|
|
4053
4110
|
}
|
|
4111
|
+
|
|
4112
|
+
/* =================================
|
|
4113
|
+
Results View - Tab bar & Chart
|
|
4114
|
+
================================= */
|
|
4115
|
+
|
|
4116
|
+
.results-tabs-bar {
|
|
4117
|
+
display: flex;
|
|
4118
|
+
align-items: center;
|
|
4119
|
+
gap: 12px;
|
|
4120
|
+
padding: 8px 16px 0;
|
|
4121
|
+
border-bottom: 1px solid var(--planner-border);
|
|
4122
|
+
background: var(--planner-surface);
|
|
4123
|
+
flex-shrink: 0;
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
.results-tabs {
|
|
4127
|
+
display: flex;
|
|
4128
|
+
gap: 2px;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
.results-tab {
|
|
4132
|
+
padding: 6px 14px;
|
|
4133
|
+
font-size: 12px;
|
|
4134
|
+
font-weight: 500;
|
|
4135
|
+
font-family: var(--font-body);
|
|
4136
|
+
background: none;
|
|
4137
|
+
border: none;
|
|
4138
|
+
border-bottom: 2px solid transparent;
|
|
4139
|
+
color: var(--planner-text-muted);
|
|
4140
|
+
cursor: pointer;
|
|
4141
|
+
transition: color 0.15s, border-color 0.15s;
|
|
4142
|
+
margin-bottom: -1px;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
.results-tab:hover:not(.disabled) {
|
|
4146
|
+
color: var(--planner-text);
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
.results-tab.active {
|
|
4150
|
+
color: var(--accent-primary);
|
|
4151
|
+
border-bottom-color: var(--accent-primary);
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
.results-tab.disabled {
|
|
4155
|
+
opacity: 0.4;
|
|
4156
|
+
cursor: not-allowed;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
.results-tabs-meta {
|
|
4160
|
+
display: flex;
|
|
4161
|
+
align-items: center;
|
|
4162
|
+
gap: 8px;
|
|
4163
|
+
margin-left: auto;
|
|
4164
|
+
padding-bottom: 6px;
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/* Chart wrapper - fills remaining height */
|
|
4168
|
+
.results-chart-wrapper {
|
|
4169
|
+
flex: 1;
|
|
4170
|
+
overflow: hidden;
|
|
4171
|
+
min-height: 0;
|
|
4172
|
+
padding: 16px 8px 8px;
|
|
4173
|
+
display: flex;
|
|
4174
|
+
align-items: stretch;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
/* KPI cards */
|
|
4178
|
+
.kpi-cards {
|
|
4179
|
+
display: flex;
|
|
4180
|
+
flex-wrap: wrap;
|
|
4181
|
+
gap: 16px;
|
|
4182
|
+
align-items: flex-start;
|
|
4183
|
+
padding: 8px;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
.kpi-card {
|
|
4187
|
+
background: var(--planner-surface);
|
|
4188
|
+
border: 1px solid var(--planner-border);
|
|
4189
|
+
border-radius: var(--radius-lg);
|
|
4190
|
+
padding: 20px 28px;
|
|
4191
|
+
min-width: 160px;
|
|
4192
|
+
text-align: center;
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
.kpi-label {
|
|
4196
|
+
font-size: 11px;
|
|
4197
|
+
font-weight: 600;
|
|
4198
|
+
color: var(--planner-text-muted);
|
|
4199
|
+
text-transform: uppercase;
|
|
4200
|
+
letter-spacing: 0.04em;
|
|
4201
|
+
margin-bottom: 8px;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
.kpi-value {
|
|
4205
|
+
font-size: 28px;
|
|
4206
|
+
font-weight: 700;
|
|
4207
|
+
color: var(--planner-text);
|
|
4208
|
+
font-family: var(--font-display);
|
|
4209
|
+
line-height: 1.1;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
.kpi-type {
|
|
4213
|
+
font-size: 10px;
|
|
4214
|
+
color: var(--planner-text-dim);
|
|
4215
|
+
margin-top: 6px;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
.chart-no-data {
|
|
4219
|
+
display: flex;
|
|
4220
|
+
align-items: center;
|
|
4221
|
+
justify-content: center;
|
|
4222
|
+
width: 100%;
|
|
4223
|
+
color: var(--planner-text-muted);
|
|
4224
|
+
font-size: 13px;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
/* =================================
|
|
4228
|
+
Small Multiples
|
|
4229
|
+
================================= */
|
|
4230
|
+
|
|
4231
|
+
.small-multiples {
|
|
4232
|
+
display: flex;
|
|
4233
|
+
flex-direction: column;
|
|
4234
|
+
gap: 0;
|
|
4235
|
+
width: 100%;
|
|
4236
|
+
height: 100%;
|
|
4237
|
+
overflow-y: auto;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
.small-multiple {
|
|
4241
|
+
flex-shrink: 0;
|
|
4242
|
+
height: 220px;
|
|
4243
|
+
padding: 0 0 8px 0;
|
|
4244
|
+
border-bottom: 1px solid var(--planner-border);
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
.small-multiple:last-child {
|
|
4248
|
+
border-bottom: none;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
.small-multiple-label {
|
|
4252
|
+
font-size: 11px;
|
|
4253
|
+
font-weight: 600;
|
|
4254
|
+
color: var(--planner-text-muted);
|
|
4255
|
+
padding: 8px 0 0 8px;
|
|
4256
|
+
text-transform: uppercase;
|
|
4257
|
+
letter-spacing: 0.04em;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
.small-multiple-chart {
|
|
4261
|
+
height: 188px;
|
|
4262
|
+
}
|
|
@@ -41,9 +41,8 @@ describe('<Root />', () => {
|
|
|
41
41
|
expect(document.title).toEqual('DataJunction');
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
// Check navigation links exist
|
|
45
|
-
expect(screen.
|
|
46
|
-
expect(screen.getByText('Query Planner')).toBeInTheDocument();
|
|
44
|
+
// Check navigation links exist (two "Explore" links: catalog browser + planner)
|
|
45
|
+
expect(screen.getAllByText('Explore')).toHaveLength(2);
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
it('renders Docs dropdown', async () => {
|
|
@@ -13,6 +13,9 @@ const DJ_GQL = process.env.REACT_APP_DJ_GQL
|
|
|
13
13
|
// Export the base URL for components that need direct access
|
|
14
14
|
export const getDJUrl = () => DJ_URL;
|
|
15
15
|
|
|
16
|
+
const QUERY_END_STATES = ['FINISHED', 'CANCELED', 'FAILED'];
|
|
17
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
|
|
16
19
|
export const DataJunctionAPI = {
|
|
17
20
|
listNodesForLanding: async function (
|
|
18
21
|
namespace,
|
|
@@ -1217,33 +1220,35 @@ export const DataJunctionAPI = {
|
|
|
1217
1220
|
metricsV3: async function (
|
|
1218
1221
|
metricSelection,
|
|
1219
1222
|
dimensionSelection,
|
|
1220
|
-
filters =
|
|
1223
|
+
filters = [],
|
|
1221
1224
|
useMaterialized = true,
|
|
1225
|
+
dialect = null,
|
|
1222
1226
|
) {
|
|
1223
1227
|
const params = new URLSearchParams();
|
|
1224
1228
|
metricSelection.forEach(metric => params.append('metrics', metric));
|
|
1225
1229
|
dimensionSelection.forEach(dimension =>
|
|
1226
1230
|
params.append('dimensions', dimension),
|
|
1227
1231
|
);
|
|
1228
|
-
if (filters) {
|
|
1229
|
-
params.append('filters',
|
|
1230
|
-
}
|
|
1231
|
-
if (useMaterialized) {
|
|
1232
|
-
params.append('use_materialized', 'true');
|
|
1233
|
-
params.append('dialect', 'druid');
|
|
1234
|
-
} else {
|
|
1235
|
-
params.append('use_materialized', 'false');
|
|
1236
|
-
params.append('dialect', 'spark');
|
|
1232
|
+
if (filters && filters.length > 0) {
|
|
1233
|
+
filters.forEach(f => params.append('filters', f));
|
|
1237
1234
|
}
|
|
1235
|
+
const resolvedDialect = dialect || (useMaterialized ? 'druid' : 'trino');
|
|
1236
|
+
params.append('use_materialized', useMaterialized ? 'true' : 'false');
|
|
1237
|
+
params.append('dialect', resolvedDialect);
|
|
1238
1238
|
return await (
|
|
1239
1239
|
await fetch(`${DJ_URL}/sql/metrics/v3/?${params}`, {
|
|
1240
1240
|
credentials: 'include',
|
|
1241
|
-
params: params,
|
|
1242
1241
|
})
|
|
1243
1242
|
).json();
|
|
1244
1243
|
},
|
|
1245
1244
|
|
|
1246
|
-
data: async function (
|
|
1245
|
+
data: async function (
|
|
1246
|
+
metricSelection,
|
|
1247
|
+
dimensionSelection,
|
|
1248
|
+
filters = [],
|
|
1249
|
+
dialect = null,
|
|
1250
|
+
onProgress = null,
|
|
1251
|
+
) {
|
|
1247
1252
|
const params = new URLSearchParams();
|
|
1248
1253
|
metricSelection.map(metric => params.append('metrics', metric));
|
|
1249
1254
|
dimensionSelection.map(dimension => params.append('dimensions', dimension));
|
|
@@ -1251,18 +1256,56 @@ export const DataJunctionAPI = {
|
|
|
1251
1256
|
filters.forEach(f => params.append('filters', f));
|
|
1252
1257
|
}
|
|
1253
1258
|
params.append('limit', '10000');
|
|
1254
|
-
|
|
1259
|
+
params.append('async_', 'true');
|
|
1260
|
+
params.append('dialect', dialect || 'trino');
|
|
1261
|
+
|
|
1262
|
+
let pollInterval = 1000;
|
|
1263
|
+
|
|
1264
|
+
// Submit the query once
|
|
1265
|
+
const submitResponse = await fetch(`${DJ_URL}/data/?${params}`, {
|
|
1255
1266
|
credentials: 'include',
|
|
1256
1267
|
});
|
|
1257
|
-
if (!
|
|
1258
|
-
const errorData = await
|
|
1268
|
+
if (!submitResponse.ok) {
|
|
1269
|
+
const errorData = await submitResponse.json().catch(() => ({}));
|
|
1259
1270
|
throw new Error(
|
|
1260
1271
|
errorData.message ||
|
|
1261
1272
|
errorData.detail ||
|
|
1262
|
-
`Query failed: ${
|
|
1273
|
+
`Query failed: ${submitResponse.status}`,
|
|
1263
1274
|
);
|
|
1264
1275
|
}
|
|
1265
|
-
|
|
1276
|
+
let results = await submitResponse.json();
|
|
1277
|
+
|
|
1278
|
+
// Report links from the first response so they're visible during polling
|
|
1279
|
+
if (onProgress && results.links?.length > 0) {
|
|
1280
|
+
onProgress({ links: results.links });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Poll by query ID using GET /data/query/{id} to avoid re-submitting
|
|
1284
|
+
while (!QUERY_END_STATES.includes(results.state)) {
|
|
1285
|
+
await sleep(pollInterval);
|
|
1286
|
+
pollInterval = Math.min(pollInterval * 2, 10000);
|
|
1287
|
+
|
|
1288
|
+
const pollResponse = await fetch(`${DJ_URL}/data/query/${results.id}`, {
|
|
1289
|
+
credentials: 'include',
|
|
1290
|
+
});
|
|
1291
|
+
if (!pollResponse.ok) {
|
|
1292
|
+
const errorData = await pollResponse.json().catch(() => ({}));
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
errorData.message ||
|
|
1295
|
+
errorData.detail ||
|
|
1296
|
+
`Query poll failed: ${pollResponse.status}`,
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
results = await pollResponse.json();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (results.state === 'CANCELED') {
|
|
1303
|
+
throw new Error('Query execution was canceled');
|
|
1304
|
+
}
|
|
1305
|
+
if (results.state === 'FAILED') {
|
|
1306
|
+
throw new Error(results.errors?.[0] || 'Query execution failed');
|
|
1307
|
+
}
|
|
1308
|
+
return results;
|
|
1266
1309
|
},
|
|
1267
1310
|
|
|
1268
1311
|
nodeData: async function (nodeName, selection = null) {
|
|
@@ -619,19 +619,17 @@ describe('DataJunctionAPI', () => {
|
|
|
619
619
|
it('calls data correctly', async () => {
|
|
620
620
|
const metricSelection = ['metric1'];
|
|
621
621
|
const dimensionSelection = ['dimension1'];
|
|
622
|
-
|
|
623
|
-
metricSelection.forEach(metric => params.append('metrics', metric));
|
|
624
|
-
dimensionSelection.forEach(dimension =>
|
|
625
|
-
params.append('dimensions', dimension),
|
|
626
|
-
);
|
|
627
|
-
fetch.mockResponseOnce(JSON.stringify({}));
|
|
622
|
+
fetch.mockResponseOnce(JSON.stringify({ state: 'FINISHED', results: [] }));
|
|
628
623
|
await DataJunctionAPI.data(metricSelection, dimensionSelection);
|
|
629
624
|
expect(fetch).toHaveBeenCalledWith(
|
|
630
|
-
`${DJ_URL}/data
|
|
631
|
-
{
|
|
632
|
-
credentials: 'include',
|
|
633
|
-
},
|
|
625
|
+
expect.stringContaining(`${DJ_URL}/data/?`),
|
|
626
|
+
{ credentials: 'include' },
|
|
634
627
|
);
|
|
628
|
+
const url = fetch.mock.calls[0][0];
|
|
629
|
+
expect(url).toContain('metrics=metric1');
|
|
630
|
+
expect(url).toContain('dimensions=dimension1');
|
|
631
|
+
expect(url).toContain('limit=10000');
|
|
632
|
+
expect(url).toContain('async_=true');
|
|
635
633
|
});
|
|
636
634
|
|
|
637
635
|
it('calls compiledSql correctly', async () => {
|
|
@@ -2449,7 +2447,7 @@ describe('DataJunctionAPI', () => {
|
|
|
2449
2447
|
// Test metricsV3 (lines 1106-1124)
|
|
2450
2448
|
it('calls metricsV3 correctly', async () => {
|
|
2451
2449
|
fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
|
|
2452
|
-
await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], 'filter=value');
|
|
2450
|
+
await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], ['filter=value']);
|
|
2453
2451
|
expect(fetch).toHaveBeenCalledWith(
|
|
2454
2452
|
expect.stringContaining('/sql/metrics/v3/?'),
|
|
2455
2453
|
expect.objectContaining({ credentials: 'include' }),
|
|
@@ -2994,19 +2992,19 @@ describe('DataJunctionAPI', () => {
|
|
|
2994
2992
|
});
|
|
2995
2993
|
|
|
2996
2994
|
// ===== metricsV3 — useMaterialized=false branch (lines 1224-1225) =====
|
|
2997
|
-
it('calls metricsV3 with useMaterialized=false (
|
|
2995
|
+
it('calls metricsV3 with useMaterialized=false (trino dialect)', async () => {
|
|
2998
2996
|
fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
|
|
2999
|
-
await DataJunctionAPI.metricsV3(['metric1'], ['dim1'],
|
|
2997
|
+
await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], [], false);
|
|
3000
2998
|
const url = fetch.mock.calls[0][0];
|
|
3001
2999
|
expect(url).toContain('use_materialized=false');
|
|
3002
|
-
expect(url).toContain('dialect=
|
|
3000
|
+
expect(url).toContain('dialect=trino');
|
|
3003
3001
|
});
|
|
3004
3002
|
|
|
3005
3003
|
// ===== data — filters non-empty branch (line 1240) =====
|
|
3006
3004
|
it('calls data with filters array', async () => {
|
|
3007
3005
|
fetch.mockResolvedValueOnce({
|
|
3008
3006
|
ok: true,
|
|
3009
|
-
json: () => Promise.resolve(
|
|
3007
|
+
json: () => Promise.resolve({ state: 'FINISHED', results: [] }),
|
|
3010
3008
|
});
|
|
3011
3009
|
await DataJunctionAPI.data(
|
|
3012
3010
|
['metric1'],
|