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,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
+ }