datajunction-ui 0.0.74 → 0.0.76

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/DashboardCard.jsx +93 -0
  3. package/src/app/components/NodeComponents.jsx +173 -0
  4. package/src/app/components/NodeListActions.jsx +8 -3
  5. package/src/app/components/__tests__/NodeComponents.test.jsx +262 -0
  6. package/src/app/hooks/__tests__/useWorkspaceData.test.js +533 -0
  7. package/src/app/hooks/useWorkspaceData.js +357 -0
  8. package/src/app/index.tsx +6 -0
  9. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +344 -0
  10. package/src/app/pages/MyWorkspacePage/CollectionsSection.jsx +188 -0
  11. package/src/app/pages/MyWorkspacePage/Loadable.jsx +6 -0
  12. package/src/app/pages/MyWorkspacePage/MaterializationsSection.jsx +190 -0
  13. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +342 -0
  14. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +632 -0
  15. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +185 -0
  16. package/src/app/pages/MyWorkspacePage/NodeList.jsx +46 -0
  17. package/src/app/pages/MyWorkspacePage/NotificationsSection.jsx +133 -0
  18. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +209 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +295 -0
  20. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +278 -0
  21. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +238 -0
  22. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +389 -0
  23. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +347 -0
  24. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +272 -0
  25. package/src/app/pages/MyWorkspacePage/__tests__/NodeList.test.jsx +162 -0
  26. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +204 -0
  27. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +556 -0
  28. package/src/app/pages/MyWorkspacePage/index.jsx +150 -0
  29. package/src/app/services/DJService.js +323 -2
@@ -0,0 +1,533 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { useContext } from 'react';
3
+ import {
4
+ useCurrentUser,
5
+ useWorkspaceOwnedNodes,
6
+ useWorkspaceRecentlyEdited,
7
+ useWorkspaceWatchedNodes,
8
+ useWorkspaceCollections,
9
+ useWorkspaceNotifications,
10
+ useWorkspaceMaterializations,
11
+ useWorkspaceNeedsAttention,
12
+ usePersonalNamespace,
13
+ } from '../useWorkspaceData';
14
+
15
+ jest.mock('react', () => ({
16
+ ...jest.requireActual('react'),
17
+ useContext: jest.fn(),
18
+ }));
19
+
20
+ const mockDjClient = {
21
+ whoami: jest.fn(),
22
+ getWorkspaceOwnedNodes: jest.fn(),
23
+ getWorkspaceRecentlyEdited: jest.fn(),
24
+ getNotificationPreferences: jest.fn(),
25
+ getNodesByNames: jest.fn(),
26
+ getWorkspaceCollections: jest.fn(),
27
+ getSubscribedHistory: jest.fn(),
28
+ getWorkspaceMaterializations: jest.fn(),
29
+ getWorkspaceNodesMissingDescription: jest.fn(),
30
+ getWorkspaceInvalidNodes: jest.fn(),
31
+ getWorkspaceDraftNodes: jest.fn(),
32
+ getWorkspaceOrphanedDimensions: jest.fn(),
33
+ namespaces: jest.fn(),
34
+ };
35
+
36
+ describe('useWorkspaceData hooks', () => {
37
+ beforeEach(() => {
38
+ jest.clearAllMocks();
39
+ useContext.mockReturnValue({ DataJunctionAPI: mockDjClient });
40
+ });
41
+
42
+ describe('useCurrentUser', () => {
43
+ it('should fetch current user successfully', async () => {
44
+ const mockUser = { username: 'test@example.com', id: 1 };
45
+ mockDjClient.whoami.mockResolvedValue(mockUser);
46
+
47
+ const { result } = renderHook(() => useCurrentUser());
48
+
49
+ expect(result.current.loading).toBe(true);
50
+
51
+ await waitFor(() => {
52
+ expect(result.current.loading).toBe(false);
53
+ });
54
+
55
+ expect(result.current.data).toEqual(mockUser);
56
+ expect(result.current.error).toBe(null);
57
+ });
58
+
59
+ it('should handle errors when fetching user', async () => {
60
+ const mockError = new Error('API error');
61
+ mockDjClient.whoami.mockRejectedValue(mockError);
62
+ const consoleErrorSpy = jest
63
+ .spyOn(console, 'error')
64
+ .mockImplementation(() => {});
65
+
66
+ const { result } = renderHook(() => useCurrentUser());
67
+
68
+ await waitFor(() => {
69
+ expect(result.current.loading).toBe(false);
70
+ });
71
+
72
+ expect(result.current.data).toBe(null);
73
+ expect(result.current.error).toEqual(mockError);
74
+ consoleErrorSpy.mockRestore();
75
+ });
76
+ });
77
+
78
+ describe('useWorkspaceOwnedNodes', () => {
79
+ it('should fetch owned nodes successfully', async () => {
80
+ const mockNodes = [
81
+ { name: 'node1', type: 'metric' },
82
+ { name: 'node2', type: 'dimension' },
83
+ ];
84
+ mockDjClient.getWorkspaceOwnedNodes.mockResolvedValue({
85
+ data: {
86
+ findNodesPaginated: {
87
+ edges: mockNodes.map(node => ({ node })),
88
+ },
89
+ },
90
+ });
91
+
92
+ const { result } = renderHook(() =>
93
+ useWorkspaceOwnedNodes('test@example.com'),
94
+ );
95
+
96
+ await waitFor(() => {
97
+ expect(result.current.loading).toBe(false);
98
+ });
99
+
100
+ expect(result.current.data).toEqual(mockNodes);
101
+ expect(result.current.error).toBe(null);
102
+ });
103
+
104
+ it('should handle empty username', async () => {
105
+ const { result } = renderHook(() => useWorkspaceOwnedNodes(null));
106
+
107
+ await waitFor(() => {
108
+ expect(result.current.loading).toBe(false);
109
+ });
110
+
111
+ expect(result.current.data).toEqual([]);
112
+ expect(mockDjClient.getWorkspaceOwnedNodes).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it('should handle errors', async () => {
116
+ const mockError = new Error('API error');
117
+ mockDjClient.getWorkspaceOwnedNodes.mockRejectedValue(mockError);
118
+ const consoleErrorSpy = jest
119
+ .spyOn(console, 'error')
120
+ .mockImplementation(() => {});
121
+
122
+ const { result } = renderHook(() =>
123
+ useWorkspaceOwnedNodes('test@example.com'),
124
+ );
125
+
126
+ await waitFor(() => {
127
+ expect(result.current.loading).toBe(false);
128
+ });
129
+
130
+ expect(result.current.error).toEqual(mockError);
131
+ consoleErrorSpy.mockRestore();
132
+ });
133
+ });
134
+
135
+ describe('useWorkspaceRecentlyEdited', () => {
136
+ it('should fetch recently edited nodes', async () => {
137
+ const mockNodes = [{ name: 'node1', type: 'metric' }];
138
+ mockDjClient.getWorkspaceRecentlyEdited.mockResolvedValue({
139
+ data: {
140
+ findNodesPaginated: {
141
+ edges: mockNodes.map(node => ({ node })),
142
+ },
143
+ },
144
+ });
145
+
146
+ const { result } = renderHook(() =>
147
+ useWorkspaceRecentlyEdited('test@example.com'),
148
+ );
149
+
150
+ await waitFor(() => {
151
+ expect(result.current.loading).toBe(false);
152
+ });
153
+
154
+ expect(result.current.data).toEqual(mockNodes);
155
+ });
156
+
157
+ it('should handle empty username', async () => {
158
+ const { result } = renderHook(() => useWorkspaceRecentlyEdited(null));
159
+
160
+ await waitFor(() => {
161
+ expect(result.current.loading).toBe(false);
162
+ });
163
+
164
+ expect(mockDjClient.getWorkspaceRecentlyEdited).not.toHaveBeenCalled();
165
+ });
166
+ });
167
+
168
+ describe('useWorkspaceWatchedNodes', () => {
169
+ it('should fetch watched nodes successfully', async () => {
170
+ const mockSubscriptions = [
171
+ { entity_name: 'node1' },
172
+ { entity_name: 'node2' },
173
+ ];
174
+ const mockNodes = [
175
+ { name: 'node1', type: 'metric' },
176
+ { name: 'node2', type: 'dimension' },
177
+ ];
178
+
179
+ mockDjClient.getNotificationPreferences.mockResolvedValue(
180
+ mockSubscriptions,
181
+ );
182
+ mockDjClient.getNodesByNames.mockResolvedValue(mockNodes);
183
+
184
+ const { result } = renderHook(() =>
185
+ useWorkspaceWatchedNodes('test@example.com'),
186
+ );
187
+
188
+ await waitFor(() => {
189
+ expect(result.current.loading).toBe(false);
190
+ });
191
+
192
+ expect(result.current.data).toEqual(mockNodes);
193
+ expect(mockDjClient.getNodesByNames).toHaveBeenCalledWith([
194
+ 'node1',
195
+ 'node2',
196
+ ]);
197
+ });
198
+
199
+ it('should handle no watched nodes', async () => {
200
+ mockDjClient.getNotificationPreferences.mockResolvedValue([]);
201
+
202
+ const { result } = renderHook(() =>
203
+ useWorkspaceWatchedNodes('test@example.com'),
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(result.current.loading).toBe(false);
208
+ });
209
+
210
+ expect(result.current.data).toEqual([]);
211
+ expect(mockDjClient.getNodesByNames).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it('should handle empty username', async () => {
215
+ const { result } = renderHook(() => useWorkspaceWatchedNodes(null));
216
+
217
+ await waitFor(() => {
218
+ expect(result.current.loading).toBe(false);
219
+ });
220
+
221
+ expect(mockDjClient.getNotificationPreferences).not.toHaveBeenCalled();
222
+ });
223
+ });
224
+
225
+ describe('useWorkspaceCollections', () => {
226
+ it('should fetch collections successfully', async () => {
227
+ const mockCollections = [
228
+ { name: 'collection1', nodeCount: 5 },
229
+ { name: 'collection2', nodeCount: 10 },
230
+ ];
231
+ mockDjClient.getWorkspaceCollections.mockResolvedValue({
232
+ data: { listCollections: mockCollections },
233
+ });
234
+
235
+ const { result } = renderHook(() =>
236
+ useWorkspaceCollections('test@example.com'),
237
+ );
238
+
239
+ await waitFor(() => {
240
+ expect(result.current.loading).toBe(false);
241
+ });
242
+
243
+ expect(result.current.data).toEqual(mockCollections);
244
+ });
245
+
246
+ it('should handle empty username', async () => {
247
+ const { result } = renderHook(() => useWorkspaceCollections(null));
248
+
249
+ await waitFor(() => {
250
+ expect(result.current.loading).toBe(false);
251
+ });
252
+
253
+ expect(mockDjClient.getWorkspaceCollections).not.toHaveBeenCalled();
254
+ });
255
+ });
256
+
257
+ describe('useWorkspaceNotifications', () => {
258
+ it('should fetch and enrich notifications', async () => {
259
+ const mockHistory = [
260
+ { entity_name: 'node1', activity: 'updated' },
261
+ { entity_name: 'node2', activity: 'created' },
262
+ ];
263
+ const mockNodes = [
264
+ {
265
+ name: 'node1',
266
+ type: 'metric',
267
+ current: { displayName: 'Node 1' },
268
+ },
269
+ {
270
+ name: 'node2',
271
+ type: 'dimension',
272
+ current: { displayName: 'Node 2' },
273
+ },
274
+ ];
275
+
276
+ mockDjClient.getSubscribedHistory.mockResolvedValue(mockHistory);
277
+ mockDjClient.getNodesByNames.mockResolvedValue(mockNodes);
278
+
279
+ const { result } = renderHook(() =>
280
+ useWorkspaceNotifications('test@example.com'),
281
+ );
282
+
283
+ await waitFor(() => {
284
+ expect(result.current.loading).toBe(false);
285
+ });
286
+
287
+ expect(result.current.data).toEqual([
288
+ {
289
+ entity_name: 'node1',
290
+ activity: 'updated',
291
+ node_type: 'metric',
292
+ display_name: 'Node 1',
293
+ },
294
+ {
295
+ entity_name: 'node2',
296
+ activity: 'created',
297
+ node_type: 'dimension',
298
+ display_name: 'Node 2',
299
+ },
300
+ ]);
301
+ });
302
+
303
+ it('should handle empty notifications', async () => {
304
+ mockDjClient.getSubscribedHistory.mockResolvedValue([]);
305
+
306
+ const { result } = renderHook(() =>
307
+ useWorkspaceNotifications('test@example.com'),
308
+ );
309
+
310
+ await waitFor(() => {
311
+ expect(result.current.loading).toBe(false);
312
+ });
313
+
314
+ expect(result.current.data).toEqual([]);
315
+ });
316
+
317
+ it('should handle empty username', async () => {
318
+ const { result } = renderHook(() => useWorkspaceNotifications(null));
319
+
320
+ await waitFor(() => {
321
+ expect(result.current.loading).toBe(false);
322
+ });
323
+
324
+ expect(mockDjClient.getSubscribedHistory).not.toHaveBeenCalled();
325
+ });
326
+ });
327
+
328
+ describe('useWorkspaceMaterializations', () => {
329
+ it('should fetch materializations successfully', async () => {
330
+ const mockNodes = [{ name: 'node1', type: 'metric' }];
331
+ mockDjClient.getWorkspaceMaterializations.mockResolvedValue({
332
+ data: {
333
+ findNodesPaginated: {
334
+ edges: mockNodes.map(node => ({ node })),
335
+ },
336
+ },
337
+ });
338
+
339
+ const { result } = renderHook(() =>
340
+ useWorkspaceMaterializations('test@example.com'),
341
+ );
342
+
343
+ await waitFor(() => {
344
+ expect(result.current.loading).toBe(false);
345
+ });
346
+
347
+ expect(result.current.data).toEqual(mockNodes);
348
+ });
349
+
350
+ it('should handle empty username', async () => {
351
+ const { result } = renderHook(() => useWorkspaceMaterializations(null));
352
+
353
+ await waitFor(() => {
354
+ expect(result.current.loading).toBe(false);
355
+ });
356
+
357
+ expect(mockDjClient.getWorkspaceMaterializations).not.toHaveBeenCalled();
358
+ });
359
+ });
360
+
361
+ describe('useWorkspaceNeedsAttention', () => {
362
+ it('should fetch all needs attention items', async () => {
363
+ const mockMissingDesc = [{ name: 'node1', type: 'metric' }];
364
+ const mockInvalid = [{ name: 'node2', type: 'dimension' }];
365
+ const mockDrafts = [
366
+ {
367
+ name: 'node3',
368
+ type: 'transform',
369
+ current: {
370
+ updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago
371
+ },
372
+ },
373
+ ];
374
+ const mockOrphaned = [{ name: 'node4', type: 'dimension' }];
375
+
376
+ mockDjClient.getWorkspaceNodesMissingDescription.mockResolvedValue({
377
+ data: {
378
+ findNodesPaginated: {
379
+ edges: mockMissingDesc.map(node => ({ node })),
380
+ },
381
+ },
382
+ });
383
+ mockDjClient.getWorkspaceInvalidNodes.mockResolvedValue({
384
+ data: {
385
+ findNodesPaginated: { edges: mockInvalid.map(node => ({ node })) },
386
+ },
387
+ });
388
+ mockDjClient.getWorkspaceDraftNodes.mockResolvedValue({
389
+ data: {
390
+ findNodesPaginated: { edges: mockDrafts.map(node => ({ node })) },
391
+ },
392
+ });
393
+ mockDjClient.getWorkspaceOrphanedDimensions.mockResolvedValue({
394
+ data: {
395
+ findNodesPaginated: { edges: mockOrphaned.map(node => ({ node })) },
396
+ },
397
+ });
398
+
399
+ const { result } = renderHook(() =>
400
+ useWorkspaceNeedsAttention('test@example.com'),
401
+ );
402
+
403
+ await waitFor(() => {
404
+ expect(result.current.loading).toBe(false);
405
+ });
406
+
407
+ expect(result.current.data.nodesMissingDescription).toEqual(
408
+ mockMissingDesc,
409
+ );
410
+ expect(result.current.data.invalidNodes).toEqual(mockInvalid);
411
+ expect(result.current.data.staleDrafts).toEqual(mockDrafts);
412
+ expect(result.current.data.orphanedDimensions).toEqual(mockOrphaned);
413
+ });
414
+
415
+ it('should filter out recent drafts', async () => {
416
+ const recentDraft = {
417
+ name: 'recent',
418
+ current: { updatedAt: new Date().toISOString() },
419
+ };
420
+ const staleDraft = {
421
+ name: 'stale',
422
+ current: {
423
+ updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000),
424
+ },
425
+ };
426
+
427
+ mockDjClient.getWorkspaceNodesMissingDescription.mockResolvedValue({
428
+ data: { findNodesPaginated: { edges: [] } },
429
+ });
430
+ mockDjClient.getWorkspaceInvalidNodes.mockResolvedValue({
431
+ data: { findNodesPaginated: { edges: [] } },
432
+ });
433
+ mockDjClient.getWorkspaceDraftNodes.mockResolvedValue({
434
+ data: {
435
+ findNodesPaginated: {
436
+ edges: [recentDraft, staleDraft].map(node => ({ node })),
437
+ },
438
+ },
439
+ });
440
+ mockDjClient.getWorkspaceOrphanedDimensions.mockResolvedValue({
441
+ data: { findNodesPaginated: { edges: [] } },
442
+ });
443
+
444
+ const { result } = renderHook(() =>
445
+ useWorkspaceNeedsAttention('test@example.com'),
446
+ );
447
+
448
+ await waitFor(() => {
449
+ expect(result.current.loading).toBe(false);
450
+ });
451
+
452
+ // Should only include stale draft (older than 7 days)
453
+ expect(result.current.data.staleDrafts).toEqual([staleDraft]);
454
+ });
455
+
456
+ it('should handle empty username', async () => {
457
+ const { result } = renderHook(() => useWorkspaceNeedsAttention(null));
458
+
459
+ await waitFor(() => {
460
+ expect(result.current.loading).toBe(false);
461
+ });
462
+
463
+ expect(
464
+ mockDjClient.getWorkspaceNodesMissingDescription,
465
+ ).not.toHaveBeenCalled();
466
+ });
467
+ });
468
+
469
+ describe('usePersonalNamespace', () => {
470
+ it('should check if personal namespace exists', async () => {
471
+ const mockNamespaces = [
472
+ { namespace: 'default' },
473
+ { namespace: 'users.testuser' },
474
+ ];
475
+ mockDjClient.namespaces.mockResolvedValue(mockNamespaces);
476
+
477
+ const { result } = renderHook(() =>
478
+ usePersonalNamespace('testuser@example.com'),
479
+ );
480
+
481
+ await waitFor(() => {
482
+ expect(result.current.loading).toBe(false);
483
+ });
484
+
485
+ expect(result.current.exists).toBe(true);
486
+ });
487
+
488
+ it('should return false if namespace does not exist', async () => {
489
+ const mockNamespaces = [{ namespace: 'default' }];
490
+ mockDjClient.namespaces.mockResolvedValue(mockNamespaces);
491
+
492
+ const { result } = renderHook(() =>
493
+ usePersonalNamespace('testuser@example.com'),
494
+ );
495
+
496
+ await waitFor(() => {
497
+ expect(result.current.loading).toBe(false);
498
+ });
499
+
500
+ expect(result.current.exists).toBe(false);
501
+ });
502
+
503
+ it('should handle empty username', async () => {
504
+ const { result } = renderHook(() => usePersonalNamespace(null));
505
+
506
+ await waitFor(() => {
507
+ expect(result.current.loading).toBe(false);
508
+ });
509
+
510
+ expect(mockDjClient.namespaces).not.toHaveBeenCalled();
511
+ });
512
+
513
+ it('should handle errors', async () => {
514
+ const mockError = new Error('API error');
515
+ mockDjClient.namespaces.mockRejectedValue(mockError);
516
+ const consoleErrorSpy = jest
517
+ .spyOn(console, 'error')
518
+ .mockImplementation(() => {});
519
+
520
+ const { result } = renderHook(() =>
521
+ usePersonalNamespace('testuser@example.com'),
522
+ );
523
+
524
+ await waitFor(() => {
525
+ expect(result.current.loading).toBe(false);
526
+ });
527
+
528
+ expect(result.current.exists).toBe(false);
529
+ expect(result.current.error).toEqual(mockError);
530
+ consoleErrorSpy.mockRestore();
531
+ });
532
+ });
533
+ });