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,357 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../providers/djclient';
3
+
4
+ /**
5
+ * Hook to fetch the current user
6
+ */
7
+ export function useCurrentUser() {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+ const [data, setData] = useState(null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [error, setError] = useState(null);
12
+
13
+ useEffect(() => {
14
+ const fetchUser = async () => {
15
+ try {
16
+ const user = await djClient.whoami();
17
+ setData(user);
18
+ } catch (err) {
19
+ console.error('Error fetching user:', err);
20
+ setError(err);
21
+ }
22
+ setLoading(false);
23
+ };
24
+ fetchUser();
25
+ }, [djClient]);
26
+
27
+ return { data, loading, error };
28
+ }
29
+
30
+ /**
31
+ * Hook to fetch workspace owned nodes
32
+ */
33
+ export function useWorkspaceOwnedNodes(username, limit = 5000) {
34
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
35
+ const [data, setData] = useState([]);
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState(null);
38
+
39
+ useEffect(() => {
40
+ if (!username) {
41
+ setLoading(false);
42
+ return;
43
+ }
44
+
45
+ const fetchData = async () => {
46
+ try {
47
+ const result = await djClient.getWorkspaceOwnedNodes(username, limit);
48
+ setData(
49
+ result?.data?.findNodesPaginated?.edges?.map(e => e.node) || [],
50
+ );
51
+ } catch (err) {
52
+ console.error('Error fetching owned nodes:', err);
53
+ setError(err);
54
+ }
55
+ setLoading(false);
56
+ };
57
+ fetchData();
58
+ }, [djClient, username, limit]);
59
+
60
+ return { data, loading, error };
61
+ }
62
+
63
+ /**
64
+ * Hook to fetch workspace recently edited nodes
65
+ */
66
+ export function useWorkspaceRecentlyEdited(username, limit = 5000) {
67
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
68
+ const [data, setData] = useState([]);
69
+ const [loading, setLoading] = useState(true);
70
+ const [error, setError] = useState(null);
71
+
72
+ useEffect(() => {
73
+ if (!username) {
74
+ setLoading(false);
75
+ return;
76
+ }
77
+
78
+ const fetchData = async () => {
79
+ try {
80
+ const result = await djClient.getWorkspaceRecentlyEdited(
81
+ username,
82
+ limit,
83
+ );
84
+ setData(
85
+ result?.data?.findNodesPaginated?.edges?.map(e => e.node) || [],
86
+ );
87
+ } catch (err) {
88
+ console.error('Error fetching recently edited nodes:', err);
89
+ setError(err);
90
+ }
91
+ setLoading(false);
92
+ };
93
+ fetchData();
94
+ }, [djClient, username, limit]);
95
+
96
+ return { data, loading, error };
97
+ }
98
+
99
+ /**
100
+ * Hook to fetch watched nodes (notification subscriptions)
101
+ */
102
+ export function useWorkspaceWatchedNodes(username) {
103
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
104
+ const [data, setData] = useState([]);
105
+ const [loading, setLoading] = useState(true);
106
+ const [error, setError] = useState(null);
107
+
108
+ useEffect(() => {
109
+ if (!username) {
110
+ setLoading(false);
111
+ return;
112
+ }
113
+
114
+ const fetchData = async () => {
115
+ try {
116
+ const subscriptions = await djClient.getNotificationPreferences({
117
+ entity_type: 'node',
118
+ });
119
+ const watchedNodeNames = (subscriptions || []).map(s => s.entity_name);
120
+
121
+ if (watchedNodeNames.length > 0) {
122
+ const nodes = await djClient.getNodesByNames(watchedNodeNames);
123
+ setData(nodes || []);
124
+ } else {
125
+ setData([]);
126
+ }
127
+ } catch (err) {
128
+ console.error('Error fetching watched nodes:', err);
129
+ setError(err);
130
+ }
131
+ setLoading(false);
132
+ };
133
+ fetchData();
134
+ }, [djClient, username]);
135
+
136
+ return { data, loading, error };
137
+ }
138
+
139
+ /**
140
+ * Hook to fetch workspace collections
141
+ */
142
+ export function useWorkspaceCollections(username) {
143
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
144
+ const [data, setData] = useState([]);
145
+ const [loading, setLoading] = useState(true);
146
+ const [error, setError] = useState(null);
147
+
148
+ useEffect(() => {
149
+ if (!username) {
150
+ setLoading(false);
151
+ return;
152
+ }
153
+
154
+ const fetchData = async () => {
155
+ try {
156
+ const result = await djClient.getWorkspaceCollections(username);
157
+ setData(result?.data?.listCollections || []);
158
+ } catch (err) {
159
+ console.error('Error fetching collections:', err);
160
+ setError(err);
161
+ }
162
+ setLoading(false);
163
+ };
164
+ fetchData();
165
+ }, [djClient, username]);
166
+
167
+ return { data, loading, error };
168
+ }
169
+
170
+ /**
171
+ * Hook to fetch workspace notifications
172
+ */
173
+ export function useWorkspaceNotifications(username, limit = 50) {
174
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
175
+ const [data, setData] = useState([]);
176
+ const [loading, setLoading] = useState(true);
177
+ const [error, setError] = useState(null);
178
+
179
+ useEffect(() => {
180
+ if (!username) {
181
+ setLoading(false);
182
+ return;
183
+ }
184
+
185
+ const fetchData = async () => {
186
+ try {
187
+ const history = await djClient.getSubscribedHistory(limit);
188
+
189
+ // Enrich with node info
190
+ const nodeNames = Array.from(
191
+ new Set((history || []).map(h => h.entity_name)),
192
+ );
193
+ let nodeInfoMap = {};
194
+ if (nodeNames.length > 0) {
195
+ const nodes = await djClient.getNodesByNames(nodeNames);
196
+ nodeInfoMap = Object.fromEntries((nodes || []).map(n => [n.name, n]));
197
+ }
198
+
199
+ const enriched = (history || []).map(entry => ({
200
+ ...entry,
201
+ node_type: nodeInfoMap[entry.entity_name]?.type,
202
+ display_name: nodeInfoMap[entry.entity_name]?.current?.displayName,
203
+ }));
204
+
205
+ setData(enriched);
206
+ } catch (err) {
207
+ console.error('Error fetching notifications:', err);
208
+ setError(err);
209
+ }
210
+ setLoading(false);
211
+ };
212
+ fetchData();
213
+ }, [djClient, username, limit]);
214
+
215
+ return { data, loading, error };
216
+ }
217
+
218
+ /**
219
+ * Hook to fetch workspace materializations
220
+ */
221
+ export function useWorkspaceMaterializations(username, limit = 5000) {
222
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
223
+ const [data, setData] = useState([]);
224
+ const [loading, setLoading] = useState(true);
225
+ const [error, setError] = useState(null);
226
+
227
+ useEffect(() => {
228
+ if (!username) {
229
+ setLoading(false);
230
+ return;
231
+ }
232
+
233
+ const fetchData = async () => {
234
+ try {
235
+ const result = await djClient.getWorkspaceMaterializations(
236
+ username,
237
+ limit,
238
+ );
239
+ setData(
240
+ result?.data?.findNodesPaginated?.edges?.map(e => e.node) || [],
241
+ );
242
+ } catch (err) {
243
+ console.error('Error fetching materializations:', err);
244
+ setError(err);
245
+ }
246
+ setLoading(false);
247
+ };
248
+ fetchData();
249
+ }, [djClient, username, limit]);
250
+
251
+ return { data, loading, error };
252
+ }
253
+
254
+ /**
255
+ * Hook to fetch all "Needs Attention" items
256
+ * Returns an object with all actionable item categories
257
+ */
258
+ export function useWorkspaceNeedsAttention(username) {
259
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
260
+ const [data, setData] = useState({
261
+ nodesMissingDescription: [],
262
+ invalidNodes: [],
263
+ staleDrafts: [],
264
+ orphanedDimensions: [],
265
+ });
266
+ const [loading, setLoading] = useState(true);
267
+ const [error, setError] = useState(null);
268
+
269
+ useEffect(() => {
270
+ if (!username) {
271
+ setLoading(false);
272
+ return;
273
+ }
274
+
275
+ const fetchData = async () => {
276
+ try {
277
+ const [missingDescData, invalidData, draftData, orphanedData] =
278
+ await Promise.all([
279
+ djClient.getWorkspaceNodesMissingDescription(username, 5000),
280
+ djClient.getWorkspaceInvalidNodes(username, 5000),
281
+ djClient.getWorkspaceDraftNodes(username, 5000),
282
+ djClient.getWorkspaceOrphanedDimensions(username, 5000),
283
+ ]);
284
+
285
+ const nodesMissingDescription =
286
+ missingDescData?.data?.findNodesPaginated?.edges?.map(e => e.node) ||
287
+ [];
288
+ const invalidNodes =
289
+ invalidData?.data?.findNodesPaginated?.edges?.map(e => e.node) || [];
290
+ const orphanedDimensions =
291
+ orphanedData?.data?.findNodesPaginated?.edges?.map(e => e.node) || [];
292
+
293
+ // Filter drafts older than 7 days
294
+ const sevenDaysAgo = new Date();
295
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
296
+ const allDrafts =
297
+ draftData?.data?.findNodesPaginated?.edges?.map(e => e.node) || [];
298
+ const staleDrafts = allDrafts.filter(node => {
299
+ const updatedAt = new Date(node.current?.updatedAt);
300
+ return updatedAt < sevenDaysAgo;
301
+ });
302
+
303
+ setData({
304
+ nodesMissingDescription,
305
+ invalidNodes,
306
+ staleDrafts,
307
+ orphanedDimensions,
308
+ });
309
+ } catch (err) {
310
+ console.error('Error fetching needs attention items:', err);
311
+ setError(err);
312
+ }
313
+ setLoading(false);
314
+ };
315
+ fetchData();
316
+ }, [djClient, username]);
317
+
318
+ return { data, loading, error };
319
+ }
320
+
321
+ /**
322
+ * Hook to check if personal namespace exists
323
+ */
324
+ export function usePersonalNamespace(username) {
325
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
326
+ const [exists, setExists] = useState(null);
327
+ const [loading, setLoading] = useState(true);
328
+ const [error, setError] = useState(null);
329
+
330
+ useEffect(() => {
331
+ if (!username) {
332
+ setLoading(false);
333
+ return;
334
+ }
335
+
336
+ const checkNamespace = async () => {
337
+ try {
338
+ // Extract username without domain for namespace
339
+ const usernameForNamespace = username.split('@')[0];
340
+ const personalNamespace = `users.${usernameForNamespace}`;
341
+ const namespaces = await djClient.namespaces();
342
+ const namespaceExists = namespaces.some(
343
+ ns => ns.namespace === personalNamespace,
344
+ );
345
+ setExists(namespaceExists);
346
+ } catch (err) {
347
+ console.error('Error checking namespace:', err);
348
+ setError(err);
349
+ setExists(false);
350
+ }
351
+ setLoading(false);
352
+ };
353
+ checkNamespace();
354
+ }, [djClient, username]);
355
+
356
+ return { exists, loading, error };
357
+ }
package/src/app/index.tsx CHANGED
@@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet-async';
8
8
  import { BrowserRouter, Routes, Route } from 'react-router-dom';
9
9
 
10
10
  import { NamespacePage } from './pages/NamespacePage/Loadable';
11
+ import { MyWorkspacePage } from './pages/MyWorkspacePage/Loadable';
11
12
  import { OverviewPage } from './pages/OverviewPage/Loadable';
12
13
  import { SettingsPage } from './pages/SettingsPage/Loadable';
13
14
  import { NotificationsPage } from './pages/NotificationsPage/Loadable';
@@ -136,6 +137,11 @@ export function App() {
136
137
  key="overview"
137
138
  element={<OverviewPage />}
138
139
  />
140
+ <Route
141
+ path="workspace"
142
+ key="workspace"
143
+ element={<MyWorkspacePage />}
144
+ />
139
145
  <Route
140
146
  path="settings"
141
147
  key="settings"