datajunction-ui 0.0.144 → 0.0.145
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/CubeBuilderPage/CubePreviewPanel.jsx +173 -0
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +268 -97
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +273 -60
- package/src/app/pages/CubeBuilderPage/__tests__/CubePreviewPanel.test.jsx +108 -0
- package/src/app/pages/CubeBuilderPage/__tests__/DimensionsSelect.test.jsx +229 -0
- package/src/app/pages/CubeBuilderPage/__tests__/MetricsSelect.test.jsx +137 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +145 -37
- package/src/app/pages/CubeBuilderPage/index.jsx +367 -125
- package/src/app/pages/CubeBuilderPage/styles.css +489 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +12 -1
- package/src/app/services/DJService.js +69 -0
- package/src/app/services/__tests__/DJService.test.jsx +100 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/* CubeBuilderPage - Query Planner inspired styling */
|
|
2
|
+
|
|
3
|
+
/* ================================
|
|
4
|
+
Page Container - viewport-constrained
|
|
5
|
+
================================ */
|
|
6
|
+
.cube-builder {
|
|
7
|
+
padding: 16px 24px;
|
|
8
|
+
background: var(--planner-bg, #f8fafc);
|
|
9
|
+
height: calc(100vh - 150px);
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
font-family: var(
|
|
13
|
+
--font-body,
|
|
14
|
+
'Inter',
|
|
15
|
+
-apple-system,
|
|
16
|
+
BlinkMacSystemFont,
|
|
17
|
+
sans-serif
|
|
18
|
+
);
|
|
19
|
+
color: var(--planner-text, #1e293b);
|
|
20
|
+
font-size: 13px;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Header */
|
|
25
|
+
.cube-builder-header {
|
|
26
|
+
margin-bottom: 16px;
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cube-builder-header h2 {
|
|
31
|
+
margin: 0;
|
|
32
|
+
font-size: 18px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ================================
|
|
37
|
+
Two-Column Layout
|
|
38
|
+
================================ */
|
|
39
|
+
.cube-builder-layout {
|
|
40
|
+
display: flex;
|
|
41
|
+
gap: 16px;
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Left: Main form area - scrolls internally */
|
|
47
|
+
.cube-builder-main {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
background: var(--planner-surface, #ffffff);
|
|
51
|
+
border: 1px solid var(--planner-border, #e2e8f0);
|
|
52
|
+
overflow-y: auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Right: Sidebar - fixed height matching main column */
|
|
56
|
+
.cube-builder-sidebar {
|
|
57
|
+
width: 480px;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
min-height: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cube-builder-sidebar .cube-preview-panel {
|
|
65
|
+
flex: 1;
|
|
66
|
+
min-height: 0;
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cube-builder-sidebar .cube-preview-panel .preview-sql-container {
|
|
72
|
+
flex: 1;
|
|
73
|
+
min-height: 100px;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ================================
|
|
78
|
+
Section styling (like Query Planner panels)
|
|
79
|
+
================================ */
|
|
80
|
+
.cube-form-section {
|
|
81
|
+
border-bottom: 1px solid var(--planner-border, #e2e8f0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cube-form-section:last-child {
|
|
85
|
+
border-bottom: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.cube-form-section-header {
|
|
89
|
+
display: flex;
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
align-items: center;
|
|
92
|
+
padding: 12px 16px;
|
|
93
|
+
background: var(--planner-surface, #ffffff);
|
|
94
|
+
border-bottom: 1px solid var(--planner-border, #e2e8f0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.cube-form-section-header h3 {
|
|
98
|
+
margin: 0;
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
letter-spacing: 0.5px;
|
|
103
|
+
color: var(--planner-text, #1e293b);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.cube-form-section-body {
|
|
107
|
+
padding: 12px 16px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ================================
|
|
111
|
+
Form Fields
|
|
112
|
+
================================ */
|
|
113
|
+
.cube-field {
|
|
114
|
+
margin-bottom: 16px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cube-field:last-child {
|
|
118
|
+
margin-bottom: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Two-column row */
|
|
122
|
+
.cube-field-row {
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 16px;
|
|
125
|
+
margin-bottom: 16px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cube-field-row:last-child {
|
|
129
|
+
margin-bottom: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cube-field-row .cube-field {
|
|
133
|
+
margin-bottom: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.cube-field-grow {
|
|
137
|
+
flex: 1;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.cube-field-small {
|
|
142
|
+
width: 140px;
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.cube-field-half {
|
|
147
|
+
flex: 1;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.cube-field-label {
|
|
152
|
+
display: block;
|
|
153
|
+
font-size: 11px;
|
|
154
|
+
font-weight: 600;
|
|
155
|
+
text-transform: uppercase;
|
|
156
|
+
letter-spacing: 0.3px;
|
|
157
|
+
color: var(--planner-text-muted, #64748b);
|
|
158
|
+
margin-bottom: 6px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.cube-field-input,
|
|
162
|
+
.cube-builder input[type='text'],
|
|
163
|
+
.cube-builder textarea,
|
|
164
|
+
.cube-builder select {
|
|
165
|
+
width: 100%;
|
|
166
|
+
height: 36px;
|
|
167
|
+
padding: 8px 12px;
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
font-family: inherit;
|
|
170
|
+
color: var(--planner-text, #1e293b);
|
|
171
|
+
background: var(--planner-surface, #ffffff);
|
|
172
|
+
border: 1px solid var(--planner-border, #e2e8f0);
|
|
173
|
+
border-radius: var(--radius-sm, 4px);
|
|
174
|
+
box-sizing: border-box;
|
|
175
|
+
transition: border-color 0.15s ease;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.cube-builder input[type='text']:focus,
|
|
179
|
+
.cube-builder textarea:focus,
|
|
180
|
+
.cube-builder select:focus {
|
|
181
|
+
outline: none;
|
|
182
|
+
border-color: var(--accent-primary, #3b82f6);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.cube-builder textarea {
|
|
186
|
+
height: auto;
|
|
187
|
+
min-height: 36px;
|
|
188
|
+
max-height: 80px;
|
|
189
|
+
resize: vertical;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cube-field-static {
|
|
193
|
+
height: 36px;
|
|
194
|
+
padding: 8px 12px;
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
color: var(--planner-text, #1e293b);
|
|
197
|
+
background: var(--planner-surface-hover, #f8fafc);
|
|
198
|
+
border: 1px solid var(--planner-border, #e2e8f0);
|
|
199
|
+
border-radius: var(--radius-sm, 4px);
|
|
200
|
+
box-sizing: border-box;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Override legacy form field styles */
|
|
204
|
+
.cube-builder .NodeCreationInput,
|
|
205
|
+
.cube-builder .CubeCreationInput,
|
|
206
|
+
.cube-builder .NodeNameInput,
|
|
207
|
+
.cube-builder .DisplayNameInput,
|
|
208
|
+
.cube-builder .DescriptionInput,
|
|
209
|
+
.cube-builder .NamespaceInput,
|
|
210
|
+
.cube-builder .FullNameInput,
|
|
211
|
+
.cube-builder .TagsInput,
|
|
212
|
+
.cube-builder .cube-field-half > div,
|
|
213
|
+
.cube-builder .cube-field-row .TagsInput,
|
|
214
|
+
.cube-builder .cube-field-row .NodeCreationInput {
|
|
215
|
+
display: block !important;
|
|
216
|
+
width: 100% !important;
|
|
217
|
+
margin: 0 !important;
|
|
218
|
+
padding: 0 !important;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cube-builder .cube-form-section-body > .NodeCreationInput,
|
|
222
|
+
.cube-builder .cube-form-section-body > .NamespaceInput {
|
|
223
|
+
margin-bottom: 16px !important;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cube-builder .NodeCreationInput label,
|
|
227
|
+
.cube-builder .CubeCreationInput label,
|
|
228
|
+
.cube-builder .NamespaceInput label,
|
|
229
|
+
.cube-builder .FullNameInput label,
|
|
230
|
+
.cube-builder .TagsInput label,
|
|
231
|
+
.cube-builder .cube-field-half label {
|
|
232
|
+
display: block !important;
|
|
233
|
+
font-size: 11px !important;
|
|
234
|
+
font-weight: 600 !important;
|
|
235
|
+
text-transform: uppercase !important;
|
|
236
|
+
letter-spacing: 0.3px !important;
|
|
237
|
+
color: var(--planner-text-muted, #64748b) !important;
|
|
238
|
+
margin-bottom: 6px !important;
|
|
239
|
+
padding: 0 !important;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ================================
|
|
243
|
+
Settings Panel (in sidebar)
|
|
244
|
+
================================ */
|
|
245
|
+
.cube-settings {
|
|
246
|
+
margin-top: 12px;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.save-cube-btn {
|
|
251
|
+
width: 100%;
|
|
252
|
+
padding: 10px 16px;
|
|
253
|
+
font-size: 13px;
|
|
254
|
+
font-weight: 600;
|
|
255
|
+
font-family: inherit;
|
|
256
|
+
background: var(--accent-primary, #3b82f6);
|
|
257
|
+
color: white;
|
|
258
|
+
border: none;
|
|
259
|
+
border-radius: var(--radius-sm, 4px);
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
transition: background 0.15s ease;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.save-cube-btn:hover {
|
|
265
|
+
background: #2563eb;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.save-cube-btn:disabled {
|
|
269
|
+
opacity: 0.6;
|
|
270
|
+
cursor: not-allowed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Success state - briefly shown after save */
|
|
274
|
+
.save-cube-btn.save-cube-btn--saved,
|
|
275
|
+
.save-cube-btn.save-cube-btn--saved:disabled {
|
|
276
|
+
background: var(--accent-success, #059669);
|
|
277
|
+
opacity: 1;
|
|
278
|
+
cursor: default;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Loading state with spinner */
|
|
282
|
+
.save-cube-btn.save-cube-btn--loading,
|
|
283
|
+
.save-cube-btn.save-cube-btn--loading:disabled {
|
|
284
|
+
opacity: 1;
|
|
285
|
+
display: inline-flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: center;
|
|
288
|
+
gap: 8px;
|
|
289
|
+
cursor: wait;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.save-spinner {
|
|
293
|
+
width: 14px;
|
|
294
|
+
height: 14px;
|
|
295
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
296
|
+
border-top-color: #ffffff;
|
|
297
|
+
border-radius: 50%;
|
|
298
|
+
animation: save-spinner-spin 0.6s linear infinite;
|
|
299
|
+
display: inline-block;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@keyframes save-spinner-spin {
|
|
303
|
+
to {
|
|
304
|
+
transform: rotate(360deg);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Error message shown above the save button */
|
|
309
|
+
.save-error-message {
|
|
310
|
+
padding: 8px 12px;
|
|
311
|
+
margin-bottom: 8px;
|
|
312
|
+
background: rgba(220, 38, 38, 0.08);
|
|
313
|
+
color: var(--accent-error, #dc2626);
|
|
314
|
+
border: 1px solid rgba(220, 38, 38, 0.2);
|
|
315
|
+
border-radius: var(--radius-sm, 4px);
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
line-height: 1.4;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ================================
|
|
321
|
+
Preview Panel (SQL)
|
|
322
|
+
================================ */
|
|
323
|
+
.cube-preview-panel {
|
|
324
|
+
background: var(--planner-surface, #ffffff);
|
|
325
|
+
border: 1px solid var(--planner-border, #e2e8f0);
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Flatten the scan estimate banner so it doesn't look like a nested panel */
|
|
330
|
+
.cube-preview-panel .scan-estimate-banner {
|
|
331
|
+
margin: 0 !important;
|
|
332
|
+
border-radius: 0 !important;
|
|
333
|
+
border: none !important;
|
|
334
|
+
border-bottom: 1px solid var(--planner-border, #e2e8f0) !important;
|
|
335
|
+
padding: 10px 16px !important;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.preview-section-header {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 8px;
|
|
342
|
+
padding: 12px 16px;
|
|
343
|
+
border-bottom: 1px solid var(--planner-border, #e2e8f0);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.preview-section-icon {
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
color: var(--planner-text-muted, #64748b);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.preview-section-title {
|
|
352
|
+
margin: 0;
|
|
353
|
+
font-size: 11px;
|
|
354
|
+
font-weight: 700;
|
|
355
|
+
text-transform: uppercase;
|
|
356
|
+
letter-spacing: 0.5px;
|
|
357
|
+
color: var(--planner-text, #1e293b);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.preview-sql-container {
|
|
361
|
+
max-height: 600px;
|
|
362
|
+
overflow: auto;
|
|
363
|
+
background: var(--planner-surface-hover, #f8fafc);
|
|
364
|
+
padding: 16px;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.preview-sql-container pre,
|
|
368
|
+
.preview-sql-container code {
|
|
369
|
+
border-radius: 0 !important;
|
|
370
|
+
background: transparent !important;
|
|
371
|
+
padding: 0 !important;
|
|
372
|
+
margin: 0 !important;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.preview-loading,
|
|
376
|
+
.preview-error,
|
|
377
|
+
.preview-empty {
|
|
378
|
+
font-size: 12px;
|
|
379
|
+
color: var(--planner-text-muted, #64748b);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.preview-error {
|
|
383
|
+
color: var(--accent-error, #dc2626);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ================================
|
|
387
|
+
Dimensions (hop groups)
|
|
388
|
+
================================ */
|
|
389
|
+
.hop-section {
|
|
390
|
+
margin-bottom: 20px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.hop-section:last-child {
|
|
394
|
+
margin-bottom: 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.hop-header {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
padding: 8px 0;
|
|
401
|
+
border-bottom: 1px solid var(--planner-border, #e2e8f0);
|
|
402
|
+
margin-bottom: 12px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.hop-title {
|
|
406
|
+
font-size: 11px;
|
|
407
|
+
font-weight: 700;
|
|
408
|
+
text-transform: uppercase;
|
|
409
|
+
letter-spacing: 0.3px;
|
|
410
|
+
color: var(--planner-text, #1e293b);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.hop-count {
|
|
414
|
+
margin-left: 8px;
|
|
415
|
+
font-size: 11px;
|
|
416
|
+
color: var(--planner-text-dim, #94a3b8);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.dimension-groups {
|
|
420
|
+
padding-left: 16px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.dimension-group {
|
|
424
|
+
margin-bottom: 12px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.dimension-group-header {
|
|
428
|
+
font-size: 13px;
|
|
429
|
+
font-weight: normal;
|
|
430
|
+
margin: 0 0 6px 0;
|
|
431
|
+
color: var(--planner-text, #1e293b);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.dimension-group-header a {
|
|
435
|
+
color: var(--accent-primary, #3b82f6);
|
|
436
|
+
text-decoration: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.dimension-group-header a:hover {
|
|
440
|
+
text-decoration: underline;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.dimension-group-header .via-text {
|
|
444
|
+
color: var(--planner-text-muted, #64748b);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* ================================
|
|
448
|
+
React-Select styling (matching Query Planner chips exactly)
|
|
449
|
+
================================ */
|
|
450
|
+
.cube-builder .cube-field-half > div > div {
|
|
451
|
+
font-size: 13px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Multi-value chips - simple spacing */
|
|
455
|
+
.cube-builder div[class*='multiValue'] {
|
|
456
|
+
margin: 2px 4px 2px 0 !important;
|
|
457
|
+
border-radius: 3px !important;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.cube-builder div[class*='multiValueLabel'] {
|
|
461
|
+
font-size: 10px !important;
|
|
462
|
+
font-weight: 500 !important;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.cube-builder div[class*='multiValueRemove'] svg {
|
|
466
|
+
width: 12px !important;
|
|
467
|
+
height: 12px !important;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Reduce Input wrapper height to match chip height */
|
|
471
|
+
.cube-builder div[class*='-Input'],
|
|
472
|
+
.cube-builder div[class*='Input'] {
|
|
473
|
+
margin: 0 !important;
|
|
474
|
+
padding: 0 !important;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.cube-builder div[class*='Input'] > div,
|
|
478
|
+
.cube-builder div[class*='Input'] > input {
|
|
479
|
+
height: 20px !important;
|
|
480
|
+
line-height: 20px !important;
|
|
481
|
+
margin: 0 !important;
|
|
482
|
+
padding: 0 !important;
|
|
483
|
+
font-size: 13px !important;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Target the grid sizer inside Input that forces a minimum height */
|
|
487
|
+
.cube-builder div[class*='Input'] [data-value] {
|
|
488
|
+
height: 20px !important;
|
|
489
|
+
}
|
|
@@ -169,7 +169,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
169
169
|
return (
|
|
170
170
|
<div
|
|
171
171
|
className="button-3 cube-element"
|
|
172
|
-
key={cubeElem.name}
|
|
172
|
+
key={cubeElem.name + (cubeElem.role || '')}
|
|
173
173
|
role="cell"
|
|
174
174
|
aria-label="CubeElement"
|
|
175
175
|
aria-hidden="false"
|
|
@@ -179,6 +179,17 @@ export default function NodeInfoTab({ node }) {
|
|
|
179
179
|
? labelize(cubeElem.node_name.split('.').slice(-1)[0]) + ' → '
|
|
180
180
|
: ''}
|
|
181
181
|
{cubeElem.display_name}
|
|
182
|
+
{cubeElem.role && (
|
|
183
|
+
<span
|
|
184
|
+
style={{
|
|
185
|
+
marginLeft: '4px',
|
|
186
|
+
fontSize: '85%',
|
|
187
|
+
color: '#6c757d',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
[{cubeElem.role}]
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
182
193
|
</a>
|
|
183
194
|
<span
|
|
184
195
|
className={`badge node_type__${
|
|
@@ -573,10 +573,12 @@ export const DataJunctionAPI = {
|
|
|
573
573
|
mode
|
|
574
574
|
cubeMetrics {
|
|
575
575
|
name
|
|
576
|
+
displayName
|
|
576
577
|
}
|
|
577
578
|
cubeDimensions {
|
|
578
579
|
name
|
|
579
580
|
attribute
|
|
581
|
+
role
|
|
580
582
|
properties
|
|
581
583
|
}
|
|
582
584
|
}
|
|
@@ -1048,6 +1050,73 @@ export const DataJunctionAPI = {
|
|
|
1048
1050
|
).json();
|
|
1049
1051
|
},
|
|
1050
1052
|
|
|
1053
|
+
getMetricsInfo: async function (names) {
|
|
1054
|
+
if (!names || names.length === 0) return [];
|
|
1055
|
+
const gqlQuery = `
|
|
1056
|
+
query GetMetricsInfo($names: [String!]!) {
|
|
1057
|
+
findNodes(names: $names) {
|
|
1058
|
+
name
|
|
1059
|
+
current {
|
|
1060
|
+
displayName
|
|
1061
|
+
}
|
|
1062
|
+
gitInfo {
|
|
1063
|
+
branch
|
|
1064
|
+
isDefaultBranch
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
`;
|
|
1069
|
+
const response = await fetch(DJ_GQL, {
|
|
1070
|
+
method: 'POST',
|
|
1071
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1072
|
+
credentials: 'include',
|
|
1073
|
+
body: JSON.stringify({
|
|
1074
|
+
query: gqlQuery,
|
|
1075
|
+
variables: { names },
|
|
1076
|
+
}),
|
|
1077
|
+
});
|
|
1078
|
+
const result = await response.json();
|
|
1079
|
+
return (result?.data?.findNodes || []).map(node => ({
|
|
1080
|
+
value: node.name,
|
|
1081
|
+
label: node.current?.displayName || node.name,
|
|
1082
|
+
name: node.name,
|
|
1083
|
+
gitInfo: node.gitInfo,
|
|
1084
|
+
}));
|
|
1085
|
+
},
|
|
1086
|
+
|
|
1087
|
+
searchMetrics: async function (query, limit = 50) {
|
|
1088
|
+
const gqlQuery = `
|
|
1089
|
+
query SearchMetrics($q: String!, $limit: Int!) {
|
|
1090
|
+
findNodes(search: $q, nodeTypes: [METRIC], limit: $limit) {
|
|
1091
|
+
name
|
|
1092
|
+
current {
|
|
1093
|
+
displayName
|
|
1094
|
+
}
|
|
1095
|
+
gitInfo {
|
|
1096
|
+
branch
|
|
1097
|
+
isDefaultBranch
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
`;
|
|
1102
|
+
const response = await fetch(DJ_GQL, {
|
|
1103
|
+
method: 'POST',
|
|
1104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1105
|
+
credentials: 'include',
|
|
1106
|
+
body: JSON.stringify({
|
|
1107
|
+
query: gqlQuery,
|
|
1108
|
+
variables: { q: query, limit },
|
|
1109
|
+
}),
|
|
1110
|
+
});
|
|
1111
|
+
const result = await response.json();
|
|
1112
|
+
return (result?.data?.findNodes || []).map(node => ({
|
|
1113
|
+
value: node.name,
|
|
1114
|
+
label: node.current?.displayName || node.name,
|
|
1115
|
+
name: node.name,
|
|
1116
|
+
gitInfo: node.gitInfo,
|
|
1117
|
+
}));
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1051
1120
|
commonDimensions: async function (metrics) {
|
|
1052
1121
|
const metricsQuery = '?' + metrics.map(m => `metric=${m}`).join('&');
|
|
1053
1122
|
return await (
|
|
@@ -2079,6 +2079,106 @@ describe('DataJunctionAPI', () => {
|
|
|
2079
2079
|
expect(result).toBeNull();
|
|
2080
2080
|
});
|
|
2081
2081
|
|
|
2082
|
+
// searchMetrics — wraps findNodes(METRIC) and shapes results for react-select
|
|
2083
|
+
it('calls searchMetrics correctly', async () => {
|
|
2084
|
+
fetch.mockResponseOnce(
|
|
2085
|
+
JSON.stringify({
|
|
2086
|
+
data: {
|
|
2087
|
+
findNodes: [
|
|
2088
|
+
{
|
|
2089
|
+
name: 'default.revenue',
|
|
2090
|
+
current: { displayName: 'Revenue' },
|
|
2091
|
+
gitInfo: { branch: 'main', isDefaultBranch: true },
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: 'default.orders',
|
|
2095
|
+
current: { displayName: null },
|
|
2096
|
+
gitInfo: null,
|
|
2097
|
+
},
|
|
2098
|
+
],
|
|
2099
|
+
},
|
|
2100
|
+
}),
|
|
2101
|
+
);
|
|
2102
|
+
|
|
2103
|
+
const result = await DataJunctionAPI.searchMetrics('rev', 25);
|
|
2104
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
2105
|
+
'http://localhost:8000/graphql',
|
|
2106
|
+
expect.objectContaining({ method: 'POST', credentials: 'include' }),
|
|
2107
|
+
);
|
|
2108
|
+
const sentBody = JSON.parse(fetch.mock.calls[0][1].body);
|
|
2109
|
+
expect(sentBody.variables).toEqual({ q: 'rev', limit: 25 });
|
|
2110
|
+
expect(result).toEqual([
|
|
2111
|
+
{
|
|
2112
|
+
value: 'default.revenue',
|
|
2113
|
+
label: 'Revenue',
|
|
2114
|
+
name: 'default.revenue',
|
|
2115
|
+
gitInfo: { branch: 'main', isDefaultBranch: true },
|
|
2116
|
+
},
|
|
2117
|
+
// Falls back to name when displayName is missing.
|
|
2118
|
+
{
|
|
2119
|
+
value: 'default.orders',
|
|
2120
|
+
label: 'default.orders',
|
|
2121
|
+
name: 'default.orders',
|
|
2122
|
+
gitInfo: null,
|
|
2123
|
+
},
|
|
2124
|
+
]);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
it('searchMetrics returns [] when the GraphQL response has no data', async () => {
|
|
2128
|
+
fetch.mockResponseOnce(JSON.stringify({}));
|
|
2129
|
+
const result = await DataJunctionAPI.searchMetrics('whatever');
|
|
2130
|
+
expect(result).toEqual([]);
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
it('searchMetrics defaults limit to 50', async () => {
|
|
2134
|
+
fetch.mockResponseOnce(JSON.stringify({ data: { findNodes: [] } }));
|
|
2135
|
+
await DataJunctionAPI.searchMetrics('rev');
|
|
2136
|
+
const sentBody = JSON.parse(fetch.mock.calls[0][1].body);
|
|
2137
|
+
expect(sentBody.variables.limit).toBe(50);
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
// getMetricsInfo — bulk-fetches existing metric metadata, used to populate
|
|
2141
|
+
// branch badges on previously-selected chips when editing a cube.
|
|
2142
|
+
it('calls getMetricsInfo correctly and shapes the response', async () => {
|
|
2143
|
+
fetch.mockResponseOnce(
|
|
2144
|
+
JSON.stringify({
|
|
2145
|
+
data: {
|
|
2146
|
+
findNodes: [
|
|
2147
|
+
{
|
|
2148
|
+
name: 'default.revenue',
|
|
2149
|
+
current: { displayName: 'Revenue' },
|
|
2150
|
+
gitInfo: { branch: 'main', isDefaultBranch: true },
|
|
2151
|
+
},
|
|
2152
|
+
],
|
|
2153
|
+
},
|
|
2154
|
+
}),
|
|
2155
|
+
);
|
|
2156
|
+
|
|
2157
|
+
const result = await DataJunctionAPI.getMetricsInfo(['default.revenue']);
|
|
2158
|
+
const sentBody = JSON.parse(fetch.mock.calls[0][1].body);
|
|
2159
|
+
expect(sentBody.variables).toEqual({ names: ['default.revenue'] });
|
|
2160
|
+
expect(result).toEqual([
|
|
2161
|
+
{
|
|
2162
|
+
value: 'default.revenue',
|
|
2163
|
+
label: 'Revenue',
|
|
2164
|
+
name: 'default.revenue',
|
|
2165
|
+
gitInfo: { branch: 'main', isDefaultBranch: true },
|
|
2166
|
+
},
|
|
2167
|
+
]);
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
it('getMetricsInfo short-circuits on empty/null input without hitting fetch', async () => {
|
|
2171
|
+
expect(await DataJunctionAPI.getMetricsInfo([])).toEqual([]);
|
|
2172
|
+
expect(await DataJunctionAPI.getMetricsInfo(null)).toEqual([]);
|
|
2173
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
it('getMetricsInfo returns [] when GraphQL has no data', async () => {
|
|
2177
|
+
fetch.mockResponseOnce(JSON.stringify({}));
|
|
2178
|
+
const result = await DataJunctionAPI.getMetricsInfo(['anything']);
|
|
2179
|
+
expect(result).toEqual([]);
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2082
2182
|
// Test logout (lines 225-230)
|
|
2083
2183
|
it('calls logout correctly', async () => {
|
|
2084
2184
|
fetch.mockResponseOnce('', { status: 200 });
|