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.
- package/package.json +1 -1
- package/src/app/components/DashboardCard.jsx +93 -0
- package/src/app/components/NodeComponents.jsx +173 -0
- package/src/app/components/NodeListActions.jsx +8 -3
- package/src/app/components/__tests__/NodeComponents.test.jsx +262 -0
- package/src/app/hooks/__tests__/useWorkspaceData.test.js +533 -0
- package/src/app/hooks/useWorkspaceData.js +357 -0
- package/src/app/index.tsx +6 -0
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +344 -0
- package/src/app/pages/MyWorkspacePage/CollectionsSection.jsx +188 -0
- package/src/app/pages/MyWorkspacePage/Loadable.jsx +6 -0
- package/src/app/pages/MyWorkspacePage/MaterializationsSection.jsx +190 -0
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +342 -0
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +632 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +185 -0
- package/src/app/pages/MyWorkspacePage/NodeList.jsx +46 -0
- package/src/app/pages/MyWorkspacePage/NotificationsSection.jsx +133 -0
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +209 -0
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +295 -0
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +278 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +238 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +389 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +347 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +272 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NodeList.test.jsx +162 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +204 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +556 -0
- package/src/app/pages/MyWorkspacePage/index.jsx +150 -0
- 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
|
+
}
|