datajunction-ui 0.0.92 → 0.0.94

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NodeComponents.jsx +4 -0
  3. package/src/app/components/Tab.jsx +11 -16
  4. package/src/app/components/__tests__/Tab.test.jsx +4 -2
  5. package/src/app/hooks/useWorkspaceData.js +226 -0
  6. package/src/app/index.tsx +17 -1
  7. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
  8. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
  9. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
  10. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
  11. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
  12. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
  13. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
  14. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
  15. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
  16. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
  17. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
  18. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
  20. package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
  21. package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
  22. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
  23. package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
  24. package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
  25. package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
  26. package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
  27. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
  28. package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
  29. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
  30. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
  31. package/src/app/pages/NodePage/index.jsx +15 -8
  32. package/src/app/services/DJService.js +73 -6
  33. package/src/app/services/__tests__/DJService.test.jsx +591 -0
  34. package/src/styles/index.css +32 -0
@@ -175,6 +175,185 @@ describe('<NotificationsSection />', () => {
175
175
  expect(link).toHaveAttribute('href', '/nodes/default.test_metric/history');
176
176
  });
177
177
 
178
+ it('should show username part for single notification from another user', () => {
179
+ const otherUserNotification = [
180
+ {
181
+ entity_name: 'default.other_metric',
182
+ activity_type: 'update',
183
+ created_at: '2024-01-01T00:00:00Z',
184
+ user: 'other.person@example.com',
185
+ node_type: 'metric',
186
+ display_name: 'Other Metric',
187
+ },
188
+ ];
189
+
190
+ render(
191
+ <MemoryRouter>
192
+ <NotificationsSection
193
+ notifications={otherUserNotification}
194
+ username="test.user@example.com"
195
+ loading={false}
196
+ />
197
+ </MemoryRouter>,
198
+ );
199
+
200
+ // Should show "other.person" (split on @)
201
+ expect(screen.getByText(/by other\.person/)).toBeInTheDocument();
202
+ });
203
+
204
+ it('should show "you + N others" when current user is among multiple updaters', () => {
205
+ const multiUserNotifications = [
206
+ {
207
+ entity_name: 'default.shared_metric',
208
+ activity_type: 'update',
209
+ created_at: '2024-01-01T00:00:00Z',
210
+ user: 'test.user@example.com',
211
+ node_type: 'metric',
212
+ display_name: 'Shared Metric',
213
+ },
214
+ {
215
+ entity_name: 'default.shared_metric',
216
+ activity_type: 'update',
217
+ created_at: '2024-01-02T00:00:00Z',
218
+ user: 'alice@example.com',
219
+ node_type: 'metric',
220
+ display_name: 'Shared Metric',
221
+ },
222
+ {
223
+ entity_name: 'default.shared_metric',
224
+ activity_type: 'update',
225
+ created_at: '2024-01-03T00:00:00Z',
226
+ user: 'bob@example.com',
227
+ node_type: 'metric',
228
+ display_name: 'Shared Metric',
229
+ },
230
+ ];
231
+
232
+ render(
233
+ <MemoryRouter>
234
+ <NotificationsSection
235
+ notifications={multiUserNotifications}
236
+ username="test.user@example.com"
237
+ loading={false}
238
+ />
239
+ </MemoryRouter>,
240
+ );
241
+
242
+ // 3 unique users including current user → "you + 2 others"
243
+ expect(screen.getByText(/you \+ 2 others/)).toBeInTheDocument();
244
+ });
245
+
246
+ it('should show "N users" when multiple users not including current user', () => {
247
+ const otherUsersNotifications = [
248
+ {
249
+ entity_name: 'default.their_metric',
250
+ activity_type: 'update',
251
+ created_at: '2024-01-01T00:00:00Z',
252
+ user: 'alice@example.com',
253
+ node_type: 'metric',
254
+ display_name: 'Their Metric',
255
+ },
256
+ {
257
+ entity_name: 'default.their_metric',
258
+ activity_type: 'update',
259
+ created_at: '2024-01-02T00:00:00Z',
260
+ user: 'bob@example.com',
261
+ node_type: 'metric',
262
+ display_name: 'Their Metric',
263
+ },
264
+ ];
265
+
266
+ render(
267
+ <MemoryRouter>
268
+ <NotificationsSection
269
+ notifications={otherUsersNotifications}
270
+ username="test.user@example.com"
271
+ loading={false}
272
+ />
273
+ </MemoryRouter>,
274
+ );
275
+
276
+ expect(screen.getByText(/2 users/)).toBeInTheDocument();
277
+ });
278
+
279
+ it('should show "you" when users array is empty and mostRecent.user is current user', () => {
280
+ const nullUserNotification = [
281
+ {
282
+ entity_name: 'default.my_metric',
283
+ activity_type: 'update',
284
+ created_at: '2024-01-01T00:00:00Z',
285
+ user: 'test.user@example.com',
286
+ // user field is present but filter(u => u != null && u !== '') keeps it
287
+ // To hit the users.length === 0 branch we need user=null or user=''
288
+ },
289
+ ];
290
+
291
+ // Use a notification where user is null/empty so allUsers filter removes it
292
+ const emptyUserNotification = [
293
+ {
294
+ entity_name: 'default.my_metric',
295
+ activity_type: 'update',
296
+ created_at: '2024-01-01T00:00:00Z',
297
+ user: null, // filtered out → users.length === 0
298
+ node_type: 'metric',
299
+ display_name: 'My Metric',
300
+ details: { version: 'v1' },
301
+ },
302
+ ];
303
+ // mostRecent.user is null → falls to 'unknown'
304
+ render(
305
+ <MemoryRouter>
306
+ <NotificationsSection
307
+ notifications={emptyUserNotification}
308
+ username="test.user@example.com"
309
+ loading={false}
310
+ />
311
+ </MemoryRouter>,
312
+ );
313
+
314
+ expect(screen.getByText(/by unknown/)).toBeInTheDocument();
315
+ });
316
+
317
+ it('should show "you" in zero-users fallback when mostRecent.user matches username', () => {
318
+ const emptyStringUserNotification = [
319
+ {
320
+ entity_name: 'default.my_metric',
321
+ activity_type: 'update',
322
+ created_at: '2024-01-01T00:00:00Z',
323
+ user: '', // filtered out → users.length === 0
324
+ node_type: 'metric',
325
+ display_name: 'My Metric',
326
+ },
327
+ ];
328
+ // mostRecent.user is '' which !== username → goes to split('@')[0] || 'unknown' → 'unknown'
329
+ // To get "you" in zero-users path: mostRecent.user must equal username
330
+ // We need user field stored (not filtered) to equal username but filtered...
331
+ // Actually user='' is filtered. Let's set user=username so it passes filter and users=[username]
332
+ // → users.length === 1 and users[0] === username → "you"
333
+ const selfNotification = [
334
+ {
335
+ entity_name: 'default.my_metric',
336
+ activity_type: 'update',
337
+ created_at: '2024-01-01T00:00:00Z',
338
+ user: 'test.user@example.com',
339
+ node_type: 'metric',
340
+ display_name: 'My Metric',
341
+ },
342
+ ];
343
+
344
+ render(
345
+ <MemoryRouter>
346
+ <NotificationsSection
347
+ notifications={selfNotification}
348
+ username="test.user@example.com"
349
+ loading={false}
350
+ />
351
+ </MemoryRouter>,
352
+ );
353
+
354
+ expect(screen.getByText(/by you/)).toBeInTheDocument();
355
+ });
356
+
178
357
  it('should limit notifications to 15', () => {
179
358
  const manyNotifications = Array.from({ length: 20 }, (_, i) => ({
180
359
  entity_name: `default.metric_${i}`,
@@ -23,7 +23,7 @@ describe('<TypeGroupGrid />', () => {
23
23
  const mockGroupedData = [
24
24
  {
25
25
  type: 'metric',
26
- count: 5,
26
+ hasMore: false,
27
27
  nodes: [
28
28
  {
29
29
  name: 'default.revenue',
@@ -63,7 +63,7 @@ describe('<TypeGroupGrid />', () => {
63
63
  },
64
64
  {
65
65
  type: 'dimension',
66
- count: 2,
66
+ hasMore: false,
67
67
  nodes: [
68
68
  {
69
69
  name: 'default.dim_users',
@@ -96,7 +96,7 @@ describe('<TypeGroupGrid />', () => {
96
96
  expect(screen.getByText('No nodes to display')).toBeInTheDocument();
97
97
  });
98
98
 
99
- it('should render type cards with correct counts', () => {
99
+ it('should render type cards', () => {
100
100
  render(
101
101
  <MemoryRouter>
102
102
  <TypeGroupGrid
@@ -107,8 +107,8 @@ describe('<TypeGroupGrid />', () => {
107
107
  </MemoryRouter>,
108
108
  );
109
109
 
110
- expect(screen.getByText('Metrics (5)')).toBeInTheDocument();
111
- expect(screen.getByText('Dimensions (2)')).toBeInTheDocument();
110
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
111
+ expect(screen.getByText('Dimensions')).toBeInTheDocument();
112
112
  });
113
113
 
114
114
  it('should display up to 10 nodes per type', () => {
@@ -130,12 +130,12 @@ describe('<TypeGroupGrid />', () => {
130
130
  expect(screen.getByText('default.bounce_rate')).toBeInTheDocument();
131
131
  });
132
132
 
133
- it('should show "+X more" link when more than 10 nodes', () => {
134
- const manyNodesData = [
133
+ it('should show "More " link when hasMore is true', () => {
134
+ const hasMoreData = [
135
135
  {
136
136
  type: 'metric',
137
- count: 15,
138
- nodes: Array.from({ length: 15 }, (_, i) => ({
137
+ hasMore: true,
138
+ nodes: Array.from({ length: 11 }, (_, i) => ({
139
139
  name: `default.metric_${i}`,
140
140
  type: 'metric',
141
141
  current: {
@@ -148,18 +148,17 @@ describe('<TypeGroupGrid />', () => {
148
148
  render(
149
149
  <MemoryRouter>
150
150
  <TypeGroupGrid
151
- groupedData={manyNodesData}
151
+ groupedData={hasMoreData}
152
152
  username="test.user@example.com"
153
153
  activeTab="owned"
154
154
  />
155
155
  </MemoryRouter>,
156
156
  );
157
157
 
158
- // Metrics: 15 nodes, showing 10, so +5 more
159
- expect(screen.getByText('+5 more →')).toBeInTheDocument();
158
+ expect(screen.getByText('More →')).toBeInTheDocument();
160
159
  });
161
160
 
162
- it('should not show "+X more" link when 10 or fewer nodes', () => {
161
+ it('should not show "More " link when hasMore is false', () => {
163
162
  render(
164
163
  <MemoryRouter>
165
164
  <TypeGroupGrid
@@ -170,17 +169,7 @@ describe('<TypeGroupGrid />', () => {
170
169
  </MemoryRouter>,
171
170
  );
172
171
 
173
- // Metrics: 5 nodes (under 10), no "+X more" needed
174
- const metricCard = screen
175
- .getByText('Metrics (5)')
176
- .closest('.type-group-card');
177
- expect(metricCard).not.toHaveTextContent('more →');
178
-
179
- // Dimensions: 2 nodes, no "+X more" needed
180
- const dimensionCard = screen
181
- .getByText('Dimensions (2)')
182
- .closest('.type-group-card');
183
- expect(dimensionCard).not.toHaveTextContent('more →');
172
+ expect(screen.queryByText('More →')).not.toBeInTheDocument();
184
173
  });
185
174
 
186
175
  it('should render node badges and links', () => {
@@ -227,7 +216,7 @@ describe('<TypeGroupGrid />', () => {
227
216
  const recentNodes = [
228
217
  {
229
218
  type: 'metric',
230
- count: 2,
219
+ hasMore: false,
231
220
  nodes: [
232
221
  {
233
222
  name: 'default.recent',
@@ -260,16 +249,15 @@ describe('<TypeGroupGrid />', () => {
260
249
  );
261
250
 
262
251
  // Should show time in hours or days format
263
- // Note: exact values depend on when test runs, so we just check they exist
264
252
  expect(screen.getAllByText(/\d+[mhd]$/)).toHaveLength(2);
265
253
  });
266
254
 
267
255
  it('should generate correct filter URLs for owned tab', () => {
268
- const manyNodesData = [
256
+ const hasMoreData = [
269
257
  {
270
258
  type: 'metric',
271
- count: 15,
272
- nodes: Array.from({ length: 15 }, (_, i) => ({
259
+ hasMore: true,
260
+ nodes: Array.from({ length: 11 }, (_, i) => ({
273
261
  name: `default.metric_${i}`,
274
262
  type: 'metric',
275
263
  current: {
@@ -282,14 +270,14 @@ describe('<TypeGroupGrid />', () => {
282
270
  render(
283
271
  <MemoryRouter>
284
272
  <TypeGroupGrid
285
- groupedData={manyNodesData}
273
+ groupedData={hasMoreData}
286
274
  username="test.user@example.com"
287
275
  activeTab="owned"
288
276
  />
289
277
  </MemoryRouter>,
290
278
  );
291
279
 
292
- const moreLink = screen.getByText('+5 more →');
280
+ const moreLink = screen.getByText('More →');
293
281
  expect(moreLink).toHaveAttribute(
294
282
  'href',
295
283
  '/?ownedBy=test.user%40example.com&type=metric',
@@ -297,11 +285,11 @@ describe('<TypeGroupGrid />', () => {
297
285
  });
298
286
 
299
287
  it('should generate correct filter URLs for edited tab', () => {
300
- const manyNodesData = [
288
+ const hasMoreData = [
301
289
  {
302
290
  type: 'metric',
303
- count: 15,
304
- nodes: Array.from({ length: 15 }, (_, i) => ({
291
+ hasMore: true,
292
+ nodes: Array.from({ length: 11 }, (_, i) => ({
305
293
  name: `default.metric_${i}`,
306
294
  type: 'metric',
307
295
  current: {
@@ -314,14 +302,14 @@ describe('<TypeGroupGrid />', () => {
314
302
  render(
315
303
  <MemoryRouter>
316
304
  <TypeGroupGrid
317
- groupedData={manyNodesData}
305
+ groupedData={hasMoreData}
318
306
  username="test.user@example.com"
319
307
  activeTab="edited"
320
308
  />
321
309
  </MemoryRouter>,
322
310
  );
323
311
 
324
- const moreLink = screen.getByText('+5 more →');
312
+ const moreLink = screen.getByText('More →');
325
313
  expect(moreLink).toHaveAttribute(
326
314
  'href',
327
315
  '/?updatedBy=test.user%40example.com&type=metric',
@@ -340,16 +328,16 @@ describe('<TypeGroupGrid />', () => {
340
328
  );
341
329
 
342
330
  // "metric" should be displayed as "Metrics"
343
- expect(screen.getByText('Metrics (5)')).toBeInTheDocument();
331
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
344
332
  // "dimension" should be displayed as "Dimensions"
345
- expect(screen.getByText('Dimensions (2)')).toBeInTheDocument();
333
+ expect(screen.getByText('Dimensions')).toBeInTheDocument();
346
334
  });
347
335
 
348
336
  it('should handle nodes with only repo (no branch)', () => {
349
337
  const repoOnlyData = [
350
338
  {
351
339
  type: 'metric',
352
- count: 1,
340
+ hasMore: false,
353
341
  nodes: [
354
342
  {
355
343
  name: 'default.test',
@@ -380,7 +368,7 @@ describe('<TypeGroupGrid />', () => {
380
368
  const branchOnlyData = [
381
369
  {
382
370
  type: 'metric',
383
- count: 1,
371
+ hasMore: false,
384
372
  nodes: [
385
373
  {
386
374
  name: 'default.test',
@@ -411,7 +399,7 @@ describe('<TypeGroupGrid />', () => {
411
399
  const noGitData = [
412
400
  {
413
401
  type: 'metric',
414
- count: 1,
402
+ hasMore: false,
415
403
  nodes: [
416
404
  {
417
405
  name: 'default.test',
@@ -440,7 +428,7 @@ describe('<TypeGroupGrid />', () => {
440
428
  const invalidNodeData = [
441
429
  {
442
430
  type: 'metric',
443
- count: 1,
431
+ hasMore: false,
444
432
  nodes: [
445
433
  {
446
434
  name: 'default.invalid_metric',
@@ -469,7 +457,7 @@ describe('<TypeGroupGrid />', () => {
469
457
  const draftNodeData = [
470
458
  {
471
459
  type: 'metric',
472
- count: 1,
460
+ hasMore: false,
473
461
  nodes: [
474
462
  {
475
463
  name: 'default.draft_metric',
@@ -494,11 +482,143 @@ describe('<TypeGroupGrid />', () => {
494
482
  expect(screen.getByTitle('Draft mode')).toBeInTheDocument();
495
483
  });
496
484
 
485
+ it('should show star icon for default branch nodes', () => {
486
+ const defaultBranchData = [
487
+ {
488
+ type: 'metric',
489
+ hasMore: false,
490
+ nodes: [
491
+ {
492
+ name: 'default.test',
493
+ type: 'metric',
494
+ gitInfo: {
495
+ repo: 'myorg/myrepo',
496
+ branch: 'main',
497
+ defaultBranch: 'main',
498
+ },
499
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
500
+ },
501
+ ],
502
+ },
503
+ ];
504
+
505
+ render(
506
+ <MemoryRouter>
507
+ <TypeGroupGrid
508
+ groupedData={defaultBranchData}
509
+ username="test.user@example.com"
510
+ activeTab="owned"
511
+ />
512
+ </MemoryRouter>,
513
+ );
514
+
515
+ // Star icon should appear for default branch
516
+ expect(screen.getByText('⭐')).toBeInTheDocument();
517
+ });
518
+
519
+ it('should not show star icon for non-default branch nodes', () => {
520
+ const featureBranchData = [
521
+ {
522
+ type: 'metric',
523
+ hasMore: false,
524
+ nodes: [
525
+ {
526
+ name: 'default.test',
527
+ type: 'metric',
528
+ gitInfo: {
529
+ repo: 'myorg/myrepo',
530
+ branch: 'feature-branch',
531
+ defaultBranch: 'main',
532
+ },
533
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
534
+ },
535
+ ],
536
+ },
537
+ ];
538
+
539
+ render(
540
+ <MemoryRouter>
541
+ <TypeGroupGrid
542
+ groupedData={featureBranchData}
543
+ username="test.user@example.com"
544
+ activeTab="owned"
545
+ />
546
+ </MemoryRouter>,
547
+ );
548
+
549
+ expect(screen.queryByText('⭐')).not.toBeInTheDocument();
550
+ });
551
+
552
+ it('should show SVG git-branch separator when both repo and branch are present', () => {
553
+ const repoPlusBranchData = [
554
+ {
555
+ type: 'metric',
556
+ hasMore: false,
557
+ nodes: [
558
+ {
559
+ name: 'default.test',
560
+ type: 'metric',
561
+ gitInfo: {
562
+ repo: 'myorg/myrepo',
563
+ branch: 'main',
564
+ defaultBranch: 'main',
565
+ },
566
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
567
+ },
568
+ ],
569
+ },
570
+ ];
571
+
572
+ const { container } = render(
573
+ <MemoryRouter>
574
+ <TypeGroupGrid
575
+ groupedData={repoPlusBranchData}
576
+ username="test.user@example.com"
577
+ activeTab="owned"
578
+ />
579
+ </MemoryRouter>,
580
+ );
581
+
582
+ // SVG separator should be present when both repo and branch exist
583
+ expect(container.querySelector('svg')).toBeInTheDocument();
584
+ });
585
+
586
+ it('should not show SVG separator when only repo (no branch)', () => {
587
+ const repoOnlyData = [
588
+ {
589
+ type: 'metric',
590
+ hasMore: false,
591
+ nodes: [
592
+ {
593
+ name: 'default.test',
594
+ type: 'metric',
595
+ gitInfo: {
596
+ repo: 'myorg/myrepo',
597
+ },
598
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
599
+ },
600
+ ],
601
+ },
602
+ ];
603
+
604
+ const { container } = render(
605
+ <MemoryRouter>
606
+ <TypeGroupGrid
607
+ groupedData={repoOnlyData}
608
+ username="test.user@example.com"
609
+ activeTab="owned"
610
+ />
611
+ </MemoryRouter>,
612
+ );
613
+
614
+ expect(container.querySelector('svg')).not.toBeInTheDocument();
615
+ });
616
+
497
617
  it('should handle nodes without updatedAt', () => {
498
618
  const noTimeData = [
499
619
  {
500
620
  type: 'metric',
501
- count: 1,
621
+ hasMore: false,
502
622
  nodes: [
503
623
  {
504
624
  name: 'default.test',
@@ -524,11 +644,11 @@ describe('<TypeGroupGrid />', () => {
524
644
  });
525
645
 
526
646
  it('should handle watched tab filter URLs', () => {
527
- const manyNodesData = [
647
+ const hasMoreData = [
528
648
  {
529
649
  type: 'metric',
530
- count: 15,
531
- nodes: Array.from({ length: 15 }, (_, i) => ({
650
+ hasMore: true,
651
+ nodes: Array.from({ length: 11 }, (_, i) => ({
532
652
  name: `default.metric_${i}`,
533
653
  type: 'metric',
534
654
  current: {
@@ -542,14 +662,14 @@ describe('<TypeGroupGrid />', () => {
542
662
  render(
543
663
  <MemoryRouter>
544
664
  <TypeGroupGrid
545
- groupedData={manyNodesData}
665
+ groupedData={hasMoreData}
546
666
  username="test.user@example.com"
547
667
  activeTab="watched"
548
668
  />
549
669
  </MemoryRouter>,
550
670
  );
551
671
 
552
- const moreLink = screen.getByText('+5 more →');
672
+ const moreLink = screen.getByText('More →');
553
673
  // For watched tab, should filter by type only
554
674
  expect(moreLink).toHaveAttribute('href', '/?type=metric');
555
675
  });