datajunction-ui 0.0.31 → 0.0.41

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.
@@ -62,14 +62,23 @@ describe('<NotificationsPage />', () => {
62
62
  const mockDjClient = createMockDjClient();
63
63
  renderWithContext(mockDjClient);
64
64
 
65
+ // Wait for async effects to complete
66
+ await waitFor(() => {
67
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
68
+ });
69
+
65
70
  expect(screen.getByText('Notifications')).toBeInTheDocument();
66
71
  });
67
72
 
68
- it('shows loading state initially', () => {
73
+ it('shows loading state initially', async () => {
74
+ // Use a controlled promise that we can resolve after the test
75
+ let resolvePromise;
76
+ const pendingPromise = new Promise(resolve => {
77
+ resolvePromise = resolve;
78
+ });
79
+
69
80
  const mockDjClient = createMockDjClient({
70
- getSubscribedHistory: jest.fn().mockImplementation(
71
- () => new Promise(() => {}), // Never resolves
72
- ),
81
+ getSubscribedHistory: jest.fn().mockImplementation(() => pendingPromise),
73
82
  });
74
83
  renderWithContext(mockDjClient);
75
84
 
@@ -78,6 +87,12 @@ describe('<NotificationsPage />', () => {
78
87
  '[style*="text-align: center"]',
79
88
  );
80
89
  expect(loadingContainer).toBeInTheDocument();
90
+
91
+ // Resolve the promise to allow cleanup without act() warnings
92
+ resolvePromise([]);
93
+ await waitFor(() => {
94
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
95
+ });
81
96
  });
82
97
 
83
98
  it('shows empty state when no notifications', async () => {
@@ -114,31 +114,65 @@ describe('SQLBuilderPage', () => {
114
114
  mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
115
115
  mockDjClient.sqls.mockResolvedValue({ sql: 'SELECT ...' });
116
116
  mockDjClient.data.mockResolvedValue({});
117
+ });
118
+
119
+ afterEach(() => {
120
+ jest.clearAllMocks();
121
+ });
117
122
 
123
+ it('renders without crashing', async () => {
118
124
  render(
119
125
  <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
120
126
  <SQLBuilderPage />
121
127
  </DJClientContext.Provider>,
122
128
  );
123
- });
124
129
 
125
- afterEach(() => {
126
- jest.clearAllMocks();
127
- });
130
+ await waitFor(() => {
131
+ expect(mockDjClient.metrics).toHaveBeenCalled();
132
+ });
128
133
 
129
- it('renders without crashing', () => {
130
134
  expect(screen.getByText('Using the SQL Builder')).toBeInTheDocument();
131
135
  });
132
136
 
133
- it('renders the Metrics section', () => {
137
+ it('renders the Metrics section', async () => {
138
+ render(
139
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
140
+ <SQLBuilderPage />
141
+ </DJClientContext.Provider>,
142
+ );
143
+
144
+ await waitFor(() => {
145
+ expect(mockDjClient.metrics).toHaveBeenCalled();
146
+ });
147
+
134
148
  expect(screen.getByText('Metrics')).toBeInTheDocument();
135
149
  });
136
150
 
137
- it('renders the Group By section', () => {
151
+ it('renders the Group By section', async () => {
152
+ render(
153
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
154
+ <SQLBuilderPage />
155
+ </DJClientContext.Provider>,
156
+ );
157
+
158
+ await waitFor(() => {
159
+ expect(mockDjClient.metrics).toHaveBeenCalled();
160
+ });
161
+
138
162
  expect(screen.getByText('Group By')).toBeInTheDocument();
139
163
  });
140
164
 
141
- it('renders the Filter By section', () => {
165
+ it('renders the Filter By section', async () => {
166
+ render(
167
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
168
+ <SQLBuilderPage />
169
+ </DJClientContext.Provider>,
170
+ );
171
+
172
+ await waitFor(() => {
173
+ expect(mockDjClient.metrics).toHaveBeenCalled();
174
+ });
175
+
142
176
  expect(screen.getByText('Filter By')).toBeInTheDocument();
143
177
  });
144
178
 
@@ -179,7 +213,11 @@ describe('SQLBuilderPage', () => {
179
213
  expect(screen.getAllByText(dim.name)[0]).toBeInTheDocument();
180
214
  fireEvent.click(screen.getAllByText(dim.name)[0]);
181
215
  }
182
- expect(mockDjClient.sqls).toHaveBeenCalled();
216
+
217
+ // Wait for SQL fetch to complete to avoid act() warnings
218
+ await waitFor(() => {
219
+ expect(mockDjClient.sqls).toHaveBeenCalled();
220
+ });
183
221
  });
184
222
  });
185
223
 
@@ -181,8 +181,8 @@ export function SQLBuilderPage() {
181
181
  setDisplayedRows(
182
182
  data[0]?.rows.slice(0, showNumRows).map((rowData, index) => (
183
183
  <tr key={`data-row:${index}`}>
184
- {rowData.map(rowValue => (
185
- <td key={rowValue}>{rowValue}</td>
184
+ {rowData.map((rowValue, colIndex) => (
185
+ <td key={`${index}-${colIndex}`}>{rowValue}</td>
186
186
  ))}
187
187
  </tr>
188
188
  )),
@@ -1895,6 +1895,7 @@ export const DataJunctionAPI = {
1895
1895
  if (filters.grain_mode) params.append('grain_mode', filters.grain_mode);
1896
1896
  if (filters.measures) params.append('measures', filters.measures);
1897
1897
  if (filters.status) params.append('status', filters.status);
1898
+ if (filters.include_stale) params.append('include_stale', 'true');
1898
1899
 
1899
1900
  return await (
1900
1901
  await fetch(`${DJ_URL}/preaggs/?${params}`, {
@@ -2050,6 +2051,31 @@ export const DataJunctionAPI = {
2050
2051
  return result;
2051
2052
  },
2052
2053
 
2054
+ // Bulk deactivate pre-aggregation workflows for a node
2055
+ bulkDeactivatePreaggWorkflows: async function (nodeName, staleOnly = false) {
2056
+ const params = new URLSearchParams();
2057
+ params.append('node_name', nodeName);
2058
+ if (staleOnly) params.append('stale_only', 'true');
2059
+
2060
+ const response = await fetch(`${DJ_URL}/preaggs/workflows?${params}`, {
2061
+ method: 'DELETE',
2062
+ credentials: 'include',
2063
+ });
2064
+ const result = await response.json();
2065
+ if (!response.ok) {
2066
+ return {
2067
+ ...result,
2068
+ _error: true,
2069
+ _status: response.status,
2070
+ message:
2071
+ result.message ||
2072
+ result.detail ||
2073
+ 'Failed to bulk deactivate workflows',
2074
+ };
2075
+ }
2076
+ return result;
2077
+ },
2078
+
2053
2079
  // Get cube details including materializations
2054
2080
  getCubeDetails: async function (cubeName) {
2055
2081
  const response = await fetch(`${DJ_URL}/cubes/${cubeName}`, {
@@ -0,0 +1,547 @@
1
+ /**
2
+ * Pre-aggregations Tab Styles
3
+ *
4
+ * Reusable CSS classes for the pre-aggregations UI components.
5
+ */
6
+
7
+ /* =============================================================================
8
+ Layout
9
+ ============================================================================= */
10
+
11
+ .preagg-container {
12
+ padding: 10px 0;
13
+ }
14
+
15
+ .preagg-section {
16
+ margin-bottom: 30px;
17
+ }
18
+
19
+ .preagg-two-column {
20
+ display: grid;
21
+ grid-template-columns: 1fr 1fr;
22
+ gap: 16px;
23
+ }
24
+
25
+ .preagg-stack {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 1.5em;
29
+ }
30
+
31
+ /* =============================================================================
32
+ Section Headers
33
+ ============================================================================= */
34
+
35
+ .preagg-section-header {
36
+ display: flex;
37
+ align-items: center;
38
+ margin-bottom: 16px;
39
+ border-bottom: 2px solid #e5e7eb;
40
+ padding-bottom: 8px;
41
+ }
42
+
43
+ .preagg-section-header--stale {
44
+ border-bottom-color: #fcd34d;
45
+ justify-content: space-between;
46
+ }
47
+
48
+ .preagg-section-title {
49
+ margin: 0;
50
+ font-size: 16px;
51
+ font-weight: 600;
52
+ color: #374151;
53
+ }
54
+
55
+ .preagg-section-title--stale {
56
+ color: #92400e;
57
+ }
58
+
59
+ .preagg-section-count {
60
+ margin-left: 12px;
61
+ font-size: 13px;
62
+ color: #6b7280;
63
+ }
64
+
65
+ .preagg-section-count--stale {
66
+ color: #b45309;
67
+ }
68
+
69
+ /* =============================================================================
70
+ Pre-agg Row (Card Container)
71
+ ============================================================================= */
72
+
73
+ .preagg-row {
74
+ border: 1px solid #e0e0e0;
75
+ border-radius: 8px;
76
+ margin-bottom: 10px;
77
+ background-color: #fff;
78
+ }
79
+
80
+ .preagg-row--stale {
81
+ background-color: #fffbeb;
82
+ }
83
+
84
+ /* Collapsed Header */
85
+ .preagg-row-header {
86
+ display: flex;
87
+ align-items: center;
88
+ padding: 12px 16px;
89
+ cursor: pointer;
90
+ gap: 12px;
91
+ }
92
+
93
+ .preagg-row-toggle {
94
+ font-size: 14px;
95
+ color: #666;
96
+ }
97
+
98
+ .preagg-row-grain-chips {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 6px;
102
+ min-width: 180px;
103
+ }
104
+
105
+ .preagg-grain-chip {
106
+ padding: 2px 8px;
107
+ background-color: #f1f5f9;
108
+ border-radius: 4px;
109
+ color: #475569;
110
+ font-size: 12px;
111
+ font-weight: 500;
112
+ font-family: monospace;
113
+ }
114
+
115
+ .preagg-grain-chip--more {
116
+ background-color: #e2e8f0;
117
+ color: #64748b;
118
+ }
119
+
120
+ .preagg-row-measures {
121
+ font-size: 12px;
122
+ color: #563a12;
123
+ background: #fff6e9;
124
+ border-radius: 8px;
125
+ padding: 2px 8px;
126
+ }
127
+
128
+ .preagg-row-schedule {
129
+ font-size: 12px;
130
+ color: #888;
131
+ }
132
+
133
+ .preagg-row-version {
134
+ font-size: 12px;
135
+ color: #b45309;
136
+ font-style: italic;
137
+ }
138
+
139
+ /* Expanded Details */
140
+ .preagg-details {
141
+ padding: 20px;
142
+ border-top: 1px solid #e0e0e0;
143
+ background-color: #f8fafc;
144
+ }
145
+
146
+ .preagg-details--stale {
147
+ background-color: #fefce8;
148
+ }
149
+
150
+ /* =============================================================================
151
+ Stale Warning Banner
152
+ ============================================================================= */
153
+
154
+ .preagg-stale-banner {
155
+ background-color: #fef3c7;
156
+ border: 1px solid #fcd34d;
157
+ border-radius: 8px;
158
+ padding: 12px 16px;
159
+ margin-bottom: 20px;
160
+ font-size: 13px;
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 10px;
164
+ }
165
+
166
+ .preagg-stale-banner-icon {
167
+ font-size: 18px;
168
+ }
169
+
170
+ .preagg-stale-banner-text {
171
+ color: #78350f;
172
+ }
173
+
174
+ /* =============================================================================
175
+ Card Boxes (Config, Grain, etc.)
176
+ ============================================================================= */
177
+
178
+ .preagg-card {
179
+ background-color: #ffffff;
180
+ border-radius: 8px;
181
+ /* border: 1px solid #e2e8f0; */
182
+ padding: 16px;
183
+ height: fit-content;
184
+ box-sizing: border-box;
185
+ }
186
+
187
+ .preagg-card--compact {
188
+ padding: 12px 16px;
189
+ }
190
+
191
+ .preagg-card-label {
192
+ font-size: 12px;
193
+ font-weight: 600;
194
+ color: #64748b;
195
+ text-transform: uppercase;
196
+ letter-spacing: 0.05em;
197
+ margin-bottom: 8px;
198
+ }
199
+
200
+ .preagg-card-label--with-info {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 6px;
204
+ }
205
+
206
+ /* =============================================================================
207
+ Config Table
208
+ ============================================================================= */
209
+
210
+ .preagg-config-table {
211
+ font-size: 13px;
212
+ border-collapse: collapse;
213
+ width: 100%;
214
+ }
215
+
216
+ .preagg-config-key {
217
+ padding: 4px 12px 4px 0;
218
+ color: #64748b;
219
+ font-weight: 500;
220
+ white-space: nowrap;
221
+ width: 100px;
222
+ }
223
+
224
+ .preagg-config-value {
225
+ padding: 4px 0;
226
+ color: #1e293b;
227
+ }
228
+
229
+ .preagg-config-value code {
230
+ font-size: 12px;
231
+ background-color: #f1f5f9;
232
+ padding: 2px 6px;
233
+ border-radius: 4px;
234
+ }
235
+
236
+ .preagg-config-schedule-cron {
237
+ margin-left: 6px;
238
+ font-size: 11px;
239
+ color: #94a3b8;
240
+ font-family: monospace;
241
+ }
242
+
243
+ /* =============================================================================
244
+ Actions
245
+ ============================================================================= */
246
+
247
+ .preagg-actions {
248
+ display: flex;
249
+ flex-wrap: wrap;
250
+ gap: 8px;
251
+ padding-top: 12px;
252
+ margin-top: 12px;
253
+ border-top: 1px solid #e2e8f0;
254
+ }
255
+
256
+ .preagg-action-btn {
257
+ display: inline-flex;
258
+ align-items: center;
259
+ padding: 5px 10px;
260
+ background-color: #ffffff;
261
+ border: 1px solid #e2e8f0;
262
+ border-radius: 6px;
263
+ color: #475569;
264
+ font-size: 12px;
265
+ font-weight: 500;
266
+ text-decoration: none;
267
+ cursor: pointer;
268
+ }
269
+
270
+ .preagg-action-btn:hover {
271
+ background-color: #f8fafc;
272
+ text-decoration: none;
273
+ }
274
+
275
+ .preagg-action-btn--danger {
276
+ border-color: #fecaca;
277
+ color: #dc2626;
278
+ }
279
+
280
+ .preagg-action-btn--danger:hover {
281
+ background-color: #fef2f2;
282
+ }
283
+
284
+ .preagg-action-btn--danger-fill {
285
+ background-color: #fee2e2;
286
+ border-color: #fca5a5;
287
+ color: #991b1b;
288
+ }
289
+
290
+ .preagg-action-btn:disabled {
291
+ cursor: not-allowed;
292
+ opacity: 0.7;
293
+ }
294
+
295
+ /* =============================================================================
296
+ Badges
297
+ ============================================================================= */
298
+
299
+ /* Base badge */
300
+ .preagg-badge {
301
+ padding: 4px 10px;
302
+ border-radius: 4px;
303
+ font-size: 12px;
304
+ font-weight: 500;
305
+ text-decoration: none;
306
+ display: inline-block;
307
+ }
308
+
309
+ /* Status badges (pill style) */
310
+ .preagg-status-badge {
311
+ padding: 2px 8px;
312
+ border-radius: 12px;
313
+ font-size: 12px;
314
+ }
315
+
316
+ .preagg-status-badge--active {
317
+ background-color: #dcfce7;
318
+ color: #166534;
319
+ }
320
+
321
+ .preagg-status-badge--paused {
322
+ background-color: #fef3c7;
323
+ color: #92400e;
324
+ }
325
+
326
+ .preagg-status-badge--pending {
327
+ background-color: #f3f4f6;
328
+ color: #6b7280;
329
+ }
330
+
331
+ /* Metric count badge (in header row) */
332
+ .preagg-metric-count-badge {
333
+ font-size: 12px;
334
+ color: #be123c;
335
+ background-color: #fff1f2;
336
+ padding: 2px 8px;
337
+ border-radius: 12px;
338
+ }
339
+
340
+ /* Grain badge */
341
+ .preagg-grain-badge,
342
+ .preagg-grain-badge:hover {
343
+ padding: 4px 10px;
344
+ background-color: #f1f5f9;
345
+ border-radius: 4px;
346
+ color: #1e40af;
347
+ font-size: 12px;
348
+ font-weight: 500;
349
+ text-decoration: none;
350
+ font-family: monospace;
351
+ }
352
+
353
+ .preagg-grain-badge:hover {
354
+ background-color: #e2e8f0;
355
+ text-decoration: none;
356
+ }
357
+
358
+ /* Aggregation badge (blue) */
359
+ .preagg-agg-badge {
360
+ background-color: #dbeafe;
361
+ padding: 4px 10px;
362
+ border-radius: 4px;
363
+ color: #1e40af;
364
+ font-size: 12px;
365
+ font-weight: 500;
366
+ }
367
+
368
+ /* Merge badge (green) */
369
+ .preagg-merge-badge {
370
+ background-color: #dcfce7;
371
+ padding: 4px 10px;
372
+ border-radius: 4px;
373
+ color: #166534;
374
+ font-size: 12px;
375
+ font-weight: 500;
376
+ }
377
+
378
+ /* Rule badge (gray) */
379
+ .preagg-rule-badge {
380
+ color: #475569;
381
+ background-color: #f1f5f9;
382
+ padding: 4px 8px;
383
+ border-radius: 4px;
384
+ font-size: 11px;
385
+ font-weight: 500;
386
+ }
387
+
388
+ /* Metric badge (red/rose) */
389
+ .preagg-metric-badge {
390
+ font-size: 11px;
391
+ color: #be123c;
392
+ background-color: #fff1f2;
393
+ padding: 3px 8px;
394
+ border-radius: 4px;
395
+ text-decoration: none;
396
+ border: 1px solid #fecdd3;
397
+ font-weight: 500;
398
+ }
399
+
400
+ .preagg-metric-badge:hover {
401
+ background-color: #ffe4e6;
402
+ text-decoration: none;
403
+ }
404
+
405
+ /* Expand/collapse button (used in grain section) */
406
+ .preagg-expand-btn {
407
+ padding: 4px 10px;
408
+ background-color: #f1f5f9;
409
+ border-radius: 4px;
410
+ color: #64748b;
411
+ font-size: 12px;
412
+ font-weight: 500;
413
+ border: none;
414
+ cursor: pointer;
415
+ }
416
+
417
+ .preagg-expand-btn:hover {
418
+ background-color: #e2e8f0;
419
+ }
420
+
421
+ /* =============================================================================
422
+ Grain List
423
+ ============================================================================= */
424
+
425
+ .preagg-grain-list {
426
+ display: flex;
427
+ flex-wrap: wrap;
428
+ gap: 8px;
429
+ }
430
+
431
+ /* =============================================================================
432
+ Measures Table
433
+ ============================================================================= */
434
+
435
+ .preagg-measures-table {
436
+ width: 100%;
437
+ font-size: 13px;
438
+ border-collapse: collapse;
439
+ }
440
+
441
+ .preagg-measures-table thead {
442
+ background-color: #fafafa;
443
+ border-bottom: 1px solid #e2e8f0;
444
+ }
445
+
446
+ .preagg-measures-table th {
447
+ padding: 10px 16px;
448
+ text-align: left;
449
+ font-weight: 500;
450
+ color: #64748b;
451
+ font-size: 12px;
452
+ }
453
+
454
+ .preagg-measures-table td {
455
+ padding: 12px 16px;
456
+ }
457
+
458
+ .preagg-measures-table tbody tr {
459
+ border-bottom: 1px solid #f1f5f9;
460
+ }
461
+
462
+ .preagg-measures-table tbody tr:last-child {
463
+ border-bottom: none;
464
+ }
465
+
466
+ .preagg-measure-name {
467
+ font-weight: 500;
468
+ color: #1e293b;
469
+ font-family: monospace;
470
+ font-size: 12px;
471
+ }
472
+
473
+ .preagg-metrics-list {
474
+ display: flex;
475
+ flex-wrap: wrap;
476
+ gap: 6px;
477
+ }
478
+
479
+ /* =============================================================================
480
+ Info Icon
481
+ ============================================================================= */
482
+
483
+ .preagg-info-icon {
484
+ cursor: help;
485
+ color: #94a3b8;
486
+ font-weight: normal;
487
+ margin-left: 4px;
488
+ }
489
+
490
+ /* =============================================================================
491
+ Empty State
492
+ ============================================================================= */
493
+
494
+ .preagg-empty {
495
+ padding: 16px;
496
+ background-color: #f9fafb;
497
+ border-radius: 8px;
498
+ color: #6b7280;
499
+ font-size: 14px;
500
+ }
501
+
502
+ /* =============================================================================
503
+ Loading & Error States
504
+ ============================================================================= */
505
+
506
+ .preagg-loading {
507
+ padding: 20px;
508
+ text-align: center;
509
+ color: #666;
510
+ }
511
+
512
+ .preagg-error {
513
+ padding: 20px;
514
+ margin: 20px 0;
515
+ }
516
+
517
+ .preagg-no-data {
518
+ padding: 20px;
519
+ }
520
+
521
+ .preagg-no-data-alert {
522
+ margin-bottom: 20px;
523
+ padding: 16px;
524
+ }
525
+
526
+ .preagg-no-data-text {
527
+ font-size: 14px;
528
+ color: #666;
529
+ }
530
+
531
+ /* =============================================================================
532
+ Section Header (Stale section - left side)
533
+ ============================================================================= */
534
+
535
+ .preagg-section-header-left {
536
+ display: flex;
537
+ align-items: center;
538
+ }
539
+
540
+ /* =============================================================================
541
+ Card Modifier for Tables
542
+ ============================================================================= */
543
+
544
+ .preagg-card--table {
545
+ padding: 0;
546
+ overflow: hidden;
547
+ }