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,190 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import DashboardCard from '../../components/DashboardCard';
|
|
4
|
+
import { NodeDisplay } from '../../components/NodeComponents';
|
|
5
|
+
|
|
6
|
+
// Materializations Section
|
|
7
|
+
export function MaterializationsSection({ nodes, loading }) {
|
|
8
|
+
// Ensure nodes is always an array
|
|
9
|
+
const sortedNodes = (nodes || []).slice().sort((a, b) => {
|
|
10
|
+
const aTs = a.current?.availability?.validThroughTs;
|
|
11
|
+
const bTs = b.current?.availability?.validThroughTs;
|
|
12
|
+
if (!aTs && !bTs) return 0;
|
|
13
|
+
if (!aTs) return 1;
|
|
14
|
+
if (!bTs) return -1;
|
|
15
|
+
return bTs - aTs;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const getAvailabilityStatus = availability => {
|
|
19
|
+
if (!availability) {
|
|
20
|
+
return { icon: '⏳', text: 'Pending', color: '#6c757d' };
|
|
21
|
+
}
|
|
22
|
+
const validThrough = availability.validThroughTs
|
|
23
|
+
? new Date(availability.validThroughTs)
|
|
24
|
+
: null;
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const hoursSinceUpdate = validThrough
|
|
27
|
+
? (now - validThrough) / (1000 * 60 * 60)
|
|
28
|
+
: null;
|
|
29
|
+
if (!validThrough) {
|
|
30
|
+
return { icon: '⏳', text: 'Pending', color: '#6c757d' };
|
|
31
|
+
} else if (hoursSinceUpdate <= 24) {
|
|
32
|
+
return {
|
|
33
|
+
icon: '🟢',
|
|
34
|
+
text: formatTimeAgo(validThrough),
|
|
35
|
+
color: '#28a745',
|
|
36
|
+
};
|
|
37
|
+
} else if (hoursSinceUpdate <= 72) {
|
|
38
|
+
return {
|
|
39
|
+
icon: '🟡',
|
|
40
|
+
text: formatTimeAgo(validThrough),
|
|
41
|
+
color: '#ffc107',
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
return {
|
|
45
|
+
icon: '🔴',
|
|
46
|
+
text: formatTimeAgo(validThrough),
|
|
47
|
+
color: '#dc3545',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formatTimeAgo = date => {
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const diffMs = now - date;
|
|
55
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
56
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
57
|
+
if (diffHours < 1) return 'just now';
|
|
58
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
59
|
+
if (diffDays === 1) return 'yesterday';
|
|
60
|
+
return `${diffDays}d ago`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const maxDisplay = 5;
|
|
64
|
+
|
|
65
|
+
const materializationsList = sortedNodes.slice(0, maxDisplay).map(node => {
|
|
66
|
+
const status = getAvailabilityStatus(node.current?.availability);
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
key={node.name}
|
|
70
|
+
style={{
|
|
71
|
+
padding: '0.5rem',
|
|
72
|
+
border: '1px solid var(--border-color, #e0e0e0)',
|
|
73
|
+
borderRadius: '4px',
|
|
74
|
+
backgroundColor: 'var(--card-bg, #fff)',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
style={{
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'space-between',
|
|
82
|
+
marginBottom: '4px',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<NodeDisplay node={node} size="medium" />
|
|
86
|
+
<span style={{ fontSize: '10px', color: status.color }}>
|
|
87
|
+
{status.icon} {status.text}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div style={{ fontSize: '10px', color: '#666' }}>
|
|
91
|
+
{node.current?.materializations?.map(mat => (
|
|
92
|
+
<span key={mat.name} style={{ marginRight: '8px' }}>
|
|
93
|
+
🕐 {mat.schedule || 'No schedule'}
|
|
94
|
+
</span>
|
|
95
|
+
))}
|
|
96
|
+
{node.current?.availability?.table && (
|
|
97
|
+
<span style={{ color: '#888' }}>
|
|
98
|
+
→ {node.current.availability.table}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<DashboardCard
|
|
108
|
+
title="Materializations"
|
|
109
|
+
actionLink="/?hasMaterialization=true"
|
|
110
|
+
loading={loading}
|
|
111
|
+
cardStyle={{
|
|
112
|
+
padding: '0.75rem 1rem',
|
|
113
|
+
maxHeight: '300px',
|
|
114
|
+
overflowY: 'auto',
|
|
115
|
+
}}
|
|
116
|
+
emptyState={
|
|
117
|
+
<div style={{ padding: '0' }}>
|
|
118
|
+
<p
|
|
119
|
+
style={{ fontSize: '12px', color: '#666', marginBottom: '0.75rem' }}
|
|
120
|
+
>
|
|
121
|
+
No materializations configured.
|
|
122
|
+
</p>
|
|
123
|
+
<div
|
|
124
|
+
style={{
|
|
125
|
+
padding: '0.75rem',
|
|
126
|
+
backgroundColor: 'var(--card-bg, #f8f9fa)',
|
|
127
|
+
border: '1px dashed var(--border-color, #dee2e6)',
|
|
128
|
+
borderRadius: '6px',
|
|
129
|
+
textAlign: 'center',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<div style={{ fontSize: '16px', marginBottom: '0.25rem' }}>📦</div>
|
|
133
|
+
<div
|
|
134
|
+
style={{
|
|
135
|
+
fontSize: '11px',
|
|
136
|
+
fontWeight: '500',
|
|
137
|
+
marginBottom: '0.25rem',
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
Materialize a node
|
|
141
|
+
</div>
|
|
142
|
+
<p
|
|
143
|
+
style={{
|
|
144
|
+
fontSize: '10px',
|
|
145
|
+
color: '#666',
|
|
146
|
+
marginBottom: '0.5rem',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
Speed up queries with cached data
|
|
150
|
+
</p>
|
|
151
|
+
<Link
|
|
152
|
+
to="/"
|
|
153
|
+
style={{
|
|
154
|
+
display: 'inline-block',
|
|
155
|
+
padding: '3px 8px',
|
|
156
|
+
fontSize: '10px',
|
|
157
|
+
backgroundColor: 'var(--primary-color, #4a90d9)',
|
|
158
|
+
color: '#fff',
|
|
159
|
+
borderRadius: '4px',
|
|
160
|
+
textDecoration: 'none',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
Browse nodes →
|
|
164
|
+
</Link>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
}
|
|
168
|
+
>
|
|
169
|
+
{sortedNodes.length > 0 && (
|
|
170
|
+
<div
|
|
171
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
|
|
172
|
+
>
|
|
173
|
+
{materializationsList}
|
|
174
|
+
{sortedNodes.length > maxDisplay && (
|
|
175
|
+
<div
|
|
176
|
+
style={{
|
|
177
|
+
textAlign: 'center',
|
|
178
|
+
padding: '0.5rem',
|
|
179
|
+
fontSize: '12px',
|
|
180
|
+
color: '#666',
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
+{sortedNodes.length - maxDisplay} more
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</DashboardCard>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import DashboardCard from '../../components/DashboardCard';
|
|
4
|
+
import { NodeList } from './NodeList';
|
|
5
|
+
import { TypeGroupGrid } from './TypeGroupGrid';
|
|
6
|
+
|
|
7
|
+
// Node type display order
|
|
8
|
+
const NODE_TYPE_ORDER = ['metric', 'cube', 'dimension', 'transform', 'source'];
|
|
9
|
+
|
|
10
|
+
// Helper to group nodes by type
|
|
11
|
+
function groupNodesByType(nodes) {
|
|
12
|
+
const groups = {};
|
|
13
|
+
nodes.forEach(node => {
|
|
14
|
+
const type = (node.type || 'unknown').toLowerCase();
|
|
15
|
+
if (!groups[type]) groups[type] = [];
|
|
16
|
+
groups[type].push(node);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Return types in defined order, only including types with nodes
|
|
20
|
+
return NODE_TYPE_ORDER.filter(type => groups[type]?.length > 0).map(type => ({
|
|
21
|
+
type,
|
|
22
|
+
nodes: groups[type],
|
|
23
|
+
count: groups[type].length,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// My Nodes Section (owned + watched, with tabs)
|
|
28
|
+
export function MyNodesSection({
|
|
29
|
+
ownedNodes,
|
|
30
|
+
watchedNodes,
|
|
31
|
+
recentlyEdited,
|
|
32
|
+
username,
|
|
33
|
+
loading,
|
|
34
|
+
}) {
|
|
35
|
+
const [activeTab, setActiveTab] = React.useState('owned');
|
|
36
|
+
const [groupByType, setGroupByType] = React.useState(() => {
|
|
37
|
+
return localStorage.getItem('workspace_groupByType') === 'true';
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const ownedNames = new Set(ownedNodes.map(n => n.name));
|
|
41
|
+
const watchedOnly = watchedNodes.filter(n => !ownedNames.has(n.name));
|
|
42
|
+
|
|
43
|
+
const allMyNodeNames = new Set([
|
|
44
|
+
...ownedNames,
|
|
45
|
+
...watchedNodes.map(n => n.name),
|
|
46
|
+
]);
|
|
47
|
+
const editedOnly = recentlyEdited.filter(n => !allMyNodeNames.has(n.name));
|
|
48
|
+
|
|
49
|
+
const getDisplayNodes = () => {
|
|
50
|
+
switch (activeTab) {
|
|
51
|
+
case 'owned':
|
|
52
|
+
return ownedNodes;
|
|
53
|
+
case 'watched':
|
|
54
|
+
return watchedOnly;
|
|
55
|
+
case 'edited':
|
|
56
|
+
return recentlyEdited;
|
|
57
|
+
default:
|
|
58
|
+
return ownedNodes;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const displayNodes = getDisplayNodes();
|
|
62
|
+
|
|
63
|
+
const hasAnyContent =
|
|
64
|
+
ownedNodes.length > 0 ||
|
|
65
|
+
watchedOnly.length > 0 ||
|
|
66
|
+
recentlyEdited.length > 0;
|
|
67
|
+
const maxDisplay = 8;
|
|
68
|
+
|
|
69
|
+
// Handle group by type toggle
|
|
70
|
+
const handleGroupByTypeChange = e => {
|
|
71
|
+
const checked = e.target.checked;
|
|
72
|
+
setGroupByType(checked);
|
|
73
|
+
localStorage.setItem('workspace_groupByType', checked.toString());
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Group nodes by type if enabled
|
|
77
|
+
const groupedData = groupByType ? groupNodesByType(displayNodes) : null;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<DashboardCard
|
|
81
|
+
title="My Nodes"
|
|
82
|
+
actionLink={`/?ownedBy=${username}`}
|
|
83
|
+
loading={loading}
|
|
84
|
+
cardStyle={{
|
|
85
|
+
padding: '0.25rem 0.75rem',
|
|
86
|
+
}}
|
|
87
|
+
emptyState={
|
|
88
|
+
<div style={{ padding: '0.75rem 0' }}>
|
|
89
|
+
<p
|
|
90
|
+
style={{ fontSize: '12px', color: '#666', marginBottom: '0.75rem' }}
|
|
91
|
+
>
|
|
92
|
+
No nodes yet.
|
|
93
|
+
</p>
|
|
94
|
+
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
flex: 1,
|
|
98
|
+
padding: '0.75rem',
|
|
99
|
+
backgroundColor: 'var(--card-bg, #f8f9fa)',
|
|
100
|
+
border: '1px dashed var(--border-color, #dee2e6)',
|
|
101
|
+
borderRadius: '6px',
|
|
102
|
+
textAlign: 'center',
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<div style={{ fontSize: '16px', marginBottom: '0.25rem' }}>
|
|
106
|
+
➕
|
|
107
|
+
</div>
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
fontSize: '11px',
|
|
111
|
+
fontWeight: '500',
|
|
112
|
+
marginBottom: '0.25rem',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Create a node
|
|
116
|
+
</div>
|
|
117
|
+
<p
|
|
118
|
+
style={{
|
|
119
|
+
fontSize: '10px',
|
|
120
|
+
color: '#666',
|
|
121
|
+
marginBottom: '0.5rem',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Build your data model
|
|
125
|
+
</p>
|
|
126
|
+
<Link
|
|
127
|
+
to="/create/source"
|
|
128
|
+
style={{
|
|
129
|
+
display: 'inline-block',
|
|
130
|
+
padding: '3px 8px',
|
|
131
|
+
fontSize: '10px',
|
|
132
|
+
backgroundColor: 'var(--primary-color, #4a90d9)',
|
|
133
|
+
color: '#fff',
|
|
134
|
+
borderRadius: '4px',
|
|
135
|
+
textDecoration: 'none',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
Create →
|
|
139
|
+
</Link>
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
flex: 1,
|
|
144
|
+
padding: '0.75rem',
|
|
145
|
+
backgroundColor: 'var(--card-bg, #f8f9fa)',
|
|
146
|
+
border: '1px dashed var(--border-color, #dee2e6)',
|
|
147
|
+
borderRadius: '6px',
|
|
148
|
+
textAlign: 'center',
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div style={{ fontSize: '16px', marginBottom: '0.25rem' }}>
|
|
152
|
+
👤
|
|
153
|
+
</div>
|
|
154
|
+
<div
|
|
155
|
+
style={{
|
|
156
|
+
fontSize: '11px',
|
|
157
|
+
fontWeight: '500',
|
|
158
|
+
marginBottom: '0.25rem',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
Claim ownership
|
|
162
|
+
</div>
|
|
163
|
+
<p
|
|
164
|
+
style={{
|
|
165
|
+
fontSize: '10px',
|
|
166
|
+
color: '#666',
|
|
167
|
+
marginBottom: '0.5rem',
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
Add yourself as owner
|
|
171
|
+
</p>
|
|
172
|
+
<Link
|
|
173
|
+
to="/"
|
|
174
|
+
style={{
|
|
175
|
+
display: 'inline-block',
|
|
176
|
+
padding: '3px 8px',
|
|
177
|
+
fontSize: '10px',
|
|
178
|
+
backgroundColor: '#6c757d',
|
|
179
|
+
color: '#fff',
|
|
180
|
+
borderRadius: '4px',
|
|
181
|
+
textDecoration: 'none',
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
Browse →
|
|
185
|
+
</Link>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{hasAnyContent && (
|
|
192
|
+
<>
|
|
193
|
+
{/* Tabs and Group by Type Toggle */}
|
|
194
|
+
<div
|
|
195
|
+
style={{
|
|
196
|
+
display: 'flex',
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
justifyContent: 'space-between',
|
|
199
|
+
gap: '0.5rem',
|
|
200
|
+
marginBottom: '0.5rem',
|
|
201
|
+
paddingTop: '0.5rem',
|
|
202
|
+
paddingBottom: displayNodes.length > 0 ? '0.5rem' : '0',
|
|
203
|
+
borderBottom:
|
|
204
|
+
displayNodes.length > 0
|
|
205
|
+
? '1px solid var(--border-color, #eee)'
|
|
206
|
+
: 'none',
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => setActiveTab('owned')}
|
|
212
|
+
style={{
|
|
213
|
+
padding: '4px 10px',
|
|
214
|
+
fontSize: '11px',
|
|
215
|
+
border: 'none',
|
|
216
|
+
borderRadius: '4px',
|
|
217
|
+
cursor: 'pointer',
|
|
218
|
+
backgroundColor:
|
|
219
|
+
activeTab === 'owned'
|
|
220
|
+
? 'var(--primary-color, #4a90d9)'
|
|
221
|
+
: '#e9ecef',
|
|
222
|
+
color: activeTab === 'owned' ? '#fff' : '#495057',
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
Owned ({ownedNodes.length})
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setActiveTab('watched')}
|
|
229
|
+
style={{
|
|
230
|
+
padding: '4px 10px',
|
|
231
|
+
fontSize: '11px',
|
|
232
|
+
border: 'none',
|
|
233
|
+
borderRadius: '4px',
|
|
234
|
+
cursor: 'pointer',
|
|
235
|
+
backgroundColor:
|
|
236
|
+
activeTab === 'watched'
|
|
237
|
+
? 'var(--primary-color, #4a90d9)'
|
|
238
|
+
: '#e9ecef',
|
|
239
|
+
color: activeTab === 'watched' ? '#fff' : '#495057',
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
Watched ({watchedOnly.length})
|
|
243
|
+
</button>
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => setActiveTab('edited')}
|
|
246
|
+
style={{
|
|
247
|
+
padding: '4px 10px',
|
|
248
|
+
fontSize: '11px',
|
|
249
|
+
border: 'none',
|
|
250
|
+
borderRadius: '4px',
|
|
251
|
+
cursor: 'pointer',
|
|
252
|
+
backgroundColor:
|
|
253
|
+
activeTab === 'edited'
|
|
254
|
+
? 'var(--primary-color, #4a90d9)'
|
|
255
|
+
: '#e9ecef',
|
|
256
|
+
color: activeTab === 'edited' ? '#fff' : '#495057',
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
Recent Edits ({recentlyEdited.length})
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Group by Type Toggle */}
|
|
264
|
+
{displayNodes.length > 0 && (
|
|
265
|
+
<div
|
|
266
|
+
style={{
|
|
267
|
+
display: 'flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
gap: '0.5rem',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<input
|
|
273
|
+
type="checkbox"
|
|
274
|
+
id="groupByType"
|
|
275
|
+
checked={groupByType}
|
|
276
|
+
onChange={handleGroupByTypeChange}
|
|
277
|
+
style={{ cursor: 'pointer' }}
|
|
278
|
+
/>
|
|
279
|
+
<label
|
|
280
|
+
htmlFor="groupByType"
|
|
281
|
+
style={{
|
|
282
|
+
fontSize: '11px',
|
|
283
|
+
cursor: 'pointer',
|
|
284
|
+
userSelect: 'none',
|
|
285
|
+
color: '#495057',
|
|
286
|
+
fontWeight: '500',
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
Group by Type
|
|
290
|
+
</label>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{displayNodes.length > 0 ? (
|
|
296
|
+
<>
|
|
297
|
+
{groupByType ? (
|
|
298
|
+
<TypeGroupGrid
|
|
299
|
+
groupedData={groupedData}
|
|
300
|
+
username={username}
|
|
301
|
+
activeTab={activeTab}
|
|
302
|
+
/>
|
|
303
|
+
) : (
|
|
304
|
+
<>
|
|
305
|
+
<NodeList
|
|
306
|
+
nodes={displayNodes.slice(0, maxDisplay)}
|
|
307
|
+
showUpdatedAt={true}
|
|
308
|
+
/>
|
|
309
|
+
{displayNodes.length > maxDisplay && (
|
|
310
|
+
<div
|
|
311
|
+
style={{
|
|
312
|
+
textAlign: 'center',
|
|
313
|
+
padding: '0.5rem',
|
|
314
|
+
fontSize: '12px',
|
|
315
|
+
color: '#666',
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
+{displayNodes.length - maxDisplay} more
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</>
|
|
322
|
+
)}
|
|
323
|
+
</>
|
|
324
|
+
) : (
|
|
325
|
+
<div
|
|
326
|
+
style={{
|
|
327
|
+
padding: '1rem',
|
|
328
|
+
textAlign: 'center',
|
|
329
|
+
color: '#666',
|
|
330
|
+
fontSize: '12px',
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
{activeTab === 'owned' && 'No owned nodes'}
|
|
334
|
+
{activeTab === 'watched' && 'No watched nodes'}
|
|
335
|
+
{activeTab === 'edited' && 'No recent edits'}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</DashboardCard>
|
|
341
|
+
);
|
|
342
|
+
}
|