datajunction-ui 0.0.92 → 0.0.94
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/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/services/DJService.js +73 -6
- package/src/app/services/__tests__/DJService.test.jsx +591 -0
- package/src/styles/index.css +32 -0
|
@@ -11,7 +11,6 @@ export function NeedsAttentionSection({
|
|
|
11
11
|
staleMaterializations,
|
|
12
12
|
orphanedDimensions,
|
|
13
13
|
username,
|
|
14
|
-
hasItems,
|
|
15
14
|
loading,
|
|
16
15
|
personalNamespace,
|
|
17
16
|
hasPersonalNamespace,
|
|
@@ -55,129 +54,116 @@ export function NeedsAttentionSection({
|
|
|
55
54
|
},
|
|
56
55
|
];
|
|
57
56
|
|
|
58
|
-
const categoriesList = categories.map(cat => (
|
|
59
|
-
<div
|
|
60
|
-
key={cat.id}
|
|
61
|
-
className="settings-card"
|
|
62
|
-
style={{ padding: '0.5rem 0.75rem', minWidth: 0 }}
|
|
63
|
-
>
|
|
64
|
-
<div
|
|
65
|
-
style={{
|
|
66
|
-
display: 'flex',
|
|
67
|
-
justifyContent: 'space-between',
|
|
68
|
-
alignItems: 'center',
|
|
69
|
-
marginBottom: '0.3rem',
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
72
|
-
<span style={{ fontSize: '11px', fontWeight: '600', color: '#555' }}>
|
|
73
|
-
{cat.icon} {cat.label}
|
|
74
|
-
<span
|
|
75
|
-
style={{
|
|
76
|
-
color: cat.nodes.length > 0 ? '#dc3545' : '#666',
|
|
77
|
-
marginLeft: '4px',
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
({cat.nodes.length})
|
|
81
|
-
</span>
|
|
82
|
-
</span>
|
|
83
|
-
{cat.nodes.length > 0 && (
|
|
84
|
-
<Link to={cat.viewAllLink} style={{ fontSize: '10px' }}>
|
|
85
|
-
View all →
|
|
86
|
-
</Link>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
{cat.nodes.length > 0 ? (
|
|
90
|
-
<div style={{ display: 'flex', gap: '0.3rem', overflow: 'hidden' }}>
|
|
91
|
-
{cat.nodes.slice(0, 10).map(node => (
|
|
92
|
-
<NodeChip key={node.name} node={node} />
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
) : (
|
|
96
|
-
<div style={{ fontSize: '10px', color: '#28a745' }}>✓ All good!</div>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
));
|
|
100
|
-
|
|
101
57
|
return (
|
|
102
58
|
<section style={{ minWidth: 0, width: '100%' }}>
|
|
103
59
|
<h2 className="settings-section-title">Needs Attention</h2>
|
|
104
|
-
<div style={{
|
|
60
|
+
<div className="settings-card" style={{ padding: '0.25rem 0.75rem' }}>
|
|
105
61
|
{loading ? (
|
|
106
|
-
<div
|
|
107
|
-
className="settings-card"
|
|
108
|
-
style={{ textAlign: 'center', padding: '1rem' }}
|
|
109
|
-
>
|
|
62
|
+
<div style={{ textAlign: 'center', padding: '1rem' }}>
|
|
110
63
|
<LoadingIcon />
|
|
111
64
|
</div>
|
|
112
65
|
) : (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
display: 'flex',
|
|
116
|
-
flexDirection: 'column',
|
|
117
|
-
width: '100%',
|
|
118
|
-
gap: '0.5rem',
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
{categoriesList}
|
|
122
|
-
{/* Personal namespace prompt if missing */}
|
|
123
|
-
{!namespaceLoading && !hasPersonalNamespace && (
|
|
66
|
+
<>
|
|
67
|
+
{categories.map((cat, i) => (
|
|
124
68
|
<div
|
|
69
|
+
key={cat.id}
|
|
125
70
|
style={{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: '0.5rem',
|
|
74
|
+
padding: '0.35rem 0',
|
|
75
|
+
borderBottom:
|
|
76
|
+
i < categories.length - 1
|
|
77
|
+
? '1px solid var(--border-color, #f0f0f0)'
|
|
78
|
+
: 'none',
|
|
79
|
+
minWidth: 0,
|
|
131
80
|
}}
|
|
132
81
|
>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</div>
|
|
136
|
-
<div
|
|
82
|
+
{/* Label + count */}
|
|
83
|
+
<span
|
|
137
84
|
style={{
|
|
138
85
|
fontSize: '11px',
|
|
139
|
-
fontWeight:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Set up your namespace
|
|
144
|
-
</div>
|
|
145
|
-
<p
|
|
146
|
-
style={{
|
|
147
|
-
fontSize: '10px',
|
|
148
|
-
color: '#666',
|
|
149
|
-
marginBottom: '0.5rem',
|
|
86
|
+
fontWeight: 600,
|
|
87
|
+
color: '#555',
|
|
88
|
+
whiteSpace: 'nowrap',
|
|
89
|
+
flexShrink: 0,
|
|
150
90
|
}}
|
|
151
91
|
>
|
|
152
|
-
|
|
153
|
-
<
|
|
92
|
+
{cat.label}{' '}
|
|
93
|
+
<span
|
|
94
|
+
style={{
|
|
95
|
+
color: cat.nodes.length > 0 ? '#dc3545' : '#28a745',
|
|
96
|
+
fontWeight: 500,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
({cat.nodes.length})
|
|
100
|
+
</span>
|
|
101
|
+
</span>
|
|
102
|
+
|
|
103
|
+
{/* Chips with right-side fade */}
|
|
104
|
+
{cat.nodes.length > 0 ? (
|
|
105
|
+
<div
|
|
154
106
|
style={{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
107
|
+
flex: 1,
|
|
108
|
+
overflow: 'hidden',
|
|
109
|
+
maskImage:
|
|
110
|
+
'linear-gradient(to right, black 75%, transparent 100%)',
|
|
111
|
+
WebkitMaskImage:
|
|
112
|
+
'linear-gradient(to right, black 75%, transparent 100%)',
|
|
159
113
|
}}
|
|
160
114
|
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
display: 'flex',
|
|
118
|
+
gap: '0.25rem',
|
|
119
|
+
flexWrap: 'nowrap',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{cat.nodes.slice(0, 20).map(node => (
|
|
123
|
+
<NodeChip key={node.name} node={node} />
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
) : (
|
|
128
|
+
<span style={{ fontSize: '10px', color: '#28a745', flex: 1 }}>
|
|
129
|
+
All good
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Arrow link — always visible, outside the fade */}
|
|
134
|
+
{cat.nodes.length > 0 && (
|
|
135
|
+
<Link
|
|
136
|
+
to={cat.viewAllLink}
|
|
137
|
+
style={{ fontSize: '11px', flexShrink: 0 }}
|
|
138
|
+
>
|
|
139
|
+
→
|
|
140
|
+
</Link>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
|
|
145
|
+
{!namespaceLoading && !hasPersonalNamespace && (
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
gap: '0.5rem',
|
|
151
|
+
padding: '0.35rem 0',
|
|
152
|
+
fontSize: '11px',
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<span style={{ fontWeight: 600, color: '#555', flexShrink: 0 }}>
|
|
156
|
+
Personal namespace
|
|
157
|
+
</span>
|
|
164
158
|
<Link
|
|
165
159
|
to={`/namespaces/${personalNamespace}`}
|
|
166
|
-
style={{
|
|
167
|
-
display: 'inline-block',
|
|
168
|
-
padding: '3px 8px',
|
|
169
|
-
fontSize: '10px',
|
|
170
|
-
backgroundColor: '#28a745',
|
|
171
|
-
color: '#fff',
|
|
172
|
-
borderRadius: '4px',
|
|
173
|
-
textDecoration: 'none',
|
|
174
|
-
}}
|
|
160
|
+
style={{ fontSize: '10px' }}
|
|
175
161
|
>
|
|
176
|
-
Create →
|
|
162
|
+
Create {personalNamespace} →
|
|
177
163
|
</Link>
|
|
178
164
|
</div>
|
|
179
165
|
)}
|
|
180
|
-
|
|
166
|
+
</>
|
|
181
167
|
)}
|
|
182
168
|
</div>
|
|
183
169
|
</section>
|
|
@@ -24,10 +24,8 @@ function capitalize(str) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Type Card Component
|
|
27
|
-
function TypeCard({ type, nodes,
|
|
28
|
-
const
|
|
29
|
-
const displayNodes = nodes.slice(0, maxDisplay);
|
|
30
|
-
const remaining = count - maxDisplay;
|
|
27
|
+
function TypeCard({ type, nodes, hasMore, username, activeTab }) {
|
|
28
|
+
const displayNodes = nodes;
|
|
31
29
|
|
|
32
30
|
// Build filter URL based on active tab and type
|
|
33
31
|
const getFilterUrl = () => {
|
|
@@ -50,9 +48,7 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
|
|
|
50
48
|
return (
|
|
51
49
|
<div className="type-group-card">
|
|
52
50
|
<div className="type-group-header">
|
|
53
|
-
<span className="type-group-title">
|
|
54
|
-
{capitalize(type)}s ({count})
|
|
55
|
-
</span>
|
|
51
|
+
<span className="type-group-title">{capitalize(type)}s</span>
|
|
56
52
|
</div>
|
|
57
53
|
|
|
58
54
|
<div className="type-group-nodes">
|
|
@@ -102,7 +98,7 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
|
|
|
102
98
|
display: 'flex',
|
|
103
99
|
alignItems: 'center',
|
|
104
100
|
gap: '4px',
|
|
105
|
-
maxWidth: '
|
|
101
|
+
maxWidth: '40%',
|
|
106
102
|
}}
|
|
107
103
|
>
|
|
108
104
|
{isDefaultBranch && (
|
|
@@ -166,9 +162,9 @@ function TypeCard({ type, nodes, count, username, activeTab }) {
|
|
|
166
162
|
})}
|
|
167
163
|
</div>
|
|
168
164
|
|
|
169
|
-
{
|
|
165
|
+
{hasMore && (
|
|
170
166
|
<Link to={getFilterUrl()} className="type-group-more">
|
|
171
|
-
|
|
167
|
+
More →
|
|
172
168
|
</Link>
|
|
173
169
|
)}
|
|
174
170
|
</div>
|
|
@@ -199,7 +195,7 @@ export function TypeGroupGrid({ groupedData, username, activeTab }) {
|
|
|
199
195
|
key={group.type}
|
|
200
196
|
type={group.type}
|
|
201
197
|
nodes={group.nodes}
|
|
202
|
-
|
|
198
|
+
hasMore={group.hasMore}
|
|
203
199
|
username={username}
|
|
204
200
|
activeTab={activeTab}
|
|
205
201
|
/>
|
|
@@ -242,15 +242,87 @@ describe('<ActiveBranchesSection />', () => {
|
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
it('should
|
|
246
|
-
const
|
|
247
|
-
.
|
|
248
|
-
|
|
245
|
+
it('should show "+N more git namespaces" when more than 3 namespaces', async () => {
|
|
246
|
+
const manyNamespaces = Array.from({ length: 5 }, (_, i) => ({
|
|
247
|
+
name: `project${i}.main.node`,
|
|
248
|
+
gitInfo: {
|
|
249
|
+
repo: `org/repo${i}`,
|
|
250
|
+
branch: 'main',
|
|
251
|
+
defaultBranch: 'main',
|
|
252
|
+
parentNamespace: `project${i}`,
|
|
253
|
+
},
|
|
254
|
+
current: { updatedAt: `2024-01-0${i + 1}T10:00:00Z` },
|
|
255
|
+
}));
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
renderWithContext({
|
|
258
|
+
ownedNodes: manyNamespaces,
|
|
259
|
+
recentlyEdited: [],
|
|
260
|
+
loading: false,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(screen.getByText('+2 more git namespaces')).toBeInTheDocument();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle node with no dots in fullNamespace (dotIdx <= 0 fallback)', async () => {
|
|
269
|
+
// Node name "ns.node" → fullNamespace = "ns" → dotIdx = -1 → baseNamespace = "ns"
|
|
270
|
+
const singleSegmentNode = [
|
|
271
|
+
{
|
|
272
|
+
name: 'ns.node',
|
|
273
|
+
gitInfo: {
|
|
274
|
+
repo: 'myorg/myrepo',
|
|
275
|
+
branch: 'main',
|
|
276
|
+
defaultBranch: 'main',
|
|
277
|
+
// No parentNamespace — fallback to parsing
|
|
278
|
+
},
|
|
279
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
280
|
+
},
|
|
281
|
+
];
|
|
253
282
|
|
|
283
|
+
renderWithContext({
|
|
284
|
+
ownedNodes: singleSegmentNode,
|
|
285
|
+
recentlyEdited: [],
|
|
286
|
+
loading: false,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
// fullNamespace = "ns", dotIdx = -1, baseNamespace = "ns"
|
|
291
|
+
expect(screen.getByText('ns')).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should sort branches with null lastActivity after those with activity', async () => {
|
|
296
|
+
// Creates two branches: one with activity, one without — sort should put active first
|
|
297
|
+
const nodesWithMixedActivity = [
|
|
298
|
+
{
|
|
299
|
+
name: 'myproject.feature.node1',
|
|
300
|
+
gitInfo: {
|
|
301
|
+
repo: 'myorg/myrepo',
|
|
302
|
+
branch: 'feature',
|
|
303
|
+
defaultBranch: 'main',
|
|
304
|
+
parentNamespace: 'myproject',
|
|
305
|
+
isDefaultBranch: false,
|
|
306
|
+
},
|
|
307
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
308
|
+
},
|
|
309
|
+
// main branch will be added by the "ensure default branch present" logic with null activity
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
renderWithContext({
|
|
313
|
+
ownedNodes: nodesWithMixedActivity,
|
|
314
|
+
recentlyEdited: [],
|
|
315
|
+
loading: false,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await waitFor(() => {
|
|
319
|
+
// Both branches should be present
|
|
320
|
+
expect(screen.getByText('feature')).toBeInTheDocument();
|
|
321
|
+
expect(screen.getAllByText('main').length).toBeGreaterThan(0);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should render without errors when given valid props', async () => {
|
|
254
326
|
renderWithContext({
|
|
255
327
|
ownedNodes: mockOwnedNodes,
|
|
256
328
|
recentlyEdited: [],
|
|
@@ -260,10 +332,6 @@ describe('<ActiveBranchesSection />', () => {
|
|
|
260
332
|
await waitFor(() => {
|
|
261
333
|
expect(screen.getByText('myproject')).toBeInTheDocument();
|
|
262
334
|
});
|
|
263
|
-
|
|
264
|
-
// Should still render despite API error
|
|
265
|
-
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
266
|
-
consoleErrorSpy.mockRestore();
|
|
267
335
|
});
|
|
268
336
|
|
|
269
337
|
it('should show default branch even when user has no nodes on it', async () => {
|
|
@@ -201,6 +201,28 @@ describe('<CollectionsSection />', () => {
|
|
|
201
201
|
});
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
it('should log response when fetching all collections succeeds', async () => {
|
|
205
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
206
|
+
mockDjClient.listAllCollections.mockResolvedValue({
|
|
207
|
+
data: { listCollections: mockCollections },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
renderWithContext({
|
|
211
|
+
collections: [],
|
|
212
|
+
loading: false,
|
|
213
|
+
currentUser: mockCurrentUser,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await waitFor(() => {
|
|
217
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
218
|
+
'All collections response:',
|
|
219
|
+
expect.anything(),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
consoleSpy.mockRestore();
|
|
224
|
+
});
|
|
225
|
+
|
|
204
226
|
it('should handle API errors gracefully', async () => {
|
|
205
227
|
mockDjClient.listAllCollections.mockRejectedValue(new Error('API error'));
|
|
206
228
|
|
|
@@ -190,6 +190,63 @@ describe('<MaterializationsSection />', () => {
|
|
|
190
190
|
expect(screen.getByText('+5 more')).toBeInTheDocument();
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it('should sort nodes with null validThroughTs after those with timestamps', () => {
|
|
194
|
+
const pendingNode = createMockNode('pending_cube', null);
|
|
195
|
+
const freshNode = createMockNode('fresh_cube', 12);
|
|
196
|
+
|
|
197
|
+
render(
|
|
198
|
+
<MemoryRouter>
|
|
199
|
+
<MaterializationsSection
|
|
200
|
+
nodes={[pendingNode, freshNode]}
|
|
201
|
+
loading={false}
|
|
202
|
+
/>
|
|
203
|
+
</MemoryRouter>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const allCubes = screen.getAllByText(/cube/);
|
|
207
|
+
// fresh (has timestamp) should appear before pending (null timestamp)
|
|
208
|
+
expect(allCubes[0]).toHaveTextContent('fresh_cube');
|
|
209
|
+
expect(allCubes[1]).toHaveTextContent('pending_cube');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should keep relative order when both nodes have null validThroughTs', () => {
|
|
213
|
+
const pending1 = createMockNode('pending_a', null);
|
|
214
|
+
const pending2 = createMockNode('pending_b', null);
|
|
215
|
+
|
|
216
|
+
render(
|
|
217
|
+
<MemoryRouter>
|
|
218
|
+
<MaterializationsSection nodes={[pending1, pending2]} loading={false} />
|
|
219
|
+
</MemoryRouter>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Both pending — neither crashes, both appear
|
|
223
|
+
expect(screen.getByText('pending_a')).toBeInTheDocument();
|
|
224
|
+
expect(screen.getByText('pending_b')).toBeInTheDocument();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should show "just now" for materializations updated less than 1 hour ago', () => {
|
|
228
|
+
const justNowTs = now - 5 * 60 * 1000; // 5 minutes ago
|
|
229
|
+
const justNowNode = {
|
|
230
|
+
name: 'just_now_cube',
|
|
231
|
+
type: 'CUBE',
|
|
232
|
+
current: {
|
|
233
|
+
displayName: 'just_now_cube',
|
|
234
|
+
availability: {
|
|
235
|
+
validThroughTs: justNowTs,
|
|
236
|
+
},
|
|
237
|
+
materializations: [{ name: 'mat1', schedule: '@hourly' }],
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
render(
|
|
242
|
+
<MemoryRouter>
|
|
243
|
+
<MaterializationsSection nodes={[justNowNode]} loading={false} />
|
|
244
|
+
</MemoryRouter>,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(screen.getByText('🟢 just now')).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
193
250
|
it('should handle undefined nodes gracefully', () => {
|
|
194
251
|
render(
|
|
195
252
|
<MemoryRouter>
|
|
@@ -18,7 +18,7 @@ jest.mock('../TypeGroupGrid', () => ({
|
|
|
18
18
|
<div data-testid="type-group-grid">
|
|
19
19
|
{groupedData.map(group => (
|
|
20
20
|
<div key={group.type}>
|
|
21
|
-
{group.type}: {group.
|
|
21
|
+
{group.type}: {group.nodes.length} nodes
|
|
22
22
|
</div>
|
|
23
23
|
))}
|
|
24
24
|
</div>
|
|
@@ -26,6 +26,11 @@ jest.mock('../TypeGroupGrid', () => ({
|
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
28
|
describe('<MyNodesSection />', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
// Default to list view for most tests; Group by Type tests manage localStorage themselves
|
|
31
|
+
localStorage.setItem('workspace_groupByType', 'false');
|
|
32
|
+
});
|
|
33
|
+
|
|
29
34
|
const mockOwnedNodes = [
|
|
30
35
|
{
|
|
31
36
|
name: 'default.owned_metric',
|
|
@@ -102,10 +107,10 @@ describe('<MyNodesSection />', () => {
|
|
|
102
107
|
</MemoryRouter>,
|
|
103
108
|
);
|
|
104
109
|
|
|
105
|
-
expect(screen.getByText('Owned
|
|
110
|
+
expect(screen.getByText('Owned')).toBeInTheDocument();
|
|
106
111
|
// Should only count watched nodes that aren't owned (1 watched-only)
|
|
107
112
|
expect(screen.getByText('Watched (1)')).toBeInTheDocument();
|
|
108
|
-
expect(screen.getByText('Recent Edits
|
|
113
|
+
expect(screen.getByText('Recent Edits')).toBeInTheDocument();
|
|
109
114
|
});
|
|
110
115
|
|
|
111
116
|
it('should show owned nodes by default', () => {
|
|
@@ -160,7 +165,7 @@ describe('<MyNodesSection />', () => {
|
|
|
160
165
|
</MemoryRouter>,
|
|
161
166
|
);
|
|
162
167
|
|
|
163
|
-
fireEvent.click(screen.getByText('Recent Edits
|
|
168
|
+
fireEvent.click(screen.getByText('Recent Edits'));
|
|
164
169
|
|
|
165
170
|
expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
|
|
166
171
|
});
|
|
@@ -183,7 +188,7 @@ describe('<MyNodesSection />', () => {
|
|
|
183
188
|
expect(screen.getByText('No watched nodes')).toBeInTheDocument();
|
|
184
189
|
});
|
|
185
190
|
|
|
186
|
-
it('should limit displayed nodes to 8', () => {
|
|
191
|
+
it('should limit displayed nodes to 8 in list view', () => {
|
|
187
192
|
const manyNodes = Array.from({ length: 15 }, (_, i) => ({
|
|
188
193
|
name: `default.metric_${i}`,
|
|
189
194
|
type: 'METRIC',
|
|
@@ -227,6 +232,45 @@ describe('<MyNodesSection />', () => {
|
|
|
227
232
|
);
|
|
228
233
|
});
|
|
229
234
|
|
|
235
|
+
it('should auto-switch to edited tab when no owned nodes but has recently edited', async () => {
|
|
236
|
+
const { waitFor } = await import('@testing-library/react');
|
|
237
|
+
|
|
238
|
+
render(
|
|
239
|
+
<MemoryRouter>
|
|
240
|
+
<MyNodesSection
|
|
241
|
+
ownedNodes={[]}
|
|
242
|
+
watchedNodes={[]}
|
|
243
|
+
recentlyEdited={mockRecentlyEdited}
|
|
244
|
+
username="test.user@example.com"
|
|
245
|
+
loading={false}
|
|
246
|
+
/>
|
|
247
|
+
</MemoryRouter>,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// After effect fires, should auto-switch to edited tab
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should show "No recent edits" when on edited tab with no recent edits', () => {
|
|
257
|
+
render(
|
|
258
|
+
<MemoryRouter>
|
|
259
|
+
<MyNodesSection
|
|
260
|
+
ownedNodes={mockOwnedNodes}
|
|
261
|
+
watchedNodes={[]}
|
|
262
|
+
recentlyEdited={[]}
|
|
263
|
+
username="test.user@example.com"
|
|
264
|
+
loading={false}
|
|
265
|
+
/>
|
|
266
|
+
</MemoryRouter>,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
fireEvent.click(screen.getByText('Recent Edits'));
|
|
270
|
+
|
|
271
|
+
expect(screen.getByText('No recent edits')).toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
|
|
230
274
|
it('should filter watched nodes to exclude owned nodes', () => {
|
|
231
275
|
const ownedNodes = [
|
|
232
276
|
{ name: 'node1', type: 'METRIC', current: {} },
|
|
@@ -290,7 +334,7 @@ describe('<MyNodesSection />', () => {
|
|
|
290
334
|
expect(screen.queryByLabelText('Group by Type')).not.toBeInTheDocument();
|
|
291
335
|
});
|
|
292
336
|
|
|
293
|
-
it('should toggle between
|
|
337
|
+
it('should toggle between grouped and list view', () => {
|
|
294
338
|
render(
|
|
295
339
|
<MemoryRouter>
|
|
296
340
|
<MyNodesSection
|
|
@@ -303,17 +347,17 @@ describe('<MyNodesSection />', () => {
|
|
|
303
347
|
</MemoryRouter>,
|
|
304
348
|
);
|
|
305
349
|
|
|
306
|
-
// Initially should show
|
|
307
|
-
expect(screen.getByTestId('
|
|
308
|
-
expect(screen.queryByTestId('
|
|
350
|
+
// Initially should show grouped view (default is true)
|
|
351
|
+
expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
|
|
352
|
+
expect(screen.queryByTestId('node-list')).not.toBeInTheDocument();
|
|
309
353
|
|
|
310
|
-
// Click toggle
|
|
354
|
+
// Click toggle to switch to list view
|
|
311
355
|
const toggle = screen.getByLabelText('Group by Type');
|
|
312
356
|
fireEvent.click(toggle);
|
|
313
357
|
|
|
314
|
-
// Should now show
|
|
315
|
-
expect(screen.queryByTestId('
|
|
316
|
-
expect(screen.getByTestId('
|
|
358
|
+
// Should now show list view
|
|
359
|
+
expect(screen.queryByTestId('type-group-grid')).not.toBeInTheDocument();
|
|
360
|
+
expect(screen.getByTestId('node-list')).toBeInTheDocument();
|
|
317
361
|
});
|
|
318
362
|
|
|
319
363
|
it('should persist toggle state to localStorage', () => {
|
|
@@ -332,10 +376,10 @@ describe('<MyNodesSection />', () => {
|
|
|
332
376
|
const toggle = screen.getByLabelText('Group by Type');
|
|
333
377
|
fireEvent.click(toggle);
|
|
334
378
|
|
|
335
|
-
expect(localStorage.getItem('workspace_groupByType')).toBe('
|
|
379
|
+
expect(localStorage.getItem('workspace_groupByType')).toBe('false');
|
|
336
380
|
|
|
337
381
|
fireEvent.click(toggle);
|
|
338
|
-
expect(localStorage.getItem('workspace_groupByType')).toBe('
|
|
382
|
+
expect(localStorage.getItem('workspace_groupByType')).toBe('true');
|
|
339
383
|
});
|
|
340
384
|
|
|
341
385
|
it('should load toggle state from localStorage', () => {
|
|
@@ -378,9 +422,7 @@ describe('<MyNodesSection />', () => {
|
|
|
378
422
|
</MemoryRouter>,
|
|
379
423
|
);
|
|
380
424
|
|
|
381
|
-
|
|
382
|
-
fireEvent.click(toggle);
|
|
383
|
-
|
|
425
|
+
// Default is grouped view (localStorage cleared by beforeEach → null → true)
|
|
384
426
|
expect(screen.getByText('metric: 2 nodes')).toBeInTheDocument();
|
|
385
427
|
expect(screen.getByText('dimension: 1 nodes')).toBeInTheDocument();
|
|
386
428
|
expect(screen.getByText('source: 1 nodes')).toBeInTheDocument();
|