datajunction-ui 0.0.94 → 0.0.96

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.
@@ -2,6 +2,15 @@
2
2
  Materialization Planner - Three Column Layout
3
3
  ================================= */
4
4
 
5
+ /* Reset native browser input styling for all planner inputs */
6
+ .cube-search,
7
+ .combobox-search,
8
+ .filter-input {
9
+ -webkit-appearance: none;
10
+ appearance: none;
11
+ box-shadow: none;
12
+ }
13
+
5
14
  /* CSS Variables for theming */
6
15
  :root {
7
16
  --planner-bg: #f8fafc;
@@ -857,6 +866,13 @@
857
866
  gap: 2px;
858
867
  }
859
868
 
869
+ .dimension-full-name {
870
+ font-size: 10px;
871
+ color: var(--planner-text-muted);
872
+ font-family: var(--font-display);
873
+ word-break: break-all;
874
+ }
875
+
860
876
  .dimension-path {
861
877
  font-size: 10px;
862
878
  color: var(--planner-text-dim);
@@ -3557,6 +3573,55 @@ a.action-btn {
3557
3573
  cursor: not-allowed;
3558
3574
  }
3559
3575
 
3576
+ /* =================================
3577
+ Engine Selection
3578
+ ================================= */
3579
+
3580
+ .engine-section {
3581
+ display: flex;
3582
+ align-items: center;
3583
+ gap: 8px;
3584
+ padding: 10px 12px;
3585
+ border-top: 1px solid var(--planner-border);
3586
+ background: var(--planner-bg);
3587
+ flex-shrink: 0;
3588
+ }
3589
+
3590
+ .engine-label {
3591
+ font-size: 12px;
3592
+ font-weight: 500;
3593
+ color: var(--text-secondary);
3594
+ white-space: nowrap;
3595
+ }
3596
+
3597
+ .engine-pills {
3598
+ display: flex;
3599
+ gap: 4px;
3600
+ }
3601
+
3602
+ .engine-pill {
3603
+ padding: 4px 10px;
3604
+ border: 1px solid var(--planner-border);
3605
+ border-radius: 12px;
3606
+ background: transparent;
3607
+ font-size: 12px;
3608
+ color: var(--text-secondary);
3609
+ cursor: pointer;
3610
+ transition: all 0.12s ease;
3611
+ }
3612
+
3613
+ .engine-pill:hover {
3614
+ border-color: #059669;
3615
+ color: #059669;
3616
+ }
3617
+
3618
+ .engine-pill.active {
3619
+ background: #059669;
3620
+ border-color: #059669;
3621
+ color: white;
3622
+ font-weight: 500;
3623
+ }
3624
+
3560
3625
  /* =================================
3561
3626
  Run Query Section
3562
3627
  ================================= */
@@ -3950,8 +4015,9 @@ a.action-btn {
3950
4015
  .results-table th {
3951
4016
  position: sticky;
3952
4017
  top: 0;
4018
+ z-index: 1;
3953
4019
  padding: 10px 12px;
3954
- background: var(--planner-bg);
4020
+ background: #f8fafc;
3955
4021
  color: var(--planner-text);
3956
4022
  font-weight: 600;
3957
4023
  text-align: left;
@@ -3962,7 +4028,7 @@ a.action-btn {
3962
4028
  }
3963
4029
 
3964
4030
  .results-table th:hover {
3965
- background: var(--planner-bg-hover, #f1f5f9);
4031
+ background: #f1f5f9;
3966
4032
  }
3967
4033
 
3968
4034
  .results-table th.sorted {
@@ -4051,3 +4117,155 @@ a.action-btn {
4051
4117
  .filters-list .filter-chip {
4052
4118
  padding: 4px 10px;
4053
4119
  }
4120
+
4121
+ /* =================================
4122
+ Results View - Tab bar & Chart
4123
+ ================================= */
4124
+
4125
+ .results-tabs-bar {
4126
+ display: flex;
4127
+ align-items: center;
4128
+ gap: 12px;
4129
+ padding: 8px 16px 0;
4130
+ border-bottom: 1px solid var(--planner-border);
4131
+ background: var(--planner-surface);
4132
+ flex-shrink: 0;
4133
+ }
4134
+
4135
+ .results-tabs {
4136
+ display: flex;
4137
+ gap: 2px;
4138
+ }
4139
+
4140
+ .results-tab {
4141
+ padding: 6px 14px;
4142
+ font-size: 12px;
4143
+ font-weight: 500;
4144
+ font-family: var(--font-body);
4145
+ background: none;
4146
+ border: none;
4147
+ border-bottom: 2px solid transparent;
4148
+ color: var(--planner-text-muted);
4149
+ cursor: pointer;
4150
+ transition: color 0.15s, border-color 0.15s;
4151
+ margin-bottom: -1px;
4152
+ }
4153
+
4154
+ .results-tab:hover:not(.disabled) {
4155
+ color: var(--planner-text);
4156
+ }
4157
+
4158
+ .results-tab.active {
4159
+ color: var(--accent-primary);
4160
+ border-bottom-color: var(--accent-primary);
4161
+ }
4162
+
4163
+ .results-tab.disabled {
4164
+ opacity: 0.4;
4165
+ cursor: not-allowed;
4166
+ }
4167
+
4168
+ .results-tabs-meta {
4169
+ display: flex;
4170
+ align-items: center;
4171
+ gap: 8px;
4172
+ margin-left: auto;
4173
+ padding-bottom: 6px;
4174
+ }
4175
+
4176
+ /* Chart wrapper - fills remaining height */
4177
+ .results-chart-wrapper {
4178
+ flex: 1;
4179
+ overflow: hidden;
4180
+ min-height: 0;
4181
+ padding: 16px 8px 8px;
4182
+ display: flex;
4183
+ align-items: stretch;
4184
+ }
4185
+
4186
+ /* KPI cards */
4187
+ .kpi-cards {
4188
+ display: flex;
4189
+ flex-wrap: wrap;
4190
+ gap: 16px;
4191
+ align-items: flex-start;
4192
+ padding: 8px;
4193
+ }
4194
+
4195
+ .kpi-card {
4196
+ background: var(--planner-surface);
4197
+ border: 1px solid var(--planner-border);
4198
+ border-radius: var(--radius-lg);
4199
+ padding: 20px 28px;
4200
+ min-width: 160px;
4201
+ text-align: center;
4202
+ }
4203
+
4204
+ .kpi-label {
4205
+ font-size: 11px;
4206
+ font-weight: 600;
4207
+ color: var(--planner-text-muted);
4208
+ text-transform: uppercase;
4209
+ letter-spacing: 0.04em;
4210
+ margin-bottom: 8px;
4211
+ }
4212
+
4213
+ .kpi-value {
4214
+ font-size: 28px;
4215
+ font-weight: 700;
4216
+ color: var(--planner-text);
4217
+ font-family: var(--font-display);
4218
+ line-height: 1.1;
4219
+ }
4220
+
4221
+ .kpi-type {
4222
+ font-size: 10px;
4223
+ color: var(--planner-text-dim);
4224
+ margin-top: 6px;
4225
+ }
4226
+
4227
+ .chart-no-data {
4228
+ display: flex;
4229
+ align-items: center;
4230
+ justify-content: center;
4231
+ width: 100%;
4232
+ color: var(--planner-text-muted);
4233
+ font-size: 13px;
4234
+ }
4235
+
4236
+ /* =================================
4237
+ Small Multiples
4238
+ ================================= */
4239
+
4240
+ .small-multiples {
4241
+ display: flex;
4242
+ flex-direction: column;
4243
+ gap: 0;
4244
+ width: 100%;
4245
+ height: 100%;
4246
+ overflow-y: auto;
4247
+ }
4248
+
4249
+ .small-multiple {
4250
+ flex-shrink: 0;
4251
+ height: 220px;
4252
+ padding: 0 0 8px 0;
4253
+ border-bottom: 1px solid var(--planner-border);
4254
+ }
4255
+
4256
+ .small-multiple:last-child {
4257
+ border-bottom: none;
4258
+ }
4259
+
4260
+ .small-multiple-label {
4261
+ font-size: 11px;
4262
+ font-weight: 600;
4263
+ color: var(--planner-text-muted);
4264
+ padding: 8px 0 0 8px;
4265
+ text-transform: uppercase;
4266
+ letter-spacing: 0.04em;
4267
+ }
4268
+
4269
+ .small-multiple-chart {
4270
+ height: 188px;
4271
+ }
@@ -42,8 +42,8 @@ describe('<Root />', () => {
42
42
  });
43
43
 
44
44
  // Check navigation links exist
45
- expect(screen.getByText('Explore')).toBeInTheDocument();
46
- expect(screen.getByText('Query Planner')).toBeInTheDocument();
45
+ expect(screen.getAllByText('Catalog')).toHaveLength(1);
46
+ expect(screen.getAllByText('Explore')).toHaveLength(1);
47
47
  });
48
48
 
49
49
  it('renders Docs dropdown', async () => {
@@ -56,12 +56,12 @@ export function Root() {
56
56
  <div className="menu-item here menu-here-bg menu-lg-down-accordion me-0 me-lg-2 fw-semibold">
57
57
  <span className="menu-link">
58
58
  <span className="menu-title">
59
- <a href="/">Explore</a>
59
+ <a href="/">Catalog</a>
60
60
  </span>
61
61
  </span>
62
62
  <span className="menu-link">
63
63
  <span className="menu-title">
64
- <a href="/planner">Query Planner</a>
64
+ <a href="/planner">Explore</a>
65
65
  </span>
66
66
  </span>
67
67
  <span className="menu-link">
@@ -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', 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 (metricSelection, dimensionSelection, filters = []) {
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
- const response = await fetch(`${DJ_URL}/data/?${params}`, {
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 (!response.ok) {
1258
- const errorData = await response.json().catch(() => ({}));
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: ${response.status}`,
1273
+ `Query failed: ${submitResponse.status}`,
1263
1274
  );
1264
1275
  }
1265
- return await response.json();
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
- const params = new URLSearchParams();
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/?${params}&limit=10000`,
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 (spark dialect)', async () => {
2995
+ it('calls metricsV3 with useMaterialized=false (trino dialect)', async () => {
2998
2996
  fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
2999
- await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], '', false);
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=spark');
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([{ col: 'metric1', value: 42 }]),
3007
+ json: () => Promise.resolve({ state: 'FINISHED', results: [] }),
3010
3008
  });
3011
3009
  await DataJunctionAPI.data(
3012
3010
  ['metric1'],