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,556 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { TypeGroupGrid } from '../TypeGroupGrid';
|
|
5
|
+
|
|
6
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
7
|
+
jest.mock('../../../components/NodeComponents', () => ({
|
|
8
|
+
NodeBadge: ({ type }) => <span data-testid="badge">{type}</span>,
|
|
9
|
+
NodeLink: ({ node }) => (
|
|
10
|
+
<a href={`/nodes/${node.name}`} data-testid={`node-link-${node.name}`}>
|
|
11
|
+
{node.name}
|
|
12
|
+
</a>
|
|
13
|
+
),
|
|
14
|
+
}));
|
|
15
|
+
jest.mock('../../../components/NodeListActions', () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: ({ nodeName }) => (
|
|
18
|
+
<div data-testid={`actions-${nodeName}`}>actions</div>
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('<TypeGroupGrid />', () => {
|
|
23
|
+
const mockGroupedData = [
|
|
24
|
+
{
|
|
25
|
+
type: 'metric',
|
|
26
|
+
count: 5,
|
|
27
|
+
nodes: [
|
|
28
|
+
{
|
|
29
|
+
name: 'default.revenue',
|
|
30
|
+
type: 'metric',
|
|
31
|
+
current: {
|
|
32
|
+
displayName: 'Revenue',
|
|
33
|
+
updatedAt: '2024-01-01T10:00:00Z',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'default.orders',
|
|
38
|
+
type: 'metric',
|
|
39
|
+
current: { displayName: 'Orders', updatedAt: '2024-01-01T12:00:00Z' },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'default.users',
|
|
43
|
+
type: 'metric',
|
|
44
|
+
current: { displayName: 'Users', updatedAt: '2024-01-01T14:00:00Z' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'default.conversion',
|
|
48
|
+
type: 'metric',
|
|
49
|
+
current: {
|
|
50
|
+
displayName: 'Conversion',
|
|
51
|
+
updatedAt: '2024-01-01T16:00:00Z',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'default.bounce_rate',
|
|
56
|
+
type: 'metric',
|
|
57
|
+
current: {
|
|
58
|
+
displayName: 'Bounce Rate',
|
|
59
|
+
updatedAt: '2024-01-01T18:00:00Z',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'dimension',
|
|
66
|
+
count: 2,
|
|
67
|
+
nodes: [
|
|
68
|
+
{
|
|
69
|
+
name: 'default.dim_users',
|
|
70
|
+
type: 'dimension',
|
|
71
|
+
current: { displayName: 'Users', updatedAt: '2024-01-01T08:00:00Z' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'default.dim_products',
|
|
75
|
+
type: 'dimension',
|
|
76
|
+
current: {
|
|
77
|
+
displayName: 'Products',
|
|
78
|
+
updatedAt: '2024-01-01T09:00:00Z',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
it('should render empty state when no data', () => {
|
|
86
|
+
render(
|
|
87
|
+
<MemoryRouter>
|
|
88
|
+
<TypeGroupGrid
|
|
89
|
+
groupedData={[]}
|
|
90
|
+
username="test.user@example.com"
|
|
91
|
+
activeTab="owned"
|
|
92
|
+
/>
|
|
93
|
+
</MemoryRouter>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('No nodes to display')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should render type cards with correct counts', () => {
|
|
100
|
+
render(
|
|
101
|
+
<MemoryRouter>
|
|
102
|
+
<TypeGroupGrid
|
|
103
|
+
groupedData={mockGroupedData}
|
|
104
|
+
username="test.user@example.com"
|
|
105
|
+
activeTab="owned"
|
|
106
|
+
/>
|
|
107
|
+
</MemoryRouter>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(screen.getByText('Metrics (5)')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText('Dimensions (2)')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should display up to 10 nodes per type', () => {
|
|
115
|
+
render(
|
|
116
|
+
<MemoryRouter>
|
|
117
|
+
<TypeGroupGrid
|
|
118
|
+
groupedData={mockGroupedData}
|
|
119
|
+
username="test.user@example.com"
|
|
120
|
+
activeTab="owned"
|
|
121
|
+
/>
|
|
122
|
+
</MemoryRouter>,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Should show all 5 metrics (under the limit of 10)
|
|
126
|
+
expect(screen.getByText('default.revenue')).toBeInTheDocument();
|
|
127
|
+
expect(screen.getByText('default.orders')).toBeInTheDocument();
|
|
128
|
+
expect(screen.getByText('default.users')).toBeInTheDocument();
|
|
129
|
+
expect(screen.getByText('default.conversion')).toBeInTheDocument();
|
|
130
|
+
expect(screen.getByText('default.bounce_rate')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should show "+X more" link when more than 10 nodes', () => {
|
|
134
|
+
const manyNodesData = [
|
|
135
|
+
{
|
|
136
|
+
type: 'metric',
|
|
137
|
+
count: 15,
|
|
138
|
+
nodes: Array.from({ length: 15 }, (_, i) => ({
|
|
139
|
+
name: `default.metric_${i}`,
|
|
140
|
+
type: 'metric',
|
|
141
|
+
current: {
|
|
142
|
+
displayName: `Metric ${i}`,
|
|
143
|
+
updatedAt: '2024-01-01T10:00:00Z',
|
|
144
|
+
},
|
|
145
|
+
})),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
render(
|
|
149
|
+
<MemoryRouter>
|
|
150
|
+
<TypeGroupGrid
|
|
151
|
+
groupedData={manyNodesData}
|
|
152
|
+
username="test.user@example.com"
|
|
153
|
+
activeTab="owned"
|
|
154
|
+
/>
|
|
155
|
+
</MemoryRouter>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Metrics: 15 nodes, showing 10, so +5 more
|
|
159
|
+
expect(screen.getByText('+5 more →')).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should not show "+X more" link when 10 or fewer nodes', () => {
|
|
163
|
+
render(
|
|
164
|
+
<MemoryRouter>
|
|
165
|
+
<TypeGroupGrid
|
|
166
|
+
groupedData={mockGroupedData}
|
|
167
|
+
username="test.user@example.com"
|
|
168
|
+
activeTab="owned"
|
|
169
|
+
/>
|
|
170
|
+
</MemoryRouter>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Metrics: 5 nodes (under 10), no "+X more" needed
|
|
174
|
+
const metricCard = screen
|
|
175
|
+
.getByText('Metrics (5)')
|
|
176
|
+
.closest('.type-group-card');
|
|
177
|
+
expect(metricCard).not.toHaveTextContent('more →');
|
|
178
|
+
|
|
179
|
+
// Dimensions: 2 nodes, no "+X more" needed
|
|
180
|
+
const dimensionCard = screen
|
|
181
|
+
.getByText('Dimensions (2)')
|
|
182
|
+
.closest('.type-group-card');
|
|
183
|
+
expect(dimensionCard).not.toHaveTextContent('more →');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should render node badges and links', () => {
|
|
187
|
+
render(
|
|
188
|
+
<MemoryRouter>
|
|
189
|
+
<TypeGroupGrid
|
|
190
|
+
groupedData={mockGroupedData}
|
|
191
|
+
username="test.user@example.com"
|
|
192
|
+
activeTab="owned"
|
|
193
|
+
/>
|
|
194
|
+
</MemoryRouter>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Should have badges for each displayed node
|
|
198
|
+
const badges = screen.getAllByTestId('badge');
|
|
199
|
+
expect(badges.length).toBeGreaterThan(0);
|
|
200
|
+
|
|
201
|
+
// Should have clickable links
|
|
202
|
+
const revenueLink = screen.getByText('default.revenue');
|
|
203
|
+
expect(revenueLink).toHaveAttribute('href', '/nodes/default.revenue');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should render node actions', () => {
|
|
207
|
+
render(
|
|
208
|
+
<MemoryRouter>
|
|
209
|
+
<TypeGroupGrid
|
|
210
|
+
groupedData={mockGroupedData}
|
|
211
|
+
username="test.user@example.com"
|
|
212
|
+
activeTab="owned"
|
|
213
|
+
/>
|
|
214
|
+
</MemoryRouter>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Should have actions for each node
|
|
218
|
+
const actions = screen.getAllByText(/^actions$/);
|
|
219
|
+
expect(actions.length).toBe(7); // 5 metrics + 2 dimensions displayed (all under maxDisplay=10)
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should format relative time correctly', () => {
|
|
223
|
+
const now = new Date();
|
|
224
|
+
const oneHourAgo = new Date(now - 60 * 60 * 1000);
|
|
225
|
+
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
226
|
+
|
|
227
|
+
const recentNodes = [
|
|
228
|
+
{
|
|
229
|
+
type: 'metric',
|
|
230
|
+
count: 2,
|
|
231
|
+
nodes: [
|
|
232
|
+
{
|
|
233
|
+
name: 'default.recent',
|
|
234
|
+
type: 'metric',
|
|
235
|
+
current: {
|
|
236
|
+
displayName: 'Recent',
|
|
237
|
+
updatedAt: oneHourAgo.toISOString(),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'default.older',
|
|
242
|
+
type: 'metric',
|
|
243
|
+
current: {
|
|
244
|
+
displayName: 'Older',
|
|
245
|
+
updatedAt: oneDayAgo.toISOString(),
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
<MemoryRouter>
|
|
254
|
+
<TypeGroupGrid
|
|
255
|
+
groupedData={recentNodes}
|
|
256
|
+
username="test.user@example.com"
|
|
257
|
+
activeTab="owned"
|
|
258
|
+
/>
|
|
259
|
+
</MemoryRouter>,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Should show time in hours or days format
|
|
263
|
+
// Note: exact values depend on when test runs, so we just check they exist
|
|
264
|
+
expect(screen.getAllByText(/\d+[mhd]$/)).toHaveLength(2);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should generate correct filter URLs for owned tab', () => {
|
|
268
|
+
const manyNodesData = [
|
|
269
|
+
{
|
|
270
|
+
type: 'metric',
|
|
271
|
+
count: 15,
|
|
272
|
+
nodes: Array.from({ length: 15 }, (_, i) => ({
|
|
273
|
+
name: `default.metric_${i}`,
|
|
274
|
+
type: 'metric',
|
|
275
|
+
current: {
|
|
276
|
+
displayName: `Metric ${i}`,
|
|
277
|
+
updatedAt: '2024-01-01T10:00:00Z',
|
|
278
|
+
},
|
|
279
|
+
})),
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
render(
|
|
283
|
+
<MemoryRouter>
|
|
284
|
+
<TypeGroupGrid
|
|
285
|
+
groupedData={manyNodesData}
|
|
286
|
+
username="test.user@example.com"
|
|
287
|
+
activeTab="owned"
|
|
288
|
+
/>
|
|
289
|
+
</MemoryRouter>,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const moreLink = screen.getByText('+5 more →');
|
|
293
|
+
expect(moreLink).toHaveAttribute(
|
|
294
|
+
'href',
|
|
295
|
+
'/?ownedBy=test.user%40example.com&type=metric',
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should generate correct filter URLs for edited tab', () => {
|
|
300
|
+
const manyNodesData = [
|
|
301
|
+
{
|
|
302
|
+
type: 'metric',
|
|
303
|
+
count: 15,
|
|
304
|
+
nodes: Array.from({ length: 15 }, (_, i) => ({
|
|
305
|
+
name: `default.metric_${i}`,
|
|
306
|
+
type: 'metric',
|
|
307
|
+
current: {
|
|
308
|
+
displayName: `Metric ${i}`,
|
|
309
|
+
updatedAt: '2024-01-01T10:00:00Z',
|
|
310
|
+
},
|
|
311
|
+
})),
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
render(
|
|
315
|
+
<MemoryRouter>
|
|
316
|
+
<TypeGroupGrid
|
|
317
|
+
groupedData={manyNodesData}
|
|
318
|
+
username="test.user@example.com"
|
|
319
|
+
activeTab="edited"
|
|
320
|
+
/>
|
|
321
|
+
</MemoryRouter>,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const moreLink = screen.getByText('+5 more →');
|
|
325
|
+
expect(moreLink).toHaveAttribute(
|
|
326
|
+
'href',
|
|
327
|
+
'/?updatedBy=test.user%40example.com&type=metric',
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should capitalize type names', () => {
|
|
332
|
+
render(
|
|
333
|
+
<MemoryRouter>
|
|
334
|
+
<TypeGroupGrid
|
|
335
|
+
groupedData={mockGroupedData}
|
|
336
|
+
username="test.user@example.com"
|
|
337
|
+
activeTab="owned"
|
|
338
|
+
/>
|
|
339
|
+
</MemoryRouter>,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// "metric" should be displayed as "Metrics"
|
|
343
|
+
expect(screen.getByText('Metrics (5)')).toBeInTheDocument();
|
|
344
|
+
// "dimension" should be displayed as "Dimensions"
|
|
345
|
+
expect(screen.getByText('Dimensions (2)')).toBeInTheDocument();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should handle nodes with only repo (no branch)', () => {
|
|
349
|
+
const repoOnlyData = [
|
|
350
|
+
{
|
|
351
|
+
type: 'metric',
|
|
352
|
+
count: 1,
|
|
353
|
+
nodes: [
|
|
354
|
+
{
|
|
355
|
+
name: 'default.test',
|
|
356
|
+
type: 'metric',
|
|
357
|
+
gitInfo: {
|
|
358
|
+
repo: 'myorg/myrepo',
|
|
359
|
+
},
|
|
360
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
render(
|
|
367
|
+
<MemoryRouter>
|
|
368
|
+
<TypeGroupGrid
|
|
369
|
+
groupedData={repoOnlyData}
|
|
370
|
+
username="test.user@example.com"
|
|
371
|
+
activeTab="owned"
|
|
372
|
+
/>
|
|
373
|
+
</MemoryRouter>,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(screen.getByText('myorg/myrepo')).toBeInTheDocument();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should handle nodes with only branch (no repo)', () => {
|
|
380
|
+
const branchOnlyData = [
|
|
381
|
+
{
|
|
382
|
+
type: 'metric',
|
|
383
|
+
count: 1,
|
|
384
|
+
nodes: [
|
|
385
|
+
{
|
|
386
|
+
name: 'default.test',
|
|
387
|
+
type: 'metric',
|
|
388
|
+
gitInfo: {
|
|
389
|
+
branch: 'feature-branch',
|
|
390
|
+
},
|
|
391
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
render(
|
|
398
|
+
<MemoryRouter>
|
|
399
|
+
<TypeGroupGrid
|
|
400
|
+
groupedData={branchOnlyData}
|
|
401
|
+
username="test.user@example.com"
|
|
402
|
+
activeTab="owned"
|
|
403
|
+
/>
|
|
404
|
+
</MemoryRouter>,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(screen.getByText('feature-branch')).toBeInTheDocument();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should handle nodes without gitInfo', () => {
|
|
411
|
+
const noGitData = [
|
|
412
|
+
{
|
|
413
|
+
type: 'metric',
|
|
414
|
+
count: 1,
|
|
415
|
+
nodes: [
|
|
416
|
+
{
|
|
417
|
+
name: 'default.test',
|
|
418
|
+
type: 'metric',
|
|
419
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
render(
|
|
426
|
+
<MemoryRouter>
|
|
427
|
+
<TypeGroupGrid
|
|
428
|
+
groupedData={noGitData}
|
|
429
|
+
username="test.user@example.com"
|
|
430
|
+
activeTab="owned"
|
|
431
|
+
/>
|
|
432
|
+
</MemoryRouter>,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Should render without git info
|
|
436
|
+
expect(screen.getByText('default.test')).toBeInTheDocument();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should show invalid node indicator', () => {
|
|
440
|
+
const invalidNodeData = [
|
|
441
|
+
{
|
|
442
|
+
type: 'metric',
|
|
443
|
+
count: 1,
|
|
444
|
+
nodes: [
|
|
445
|
+
{
|
|
446
|
+
name: 'default.invalid_metric',
|
|
447
|
+
type: 'metric',
|
|
448
|
+
status: 'invalid',
|
|
449
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
render(
|
|
456
|
+
<MemoryRouter>
|
|
457
|
+
<TypeGroupGrid
|
|
458
|
+
groupedData={invalidNodeData}
|
|
459
|
+
username="test.user@example.com"
|
|
460
|
+
activeTab="owned"
|
|
461
|
+
/>
|
|
462
|
+
</MemoryRouter>,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(screen.getByTitle('Invalid node')).toBeInTheDocument();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should show draft mode indicator', () => {
|
|
469
|
+
const draftNodeData = [
|
|
470
|
+
{
|
|
471
|
+
type: 'metric',
|
|
472
|
+
count: 1,
|
|
473
|
+
nodes: [
|
|
474
|
+
{
|
|
475
|
+
name: 'default.draft_metric',
|
|
476
|
+
type: 'metric',
|
|
477
|
+
mode: 'draft',
|
|
478
|
+
current: { updatedAt: '2024-01-01T10:00:00Z' },
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
render(
|
|
485
|
+
<MemoryRouter>
|
|
486
|
+
<TypeGroupGrid
|
|
487
|
+
groupedData={draftNodeData}
|
|
488
|
+
username="test.user@example.com"
|
|
489
|
+
activeTab="owned"
|
|
490
|
+
/>
|
|
491
|
+
</MemoryRouter>,
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
expect(screen.getByTitle('Draft mode')).toBeInTheDocument();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should handle nodes without updatedAt', () => {
|
|
498
|
+
const noTimeData = [
|
|
499
|
+
{
|
|
500
|
+
type: 'metric',
|
|
501
|
+
count: 1,
|
|
502
|
+
nodes: [
|
|
503
|
+
{
|
|
504
|
+
name: 'default.test',
|
|
505
|
+
type: 'metric',
|
|
506
|
+
current: {},
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
render(
|
|
513
|
+
<MemoryRouter>
|
|
514
|
+
<TypeGroupGrid
|
|
515
|
+
groupedData={noTimeData}
|
|
516
|
+
username="test.user@example.com"
|
|
517
|
+
activeTab="owned"
|
|
518
|
+
/>
|
|
519
|
+
</MemoryRouter>,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Should render without timestamp
|
|
523
|
+
expect(screen.getByText('default.test')).toBeInTheDocument();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should handle watched tab filter URLs', () => {
|
|
527
|
+
const manyNodesData = [
|
|
528
|
+
{
|
|
529
|
+
type: 'metric',
|
|
530
|
+
count: 15,
|
|
531
|
+
nodes: Array.from({ length: 15 }, (_, i) => ({
|
|
532
|
+
name: `default.metric_${i}`,
|
|
533
|
+
type: 'metric',
|
|
534
|
+
current: {
|
|
535
|
+
displayName: `Metric ${i}`,
|
|
536
|
+
updatedAt: '2024-01-01T10:00:00Z',
|
|
537
|
+
},
|
|
538
|
+
})),
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
render(
|
|
543
|
+
<MemoryRouter>
|
|
544
|
+
<TypeGroupGrid
|
|
545
|
+
groupedData={manyNodesData}
|
|
546
|
+
username="test.user@example.com"
|
|
547
|
+
activeTab="watched"
|
|
548
|
+
/>
|
|
549
|
+
</MemoryRouter>,
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const moreLink = screen.getByText('+5 more →');
|
|
553
|
+
// For watched tab, should filter by type only
|
|
554
|
+
expect(moreLink).toHaveAttribute('href', '/?type=metric');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
3
|
+
import {
|
|
4
|
+
useCurrentUser,
|
|
5
|
+
useWorkspaceOwnedNodes,
|
|
6
|
+
useWorkspaceRecentlyEdited,
|
|
7
|
+
useWorkspaceWatchedNodes,
|
|
8
|
+
useWorkspaceCollections,
|
|
9
|
+
useWorkspaceNotifications,
|
|
10
|
+
useWorkspaceMaterializations,
|
|
11
|
+
useWorkspaceNeedsAttention,
|
|
12
|
+
usePersonalNamespace,
|
|
13
|
+
} from '../../hooks/useWorkspaceData';
|
|
14
|
+
import { NotificationsSection } from './NotificationsSection';
|
|
15
|
+
import { NeedsAttentionSection } from './NeedsAttentionSection';
|
|
16
|
+
import { MyNodesSection } from './MyNodesSection';
|
|
17
|
+
import { CollectionsSection } from './CollectionsSection';
|
|
18
|
+
import { MaterializationsSection } from './MaterializationsSection';
|
|
19
|
+
import { ActiveBranchesSection } from './ActiveBranchesSection';
|
|
20
|
+
|
|
21
|
+
import 'styles/settings.css';
|
|
22
|
+
import './MyWorkspacePage.css';
|
|
23
|
+
|
|
24
|
+
export function MyWorkspacePage() {
|
|
25
|
+
// Use custom hooks for all data fetching
|
|
26
|
+
const { data: currentUser, loading: userLoading } = useCurrentUser();
|
|
27
|
+
const { data: ownedNodes, loading: ownedLoading } = useWorkspaceOwnedNodes(
|
|
28
|
+
currentUser?.username,
|
|
29
|
+
);
|
|
30
|
+
const { data: recentlyEdited, loading: editedLoading } =
|
|
31
|
+
useWorkspaceRecentlyEdited(currentUser?.username);
|
|
32
|
+
const { data: watchedNodes, loading: watchedLoading } =
|
|
33
|
+
useWorkspaceWatchedNodes(currentUser?.username);
|
|
34
|
+
const { data: collections, loading: collectionsLoading } =
|
|
35
|
+
useWorkspaceCollections(currentUser?.username);
|
|
36
|
+
const { data: notifications, loading: notificationsLoading } =
|
|
37
|
+
useWorkspaceNotifications(currentUser?.username);
|
|
38
|
+
const { data: materializedNodes, loading: materializationsLoading } =
|
|
39
|
+
useWorkspaceMaterializations(currentUser?.username);
|
|
40
|
+
const { data: needsAttentionData, loading: needsAttentionLoading } =
|
|
41
|
+
useWorkspaceNeedsAttention(currentUser?.username);
|
|
42
|
+
const { exists: hasPersonalNamespace, loading: namespaceLoading } =
|
|
43
|
+
usePersonalNamespace(currentUser?.username);
|
|
44
|
+
|
|
45
|
+
// Extract needs attention data
|
|
46
|
+
const {
|
|
47
|
+
nodesMissingDescription = [],
|
|
48
|
+
invalidNodes = [],
|
|
49
|
+
staleDrafts = [],
|
|
50
|
+
orphanedDimensions = [],
|
|
51
|
+
} = needsAttentionData || {};
|
|
52
|
+
|
|
53
|
+
// Combine loading states for "My Nodes" section
|
|
54
|
+
const myNodesLoading = ownedLoading || editedLoading || watchedLoading;
|
|
55
|
+
|
|
56
|
+
// Filter stale materializations (> 72 hours old)
|
|
57
|
+
const staleMaterializations = materializedNodes.filter(node => {
|
|
58
|
+
const validThroughTs = node.current?.availability?.validThroughTs;
|
|
59
|
+
if (!validThroughTs) return false; // Pending ones aren't "stale"
|
|
60
|
+
const hoursSinceUpdate = (Date.now() - validThroughTs) / (1000 * 60 * 60);
|
|
61
|
+
return hoursSinceUpdate > 72;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const hasActionableItems =
|
|
65
|
+
nodesMissingDescription.length > 0 ||
|
|
66
|
+
invalidNodes.length > 0 ||
|
|
67
|
+
staleDrafts.length > 0 ||
|
|
68
|
+
staleMaterializations.length > 0 ||
|
|
69
|
+
orphanedDimensions.length > 0;
|
|
70
|
+
|
|
71
|
+
// Personal namespace for the user
|
|
72
|
+
const usernameForNamespace = currentUser?.username?.split('@')[0] || '';
|
|
73
|
+
const personalNamespace = `users.${usernameForNamespace}`;
|
|
74
|
+
|
|
75
|
+
if (userLoading) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="settings-page" style={{ padding: '1.5rem 2rem' }}>
|
|
78
|
+
<h1 className="settings-title">Dashboard</h1>
|
|
79
|
+
<div style={{ textAlign: 'center', padding: '3rem' }}>
|
|
80
|
+
<LoadingIcon />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate stats
|
|
87
|
+
return (
|
|
88
|
+
<div className="settings-page" style={{ padding: '1.5rem 2rem' }}>
|
|
89
|
+
{/* Two Column Layout: Collections/Organization (left) + Activity (right) */}
|
|
90
|
+
<div className="workspace-layout">
|
|
91
|
+
{/* Left Column: Organization (65%) */}
|
|
92
|
+
<div className="workspace-left-column">
|
|
93
|
+
{/* Collections (My + Featured) */}
|
|
94
|
+
<CollectionsSection
|
|
95
|
+
collections={collections}
|
|
96
|
+
loading={collectionsLoading}
|
|
97
|
+
currentUser={currentUser}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{/* My Nodes */}
|
|
101
|
+
<MyNodesSection
|
|
102
|
+
ownedNodes={ownedNodes}
|
|
103
|
+
watchedNodes={watchedNodes}
|
|
104
|
+
recentlyEdited={recentlyEdited}
|
|
105
|
+
username={currentUser?.username}
|
|
106
|
+
loading={myNodesLoading}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Right Column: Activity (35%) */}
|
|
111
|
+
<div className="workspace-right-column">
|
|
112
|
+
{/* Notifications */}
|
|
113
|
+
<NotificationsSection
|
|
114
|
+
notifications={notifications}
|
|
115
|
+
username={currentUser?.username}
|
|
116
|
+
loading={notificationsLoading}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
{/* Active Branches */}
|
|
120
|
+
<ActiveBranchesSection
|
|
121
|
+
ownedNodes={ownedNodes}
|
|
122
|
+
recentlyEdited={recentlyEdited}
|
|
123
|
+
loading={myNodesLoading}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
{/* Materializations */}
|
|
127
|
+
<MaterializationsSection
|
|
128
|
+
nodes={materializedNodes}
|
|
129
|
+
loading={materializationsLoading}
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
{/* Needs Attention */}
|
|
133
|
+
<NeedsAttentionSection
|
|
134
|
+
nodesMissingDescription={nodesMissingDescription}
|
|
135
|
+
invalidNodes={invalidNodes}
|
|
136
|
+
staleDrafts={staleDrafts}
|
|
137
|
+
staleMaterializations={staleMaterializations}
|
|
138
|
+
orphanedDimensions={orphanedDimensions}
|
|
139
|
+
username={currentUser?.username}
|
|
140
|
+
hasItems={hasActionableItems}
|
|
141
|
+
loading={needsAttentionLoading || materializationsLoading}
|
|
142
|
+
personalNamespace={personalNamespace}
|
|
143
|
+
hasPersonalNamespace={hasPersonalNamespace}
|
|
144
|
+
namespaceLoading={namespaceLoading}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|