datajunction-ui 0.0.75 → 0.0.77

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,344 @@
1
+ import * as React from 'react';
2
+ import { useContext, useEffect, useState } from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import DashboardCard from '../../components/DashboardCard';
5
+ import { formatRelativeTime } from '../../utils/date';
6
+
7
+ // Git Namespaces Section - shows git-managed namespaces with their branches
8
+ export function ActiveBranchesSection({ ownedNodes, recentlyEdited, loading }) {
9
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
10
+ const [branchCounts, setBranchCounts] = useState({});
11
+ const [countsLoading, setCountsLoading] = useState(true);
12
+
13
+ // Combine owned and edited nodes to get all user's nodes
14
+ const allNodes = [...ownedNodes, ...recentlyEdited];
15
+
16
+ // Group nodes by base git namespace (the root namespace that's git-managed)
17
+ const gitNamespaceMap = new Map();
18
+ allNodes.forEach(node => {
19
+ if (node.gitInfo && node.gitInfo.repo) {
20
+ // Extract the base namespace (everything before the branch part)
21
+ const fullNamespace = node.name.split('.').slice(0, -1).join('.');
22
+
23
+ // For git-managed namespaces, the base is the parent namespace
24
+ // If not available, we derive it by removing the branch from the full namespace
25
+ let baseNamespace;
26
+ if (node.gitInfo.parentNamespace) {
27
+ baseNamespace = node.gitInfo.parentNamespace;
28
+ } else {
29
+ // Fallback: remove the branch part from fullNamespace
30
+ // fullNamespace is like "myproject.main", we want "myproject"
31
+ const parts = fullNamespace.split('.');
32
+ baseNamespace = parts.slice(0, -1).join('.');
33
+ }
34
+
35
+ if (!gitNamespaceMap.has(baseNamespace)) {
36
+ gitNamespaceMap.set(baseNamespace, {
37
+ baseNamespace,
38
+ repo: node.gitInfo.repo,
39
+ defaultBranch: node.gitInfo.defaultBranch,
40
+ branches: new Map(),
41
+ });
42
+ }
43
+
44
+ const gitNs = gitNamespaceMap.get(baseNamespace);
45
+ const branchKey = node.gitInfo.branch;
46
+
47
+ if (!gitNs.branches.has(branchKey)) {
48
+ gitNs.branches.set(branchKey, {
49
+ branch: node.gitInfo.branch,
50
+ isDefault: node.gitInfo.isDefaultBranch,
51
+ namespace: fullNamespace,
52
+ nodes: [],
53
+ lastActivity: node.current?.updatedAt,
54
+ });
55
+ }
56
+
57
+ const branchInfo = gitNs.branches.get(branchKey);
58
+ branchInfo.nodes.push(node);
59
+ // Track most recent activity
60
+ if (
61
+ node.current?.updatedAt &&
62
+ (!branchInfo.lastActivity ||
63
+ node.current.updatedAt > branchInfo.lastActivity)
64
+ ) {
65
+ branchInfo.lastActivity = node.current.updatedAt;
66
+ }
67
+ }
68
+ });
69
+
70
+ // Ensure default branch is always present (even if user has no nodes there)
71
+ gitNamespaceMap.forEach(gitNs => {
72
+ if (gitNs.defaultBranch && !gitNs.branches.has(gitNs.defaultBranch)) {
73
+ gitNs.branches.set(gitNs.defaultBranch, {
74
+ branch: gitNs.defaultBranch,
75
+ isDefault: true,
76
+ namespace: `${gitNs.baseNamespace}.${gitNs.defaultBranch}`,
77
+ nodes: [],
78
+ lastActivity: null,
79
+ });
80
+ }
81
+ });
82
+
83
+ // Convert to array and sort
84
+ const gitNamespaces = Array.from(gitNamespaceMap.values())
85
+ .map(ns => ({
86
+ ...ns,
87
+ branches: Array.from(ns.branches.values()).sort((a, b) => {
88
+ // Default branch first
89
+ if (a.isDefault && !b.isDefault) return -1;
90
+ if (!a.isDefault && b.isDefault) return 1;
91
+ // Then by last activity
92
+ if (!a.lastActivity && !b.lastActivity) return 0;
93
+ if (!a.lastActivity) return 1;
94
+ if (!b.lastActivity) return -1;
95
+ return new Date(b.lastActivity) - new Date(a.lastActivity);
96
+ }),
97
+ }))
98
+ .sort((a, b) => {
99
+ // Sort git namespaces by most recent activity across all branches
100
+ const aLatest = Math.max(
101
+ ...a.branches.map(b =>
102
+ b.lastActivity ? new Date(b.lastActivity).getTime() : 0,
103
+ ),
104
+ );
105
+ const bLatest = Math.max(
106
+ ...b.branches.map(b =>
107
+ b.lastActivity ? new Date(b.lastActivity).getTime() : 0,
108
+ ),
109
+ );
110
+ return bLatest - aLatest;
111
+ });
112
+
113
+ const maxDisplay = 3; // Show top 3 git namespaces
114
+
115
+ // Fetch total node counts for each branch
116
+ useEffect(() => {
117
+ if (loading) return;
118
+
119
+ if (gitNamespaces.length === 0) {
120
+ setCountsLoading(false);
121
+ return;
122
+ }
123
+
124
+ const fetchCounts = async () => {
125
+ const counts = {};
126
+
127
+ // Collect all branch namespaces to query
128
+ const branchNamespaces = [];
129
+ gitNamespaces.forEach(gitNs => {
130
+ gitNs.branches.forEach(branch => {
131
+ branchNamespaces.push(branch.namespace);
132
+ });
133
+ });
134
+
135
+ // Fetch counts for all branches in parallel
136
+ const countPromises = branchNamespaces.map(async namespace => {
137
+ try {
138
+ const result = await djClient.listNodesForLanding(
139
+ namespace,
140
+ ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'], // Exclude SOURCE nodes
141
+ null, // tags
142
+ null, // editedBy
143
+ null, // before
144
+ null, // after
145
+ 5000, // high limit to get accurate count
146
+ { key: 'name', direction: 'ascending' }, // sortConfig (required, but doesn't matter for counting)
147
+ null, // mode
148
+ );
149
+ const count = result?.data?.findNodesPaginated?.edges?.length || 0;
150
+ counts[namespace] = count;
151
+ } catch (error) {
152
+ console.error(`Error fetching count for ${namespace}:`, error);
153
+ counts[namespace] = 0;
154
+ }
155
+ });
156
+
157
+ await Promise.all(countPromises);
158
+ setBranchCounts(counts);
159
+ setCountsLoading(false);
160
+ };
161
+
162
+ fetchCounts();
163
+ }, [djClient, loading, gitNamespaces.length]);
164
+
165
+ const gitNamespacesList = gitNamespaces
166
+ .slice(0, maxDisplay)
167
+ .map((gitNs, gitNsIdx) => {
168
+ const isLastGitNs =
169
+ gitNsIdx === gitNamespaces.slice(0, maxDisplay).length - 1;
170
+ return (
171
+ <div key={gitNs.baseNamespace}>
172
+ {/* Base namespace and repository header */}
173
+ <div
174
+ style={{
175
+ display: 'flex',
176
+ alignItems: 'center',
177
+ gap: '8px',
178
+ padding: '0.75rem 0 0.5rem 0',
179
+ marginTop: gitNsIdx === 0 ? '0' : '0',
180
+ }}
181
+ >
182
+ <a
183
+ href={`/namespaces/${gitNs.baseNamespace}`}
184
+ style={{
185
+ fontSize: '12px',
186
+ fontWeight: '600',
187
+ textDecoration: 'none',
188
+ }}
189
+ >
190
+ {gitNs.baseNamespace}
191
+ </a>
192
+ <span
193
+ style={{
194
+ fontSize: '9px',
195
+ padding: '2px 6px',
196
+ backgroundColor: '#f0f0f0',
197
+ color: '#666',
198
+ borderRadius: '3px',
199
+ fontWeight: '500',
200
+ }}
201
+ >
202
+ {gitNs.repo}
203
+ </span>
204
+ </div>
205
+
206
+ {/* Branch list */}
207
+ {gitNs.branches.map(branchInfo => {
208
+ const totalNodes = branchCounts[branchInfo.namespace] ?? 0;
209
+ return (
210
+ <div
211
+ key={branchInfo.branch}
212
+ style={{
213
+ display: 'flex',
214
+ alignItems: 'center',
215
+ justifyContent: 'space-between',
216
+ padding: '0px 1em 0.4em 1em',
217
+ }}
218
+ >
219
+ <div
220
+ style={{
221
+ display: 'flex',
222
+ alignItems: 'center',
223
+ gap: '6px',
224
+ minWidth: 0,
225
+ flex: 1,
226
+ }}
227
+ >
228
+ <a
229
+ href={`/namespaces/${branchInfo.namespace}`}
230
+ style={{
231
+ fontSize: '12px',
232
+ fontWeight: '500',
233
+ overflow: 'hidden',
234
+ textOverflow: 'ellipsis',
235
+ whiteSpace: 'nowrap',
236
+ }}
237
+ >
238
+ {branchInfo.branch}
239
+ </a>
240
+ {branchInfo.isDefault && (
241
+ <span style={{ fontSize: '10px' }}>⭐</span>
242
+ )}
243
+ </div>
244
+ <div
245
+ style={{
246
+ display: 'flex',
247
+ alignItems: 'center',
248
+ gap: '6px',
249
+ fontSize: '10px',
250
+ color: '#666',
251
+ whiteSpace: 'nowrap',
252
+ }}
253
+ >
254
+ <span>
255
+ {totalNodes} node{totalNodes !== 1 ? 's' : ''}
256
+ </span>
257
+ {branchInfo.lastActivity && (
258
+ <>
259
+ <span>•</span>
260
+ <span style={{ color: '#888' }}>
261
+ updated {formatRelativeTime(branchInfo.lastActivity)}
262
+ </span>
263
+ </>
264
+ )}
265
+ </div>
266
+ </div>
267
+ );
268
+ })}
269
+
270
+ {/* Horizontal line between namespaces */}
271
+ {!isLastGitNs && (
272
+ <div
273
+ style={{
274
+ borderBottom: '1px solid var(--border-color, #ddd)',
275
+ margin: '0.5rem 0',
276
+ }}
277
+ />
278
+ )}
279
+ </div>
280
+ );
281
+ });
282
+
283
+ return (
284
+ <DashboardCard
285
+ title="Git Namespaces"
286
+ loading={loading || countsLoading}
287
+ cardStyle={{
288
+ padding: '0.25rem 0.25rem 0.5em 0.75rem',
289
+ maxHeight: '300px',
290
+ overflowY: 'auto',
291
+ }}
292
+ emptyState={
293
+ <div style={{ padding: '0', textAlign: 'center' }}>
294
+ <p
295
+ style={{ fontSize: '12px', color: '#666', marginBottom: '0.75rem' }}
296
+ >
297
+ No git-managed namespaces.
298
+ </p>
299
+ <div
300
+ style={{
301
+ padding: '0.75rem',
302
+ backgroundColor: 'var(--card-bg, #f8f9fa)',
303
+ border: '1px dashed var(--border-color, #dee2e6)',
304
+ borderRadius: '6px',
305
+ }}
306
+ >
307
+ <div style={{ fontSize: '16px', marginBottom: '0.25rem' }}>🌿</div>
308
+ <div
309
+ style={{
310
+ fontSize: '11px',
311
+ fontWeight: '500',
312
+ marginBottom: '0.25rem',
313
+ }}
314
+ >
315
+ Set up git-backed namespaces
316
+ </div>
317
+ <p style={{ fontSize: '10px', color: '#666', marginBottom: '0' }}>
318
+ Manage your nodes with version control
319
+ </p>
320
+ </div>
321
+ </div>
322
+ }
323
+ >
324
+ {gitNamespaces.length > 0 && (
325
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
326
+ {gitNamespacesList}
327
+ {gitNamespaces.length > maxDisplay && (
328
+ <div
329
+ style={{
330
+ textAlign: 'center',
331
+ padding: '0.5rem',
332
+ fontSize: '12px',
333
+ color: '#666',
334
+ }}
335
+ >
336
+ +{gitNamespaces.length - maxDisplay} more git namespace
337
+ {gitNamespaces.length - maxDisplay !== 1 ? 's' : ''}
338
+ </div>
339
+ )}
340
+ </div>
341
+ )}
342
+ </DashboardCard>
343
+ );
344
+ }
@@ -0,0 +1,188 @@
1
+ import * as React from 'react';
2
+ import { useContext, useEffect, useState } from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import DashboardCard from '../../components/DashboardCard';
6
+
7
+ // Collections Section (includes featured + my collections)
8
+ export function CollectionsSection({ collections, loading, currentUser }) {
9
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
10
+ const [allCollections, setAllCollections] = useState([]);
11
+ const [allLoading, setAllLoading] = useState(true);
12
+
13
+ useEffect(() => {
14
+ const fetchAll = async () => {
15
+ try {
16
+ const response = await djClient.listAllCollections();
17
+ console.log('All collections response:', response);
18
+ const all = response?.data?.listCollections || [];
19
+ setAllCollections(all);
20
+ } catch (error) {
21
+ console.error('Error fetching all collections:', error);
22
+ // Fall back to user's collections if fetching all fails
23
+ setAllCollections(collections);
24
+ }
25
+ setAllLoading(false);
26
+ };
27
+ fetchAll();
28
+ }, [djClient, collections]);
29
+
30
+ // Sort: user's collections first, then others
31
+ // If allCollections is empty, fall back to the collections prop
32
+ const collectionsToUse =
33
+ allCollections.length > 0 ? allCollections : collections;
34
+ const myCollections = collectionsToUse.filter(
35
+ c => c.createdBy?.username === currentUser?.username || !c.createdBy,
36
+ );
37
+ const otherCollections = collectionsToUse.filter(
38
+ c => c.createdBy && c.createdBy?.username !== currentUser?.username,
39
+ );
40
+ const allToShow = [...myCollections, ...otherCollections].slice(0, 8);
41
+
42
+ const collectionsGrid = allToShow.map(collection => {
43
+ const createdByUsername = collection.createdBy?.username;
44
+ const isOwner = createdByUsername === currentUser?.username;
45
+ const ownerDisplay = isOwner
46
+ ? 'you'
47
+ : createdByUsername?.split('@')[0] || 'unknown';
48
+
49
+ return (
50
+ <a
51
+ key={collection.name}
52
+ href={`/collections/${collection.name}`}
53
+ style={{
54
+ display: 'flex',
55
+ flexDirection: 'column',
56
+ padding: '1rem',
57
+ border: '1px solid var(--border-color, #e0e0e0)',
58
+ borderRadius: '8px',
59
+ textDecoration: 'none',
60
+ color: 'inherit',
61
+ transition: 'all 0.15s ease',
62
+ backgroundColor: 'var(--card-bg, #fff)',
63
+ cursor: 'pointer',
64
+ }}
65
+ onMouseEnter={e => {
66
+ e.currentTarget.style.borderColor = 'var(--primary-color, #007bff)';
67
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
68
+ e.currentTarget.style.transform = 'translateY(-2px)';
69
+ }}
70
+ onMouseLeave={e => {
71
+ e.currentTarget.style.borderColor = 'var(--border-color, #e0e0e0)';
72
+ e.currentTarget.style.boxShadow = 'none';
73
+ e.currentTarget.style.transform = 'translateY(0)';
74
+ }}
75
+ >
76
+ <div
77
+ style={{
78
+ fontWeight: '600',
79
+ fontSize: '14px',
80
+ marginBottom: '0.5rem',
81
+ lineHeight: '1.3',
82
+ }}
83
+ >
84
+ {collection.name}
85
+ </div>
86
+ {collection.description && (
87
+ <div
88
+ style={{
89
+ fontSize: '12px',
90
+ color: '#666',
91
+ lineHeight: '1.4',
92
+ marginBottom: '0.75rem',
93
+ overflow: 'hidden',
94
+ textOverflow: 'ellipsis',
95
+ display: '-webkit-box',
96
+ WebkitLineClamp: 2,
97
+ WebkitBoxOrient: 'vertical',
98
+ flex: 1,
99
+ }}
100
+ >
101
+ {collection.description}
102
+ </div>
103
+ )}
104
+ <div
105
+ style={{
106
+ display: 'flex',
107
+ justifyContent: 'space-between',
108
+ alignItems: 'center',
109
+ fontSize: '11px',
110
+ color: '#888',
111
+ marginTop: 'auto',
112
+ }}
113
+ >
114
+ <span>
115
+ {collection.nodeCount}{' '}
116
+ {collection.nodeCount === 1 ? 'node' : 'nodes'}
117
+ </span>
118
+ <span
119
+ style={{
120
+ color: isOwner ? 'var(--primary-color, #4a90d9)' : '#888',
121
+ }}
122
+ >
123
+ by {ownerDisplay}
124
+ </span>
125
+ </div>
126
+ </a>
127
+ );
128
+ });
129
+
130
+ return (
131
+ <DashboardCard
132
+ title="Collections"
133
+ actionLink="/collections"
134
+ actionText="+ Create"
135
+ loading={loading || allLoading}
136
+ cardStyle={{ padding: '0.75rem', minHeight: '200px' }}
137
+ emptyState={
138
+ <div style={{ padding: '1rem', textAlign: 'center' }}>
139
+ <div
140
+ style={{ fontSize: '48px', marginBottom: '0.5rem', opacity: 0.3 }}
141
+ >
142
+ 📁
143
+ </div>
144
+ <p style={{ fontSize: '13px', color: '#666', marginBottom: '1rem' }}>
145
+ No collections yet
146
+ </p>
147
+ <p
148
+ style={{
149
+ fontSize: '11px',
150
+ color: '#999',
151
+ marginBottom: '1rem',
152
+ lineHeight: '1.4',
153
+ }}
154
+ >
155
+ Group related metrics and dimensions together for easier discovery
156
+ </p>
157
+ <Link
158
+ to="/collections"
159
+ style={{
160
+ display: 'inline-block',
161
+ padding: '6px 12px',
162
+ fontSize: '11px',
163
+ backgroundColor: 'var(--primary-color, #4a90d9)',
164
+ color: '#fff',
165
+ borderRadius: '6px',
166
+ textDecoration: 'none',
167
+ fontWeight: '500',
168
+ }}
169
+ >
170
+ + Create Collection
171
+ </Link>
172
+ </div>
173
+ }
174
+ >
175
+ {allToShow.length > 0 && (
176
+ <div
177
+ style={{
178
+ display: 'grid',
179
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
180
+ gap: '0.75rem',
181
+ }}
182
+ >
183
+ {collectionsGrid}
184
+ </div>
185
+ )}
186
+ </DashboardCard>
187
+ );
188
+ }
@@ -0,0 +1,6 @@
1
+ import { lazyLoad } from 'utils/loadable';
2
+
3
+ export const MyWorkspacePage = lazyLoad(
4
+ () => import('./index'),
5
+ module => module.MyWorkspacePage,
6
+ );