datajunction-ui 0.0.75 → 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,295 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { ActiveBranchesSection } from '../ActiveBranchesSection';
5
+ import DJClientContext from '../../../providers/djclient';
6
+
7
+ jest.mock('../MyWorkspacePage.css', () => ({}));
8
+ jest.mock('../../../utils/date', () => ({
9
+ formatRelativeTime: () => '2d ago',
10
+ }));
11
+
12
+ describe('<ActiveBranchesSection />', () => {
13
+ const mockDjClient = {
14
+ listNodesForLanding: jest.fn().mockResolvedValue({
15
+ data: { findNodesPaginated: { edges: [] } },
16
+ }),
17
+ };
18
+
19
+ const renderWithContext = props => {
20
+ return render(
21
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
22
+ <MemoryRouter>
23
+ <ActiveBranchesSection {...props} />
24
+ </MemoryRouter>
25
+ </DJClientContext.Provider>,
26
+ );
27
+ };
28
+
29
+ const mockOwnedNodes = [
30
+ {
31
+ name: 'myproject.main.users',
32
+ gitInfo: {
33
+ repo: 'myorg/myrepo',
34
+ branch: 'main',
35
+ defaultBranch: 'main',
36
+ parentNamespace: 'myproject',
37
+ },
38
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
39
+ },
40
+ {
41
+ name: 'myproject.feature-1.orders',
42
+ gitInfo: {
43
+ repo: 'myorg/myrepo',
44
+ branch: 'feature-1',
45
+ defaultBranch: 'main',
46
+ parentNamespace: 'myproject',
47
+ },
48
+ current: { updatedAt: '2024-01-02T10:00:00Z' },
49
+ },
50
+ {
51
+ name: 'otherproject.main.products',
52
+ gitInfo: {
53
+ repo: 'myorg/otherrepo',
54
+ branch: 'main',
55
+ defaultBranch: 'main',
56
+ parentNamespace: 'otherproject',
57
+ },
58
+ current: { updatedAt: '2024-01-03T10:00:00Z' },
59
+ },
60
+ ];
61
+
62
+ const mockRecentlyEdited = [
63
+ {
64
+ name: 'myproject.feature-1.orders',
65
+ gitInfo: {
66
+ repo: 'myorg/myrepo',
67
+ branch: 'feature-1',
68
+ defaultBranch: 'main',
69
+ parentNamespace: 'myproject',
70
+ },
71
+ current: { updatedAt: '2024-01-02T10:00:00Z' },
72
+ },
73
+ ];
74
+
75
+ it('should render loading state', () => {
76
+ renderWithContext({
77
+ ownedNodes: [],
78
+ recentlyEdited: [],
79
+ loading: true,
80
+ });
81
+
82
+ expect(screen.getByText('Git Namespaces')).toBeInTheDocument();
83
+ });
84
+
85
+ it('should render empty state when no branches', async () => {
86
+ renderWithContext({
87
+ ownedNodes: [],
88
+ recentlyEdited: [],
89
+ loading: false,
90
+ });
91
+
92
+ await waitFor(() => {
93
+ expect(
94
+ screen.getByText('No git-managed namespaces.'),
95
+ ).toBeInTheDocument();
96
+ });
97
+ });
98
+
99
+ it('should group nodes by base namespace', async () => {
100
+ renderWithContext({
101
+ ownedNodes: mockOwnedNodes,
102
+ recentlyEdited: mockRecentlyEdited,
103
+ loading: false,
104
+ });
105
+
106
+ // Should show both base namespaces
107
+ await waitFor(() => {
108
+ expect(screen.getByText('myproject')).toBeInTheDocument();
109
+ expect(screen.getByText('otherproject')).toBeInTheDocument();
110
+ });
111
+ });
112
+
113
+ it('should show repo and branch names', async () => {
114
+ renderWithContext({
115
+ ownedNodes: mockOwnedNodes,
116
+ recentlyEdited: mockRecentlyEdited,
117
+ loading: false,
118
+ });
119
+
120
+ // Should show repos
121
+ await waitFor(() => {
122
+ expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
123
+ expect(screen.getByText('myorg/otherrepo')).toBeInTheDocument();
124
+ });
125
+
126
+ // Should show branches (there can be multiple "main" branches)
127
+ await waitFor(() => {
128
+ expect(screen.getAllByText('main').length).toBeGreaterThan(0);
129
+ expect(screen.getByText('feature-1')).toBeInTheDocument();
130
+ });
131
+ });
132
+
133
+ it('should handle nodes without git info', async () => {
134
+ const nodesWithoutGit = [
135
+ {
136
+ name: 'default.node1',
137
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
138
+ },
139
+ ...mockOwnedNodes,
140
+ ];
141
+
142
+ renderWithContext({
143
+ ownedNodes: nodesWithoutGit,
144
+ recentlyEdited: [],
145
+ loading: false,
146
+ });
147
+
148
+ // Should still render the git-managed nodes
149
+ await waitFor(() => {
150
+ expect(screen.getByText('myproject')).toBeInTheDocument();
151
+ expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
152
+ });
153
+ });
154
+
155
+ it('should render with many namespaces without crashing', async () => {
156
+ const manyNamespaces = Array.from({ length: 10 }, (_, i) => ({
157
+ name: `project${i}.main.node`,
158
+ gitInfo: {
159
+ repo: `org/repo${i}`,
160
+ branch: 'main',
161
+ defaultBranch: 'main',
162
+ parentNamespace: `project${i}`,
163
+ },
164
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
165
+ }));
166
+
167
+ renderWithContext({
168
+ ownedNodes: manyNamespaces,
169
+ recentlyEdited: [],
170
+ loading: false,
171
+ });
172
+
173
+ // Should render component successfully
174
+ expect(screen.getByText('Git Namespaces')).toBeInTheDocument();
175
+ // Should show some of the namespaces (maxDisplay is 3)
176
+ await waitFor(() => {
177
+ expect(screen.getByText('project0')).toBeInTheDocument();
178
+ });
179
+ });
180
+
181
+ it('should handle nodes without parentNamespace (fallback parsing)', async () => {
182
+ const nodesWithoutParent = [
183
+ {
184
+ name: 'repo1.branch1.node1',
185
+ gitInfo: {
186
+ repo: 'myorg/myrepo',
187
+ branch: 'branch1',
188
+ defaultBranch: 'main',
189
+ // No parentNamespace - should fallback to parsing
190
+ },
191
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
192
+ },
193
+ ];
194
+
195
+ renderWithContext({
196
+ ownedNodes: nodesWithoutParent,
197
+ recentlyEdited: [],
198
+ loading: false,
199
+ });
200
+
201
+ await waitFor(() => {
202
+ // Should extract "repo1" as base namespace
203
+ expect(screen.getByText('repo1')).toBeInTheDocument();
204
+ expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
205
+ });
206
+ });
207
+
208
+ it('should track most recent activity across multiple nodes', async () => {
209
+ const nodesWithDifferentTimes = [
210
+ {
211
+ name: 'myproject.main.node1',
212
+ gitInfo: {
213
+ repo: 'myorg/myrepo',
214
+ branch: 'main',
215
+ defaultBranch: 'main',
216
+ parentNamespace: 'myproject',
217
+ },
218
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
219
+ },
220
+ {
221
+ name: 'myproject.main.node2',
222
+ gitInfo: {
223
+ repo: 'myorg/myrepo',
224
+ branch: 'main',
225
+ defaultBranch: 'main',
226
+ parentNamespace: 'myproject',
227
+ },
228
+ current: { updatedAt: '2024-01-05T10:00:00Z' }, // More recent
229
+ },
230
+ ];
231
+
232
+ renderWithContext({
233
+ ownedNodes: nodesWithDifferentTimes,
234
+ recentlyEdited: [],
235
+ loading: false,
236
+ });
237
+
238
+ await waitFor(() => {
239
+ expect(screen.getByText('myproject')).toBeInTheDocument();
240
+ // Should show the more recent update time
241
+ expect(screen.getByText(/updated 2d ago/)).toBeInTheDocument();
242
+ });
243
+ });
244
+
245
+ it('should handle API errors when fetching counts', async () => {
246
+ const consoleErrorSpy = jest
247
+ .spyOn(console, 'error')
248
+ .mockImplementation(() => {});
249
+
250
+ mockDjClient.listNodesForLanding.mockRejectedValueOnce(
251
+ new Error('API error'),
252
+ );
253
+
254
+ renderWithContext({
255
+ ownedNodes: mockOwnedNodes,
256
+ recentlyEdited: [],
257
+ loading: false,
258
+ });
259
+
260
+ await waitFor(() => {
261
+ expect(screen.getByText('myproject')).toBeInTheDocument();
262
+ });
263
+
264
+ // Should still render despite API error
265
+ expect(consoleErrorSpy).toHaveBeenCalled();
266
+ consoleErrorSpy.mockRestore();
267
+ });
268
+
269
+ it('should show default branch even when user has no nodes on it', async () => {
270
+ const nodesOnlyFeatureBranch = [
271
+ {
272
+ name: 'myproject.feature.node1',
273
+ gitInfo: {
274
+ repo: 'myorg/myrepo',
275
+ branch: 'feature',
276
+ defaultBranch: 'main',
277
+ parentNamespace: 'myproject',
278
+ },
279
+ current: { updatedAt: '2024-01-01T10:00:00Z' },
280
+ },
281
+ ];
282
+
283
+ renderWithContext({
284
+ ownedNodes: nodesOnlyFeatureBranch,
285
+ recentlyEdited: [],
286
+ loading: false,
287
+ });
288
+
289
+ await waitFor(() => {
290
+ // Should show both the feature branch and the default main branch
291
+ expect(screen.getAllByText('main').length).toBeGreaterThan(0);
292
+ expect(screen.getByText('feature')).toBeInTheDocument();
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,278 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { CollectionsSection } from '../CollectionsSection';
5
+ import DJClientContext from '../../../providers/djclient';
6
+
7
+ jest.mock('../MyWorkspacePage.css', () => ({}));
8
+
9
+ describe('<CollectionsSection />', () => {
10
+ const mockDjClient = {
11
+ listAllCollections: jest.fn(),
12
+ };
13
+
14
+ const mockCollections = [
15
+ {
16
+ name: 'my_collection',
17
+ description: 'My test collection',
18
+ nodeCount: 5,
19
+ createdBy: { username: 'test.user@example.com' },
20
+ },
21
+ {
22
+ name: 'other_collection',
23
+ description: 'Another collection',
24
+ nodeCount: 10,
25
+ createdBy: { username: 'other.user@example.com' },
26
+ },
27
+ ];
28
+
29
+ const mockCurrentUser = {
30
+ username: 'test.user@example.com',
31
+ };
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ const renderWithContext = props => {
38
+ return render(
39
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
40
+ <MemoryRouter>
41
+ <CollectionsSection {...props} />
42
+ </MemoryRouter>
43
+ </DJClientContext.Provider>,
44
+ );
45
+ };
46
+
47
+ it('should render loading state', () => {
48
+ mockDjClient.listAllCollections.mockResolvedValue({
49
+ data: { listCollections: [] },
50
+ });
51
+
52
+ renderWithContext({
53
+ collections: [],
54
+ loading: true,
55
+ currentUser: mockCurrentUser,
56
+ });
57
+
58
+ expect(screen.getByText('Collections')).toBeInTheDocument();
59
+ });
60
+
61
+ it('should fetch and display all collections', async () => {
62
+ mockDjClient.listAllCollections.mockResolvedValue({
63
+ data: { listCollections: mockCollections },
64
+ });
65
+
66
+ renderWithContext({
67
+ collections: [],
68
+ loading: false,
69
+ currentUser: mockCurrentUser,
70
+ });
71
+
72
+ await waitFor(() => {
73
+ expect(mockDjClient.listAllCollections).toHaveBeenCalled();
74
+ });
75
+
76
+ await waitFor(() => {
77
+ expect(screen.getByText('my_collection')).toBeInTheDocument();
78
+ expect(screen.getByText('other_collection')).toBeInTheDocument();
79
+ });
80
+ });
81
+
82
+ it('should show owner as "you" for current user collections', async () => {
83
+ mockDjClient.listAllCollections.mockResolvedValue({
84
+ data: { listCollections: mockCollections },
85
+ });
86
+
87
+ renderWithContext({
88
+ collections: [],
89
+ loading: false,
90
+ currentUser: mockCurrentUser,
91
+ });
92
+
93
+ await waitFor(() => {
94
+ expect(screen.getByText('by you')).toBeInTheDocument();
95
+ });
96
+ });
97
+
98
+ it('should show username for other user collections', async () => {
99
+ mockDjClient.listAllCollections.mockResolvedValue({
100
+ data: { listCollections: mockCollections },
101
+ });
102
+
103
+ renderWithContext({
104
+ collections: [],
105
+ loading: false,
106
+ currentUser: mockCurrentUser,
107
+ });
108
+
109
+ await waitFor(() => {
110
+ expect(screen.getByText('by other.user')).toBeInTheDocument();
111
+ });
112
+ });
113
+
114
+ it('should display node counts', async () => {
115
+ mockDjClient.listAllCollections.mockResolvedValue({
116
+ data: { listCollections: mockCollections },
117
+ });
118
+
119
+ renderWithContext({
120
+ collections: [],
121
+ loading: false,
122
+ currentUser: mockCurrentUser,
123
+ });
124
+
125
+ await waitFor(() => {
126
+ expect(screen.getByText('5 nodes')).toBeInTheDocument();
127
+ expect(screen.getByText('10 nodes')).toBeInTheDocument();
128
+ });
129
+ });
130
+
131
+ it('should render empty state when no collections', async () => {
132
+ mockDjClient.listAllCollections.mockResolvedValue({
133
+ data: { listCollections: [] },
134
+ });
135
+
136
+ renderWithContext({
137
+ collections: [],
138
+ loading: false,
139
+ currentUser: mockCurrentUser,
140
+ });
141
+
142
+ await waitFor(() => {
143
+ expect(screen.getByText('No collections yet')).toBeInTheDocument();
144
+ });
145
+ });
146
+
147
+ it('should display user and other collections', async () => {
148
+ const collections = [
149
+ {
150
+ name: 'other_first',
151
+ description: 'Other collection',
152
+ createdBy: { username: 'other@example.com' },
153
+ nodeCount: 1,
154
+ },
155
+ {
156
+ name: 'my_first',
157
+ description: 'My collection',
158
+ createdBy: { username: 'test.user@example.com' },
159
+ nodeCount: 2,
160
+ },
161
+ ];
162
+
163
+ mockDjClient.listAllCollections.mockResolvedValue({
164
+ data: { listCollections: collections },
165
+ });
166
+
167
+ renderWithContext({
168
+ collections: [],
169
+ loading: false,
170
+ currentUser: mockCurrentUser,
171
+ });
172
+
173
+ await waitFor(() => {
174
+ // Both collections should be displayed
175
+ expect(screen.getByText('my_first')).toBeInTheDocument();
176
+ expect(screen.getByText('other_first')).toBeInTheDocument();
177
+ });
178
+ });
179
+
180
+ it('should limit display to 8 collections', async () => {
181
+ const manyCollections = Array.from({ length: 12 }, (_, i) => ({
182
+ name: `collection_${i}`,
183
+ nodeCount: i,
184
+ createdBy: { username: 'test@example.com' },
185
+ }));
186
+
187
+ mockDjClient.listAllCollections.mockResolvedValue({
188
+ data: { listCollections: manyCollections },
189
+ });
190
+
191
+ renderWithContext({
192
+ collections: [],
193
+ loading: false,
194
+ currentUser: mockCurrentUser,
195
+ });
196
+
197
+ await waitFor(() => {
198
+ const links = screen.getAllByRole('link');
199
+ // Should have at most 9 links (8 collections + 1 "Create Collection")
200
+ expect(links.length).toBeLessThanOrEqual(9);
201
+ });
202
+ });
203
+
204
+ it('should handle API errors gracefully', async () => {
205
+ mockDjClient.listAllCollections.mockRejectedValue(new Error('API error'));
206
+
207
+ const fallbackCollections = [
208
+ {
209
+ name: 'fallback_collection',
210
+ nodeCount: 1,
211
+ createdBy: { username: 'test@example.com' },
212
+ },
213
+ ];
214
+
215
+ renderWithContext({
216
+ collections: fallbackCollections,
217
+ loading: false,
218
+ currentUser: mockCurrentUser,
219
+ });
220
+
221
+ await waitFor(() => {
222
+ // Should fall back to props collections
223
+ expect(screen.getByText('fallback_collection')).toBeInTheDocument();
224
+ });
225
+ });
226
+
227
+ it('should handle collections without createdBy', async () => {
228
+ const collectionsWithoutOwner = [
229
+ {
230
+ name: 'no_owner',
231
+ nodeCount: 1,
232
+ createdBy: null,
233
+ },
234
+ ];
235
+
236
+ mockDjClient.listAllCollections.mockResolvedValue({
237
+ data: { listCollections: collectionsWithoutOwner },
238
+ });
239
+
240
+ renderWithContext({
241
+ collections: [],
242
+ loading: false,
243
+ currentUser: mockCurrentUser,
244
+ });
245
+
246
+ await waitFor(() => {
247
+ expect(screen.getByText('by unknown')).toBeInTheDocument();
248
+ });
249
+ });
250
+
251
+ it('should handle hover interactions on collection cards', async () => {
252
+ mockDjClient.listAllCollections.mockResolvedValue({
253
+ data: { listCollections: mockCollections },
254
+ });
255
+
256
+ renderWithContext({
257
+ collections: [],
258
+ loading: false,
259
+ currentUser: mockCurrentUser,
260
+ });
261
+
262
+ await waitFor(() => {
263
+ expect(screen.getByText('my_collection')).toBeInTheDocument();
264
+ });
265
+
266
+ const collectionLink = screen.getByText('my_collection').closest('a');
267
+
268
+ // Test mouse enter - should change styles
269
+ if (collectionLink) {
270
+ fireEvent.mouseEnter(collectionLink);
271
+ expect(collectionLink).toBeInTheDocument();
272
+
273
+ // Test mouse leave - should reset styles
274
+ fireEvent.mouseLeave(collectionLink);
275
+ expect(collectionLink).toBeInTheDocument();
276
+ }
277
+ });
278
+ });