datajunction-ui 0.0.143 → 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.
@@ -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
+ }
@@ -639,14 +639,14 @@ describe('NamespacePage', () => {
639
639
  git_branch: 'main',
640
640
  num_nodes: 10,
641
641
  invalid_node_count: 1,
642
- last_deployed_at: '2024-10-18T12:00:00+00:00',
642
+ last_updated_at: '2024-10-18T12:00:00+00:00',
643
643
  },
644
644
  {
645
645
  namespace: 'default.feature-xyz',
646
646
  git_branch: 'feature-xyz',
647
647
  num_nodes: 5,
648
648
  invalid_node_count: 0,
649
- last_deployed_at: null,
649
+ last_updated_at: null,
650
650
  },
651
651
  ];
652
652
 
@@ -797,7 +797,7 @@ describe('NamespacePage', () => {
797
797
  });
798
798
 
799
799
  describe('formatRelativeTime', () => {
800
- it('shows last_deployed_at timestamp on branch cards', async () => {
800
+ it('shows last_updated_at timestamp on branch cards', async () => {
801
801
  mockDjClient.getNamespaceGitConfig.mockResolvedValue({
802
802
  github_repo_path: 'org/repo',
803
803
  git_branch: 'main',
@@ -811,7 +811,7 @@ describe('NamespacePage', () => {
811
811
  git_branch: 'main',
812
812
  num_nodes: 3,
813
813
  invalid_node_count: 0,
814
- last_deployed_at: new Date(
814
+ last_updated_at: new Date(
815
815
  Date.now() - 2 * 24 * 60 * 60 * 1000,
816
816
  ).toISOString(),
817
817
  },
@@ -56,7 +56,7 @@ function DefaultBranchPreview({ groups, defaultBranchNs }) {
56
56
  margin: '20px',
57
57
  }}
58
58
  >
59
- {filtered.map(({ type, nodes: typeNodes, hasMore }, idx) => {
59
+ {filtered.map(({ type, nodes: typeNodes, hasMore, totalCount }, idx) => {
60
60
  const shown = typeNodes;
61
61
  const isLeftCol = idx % 2 === 0;
62
62
  return (
@@ -100,7 +100,8 @@ function DefaultBranchPreview({ groups, defaultBranchNs }) {
100
100
  borderRadius: '8px',
101
101
  }}
102
102
  >
103
- {typeNodes.length}
103
+ {totalCount ??
104
+ (hasMore ? `${MAX_PER_TYPE}+` : typeNodes.length)}
104
105
  </span>
105
106
  </span>
106
107
  {hasMore && (
@@ -440,6 +441,8 @@ export function NamespacePage() {
440
441
  )
441
442
  .then(result => {
442
443
  const edges = result?.data?.findNodesPaginated?.edges ?? [];
444
+ const totalCount =
445
+ result?.data?.findNodesPaginated?.totalCount ?? null;
443
446
  const nodes = edges.map(e => ({
444
447
  ...e.node,
445
448
  status: e.node.current?.status,
@@ -449,9 +452,10 @@ export function NamespacePage() {
449
452
  type,
450
453
  nodes: nodes.slice(0, MAX_PER_TYPE),
451
454
  hasMore: nodes.length > MAX_PER_TYPE,
455
+ totalCount,
452
456
  };
453
457
  })
454
- .catch(() => ({ type, nodes: [], hasMore: false })),
458
+ .catch(() => ({ type, nodes: [], hasMore: false, totalCount: null })),
455
459
  ),
456
460
  )
457
461
  .then(groups => setDefaultBranchGroups(groups))
@@ -1430,7 +1434,7 @@ export function NamespacePage() {
1430
1434
  {b.invalid_node_count} invalid
1431
1435
  </span>
1432
1436
  )}
1433
- {b.last_deployed_at && (
1437
+ {b.last_updated_at && (
1434
1438
  <span
1435
1439
  style={{
1436
1440
  display: 'flex',
@@ -1438,9 +1442,9 @@ export function NamespacePage() {
1438
1442
  gap: '3px',
1439
1443
  color: '#94a3b8',
1440
1444
  }}
1441
- title={new Date(
1442
- b.last_deployed_at,
1443
- ).toLocaleString()}
1445
+ title={`Last node update: ${new Date(
1446
+ b.last_updated_at,
1447
+ ).toLocaleString()}`}
1444
1448
  >
1445
1449
  <svg
1446
1450
  xmlns="http://www.w3.org/2000/svg"
@@ -1456,7 +1460,7 @@ export function NamespacePage() {
1456
1460
  <circle cx="12" cy="12" r="10" />
1457
1461
  <polyline points="12 6 12 12 16 14" />
1458
1462
  </svg>
1459
- {formatRelativeTime(b.last_deployed_at)}
1463
+ {formatRelativeTime(b.last_updated_at)}
1460
1464
  </span>
1461
1465
  )}
1462
1466
  </div>
@@ -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__${
@@ -60,6 +60,7 @@ export const DataJunctionAPI = {
60
60
  hasPrevPage
61
61
  startCursor
62
62
  }
63
+ totalCount
63
64
  edges {
64
65
  node {
65
66
  name
@@ -572,10 +573,12 @@ export const DataJunctionAPI = {
572
573
  mode
573
574
  cubeMetrics {
574
575
  name
576
+ displayName
575
577
  }
576
578
  cubeDimensions {
577
579
  name
578
580
  attribute
581
+ role
579
582
  properties
580
583
  }
581
584
  }
@@ -1047,6 +1050,73 @@ export const DataJunctionAPI = {
1047
1050
  ).json();
1048
1051
  },
1049
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
+
1050
1120
  commonDimensions: async function (metrics) {
1051
1121
  const metricsQuery = '?' + metrics.map(m => `metric=${m}`).join('&');
1052
1122
  return await (