datajunction-ui 0.0.74 → 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,238 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { MaterializationsSection } from '../MaterializationsSection';
|
|
5
|
+
|
|
6
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
7
|
+
|
|
8
|
+
describe('<MaterializationsSection />', () => {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
|
|
11
|
+
const createMockNode = (name, hoursAgo, schedule = '@daily') => ({
|
|
12
|
+
name,
|
|
13
|
+
type: 'CUBE',
|
|
14
|
+
current: {
|
|
15
|
+
displayName: name,
|
|
16
|
+
availability: {
|
|
17
|
+
validThroughTs:
|
|
18
|
+
hoursAgo !== null ? now - hoursAgo * 60 * 60 * 1000 : null,
|
|
19
|
+
table: 'warehouse.materialized_table',
|
|
20
|
+
},
|
|
21
|
+
materializations: [{ name: 'mat1', schedule }],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render loading state', () => {
|
|
26
|
+
render(
|
|
27
|
+
<MemoryRouter>
|
|
28
|
+
<MaterializationsSection nodes={[]} loading={true} />
|
|
29
|
+
</MemoryRouter>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(
|
|
33
|
+
screen.queryByText('No materializations configured.'),
|
|
34
|
+
).not.toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should render empty state when no materializations', () => {
|
|
38
|
+
render(
|
|
39
|
+
<MemoryRouter>
|
|
40
|
+
<MaterializationsSection nodes={[]} loading={false} />
|
|
41
|
+
</MemoryRouter>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
screen.getByText('No materializations configured.'),
|
|
46
|
+
).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('Materialize a node')).toBeInTheDocument();
|
|
48
|
+
expect(
|
|
49
|
+
screen.getByText('Speed up queries with cached data'),
|
|
50
|
+
).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should render pending status for nodes without validThroughTs', () => {
|
|
54
|
+
const pendingNode = createMockNode('pending_cube', null);
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<MemoryRouter>
|
|
58
|
+
<MaterializationsSection nodes={[pendingNode]} loading={false} />
|
|
59
|
+
</MemoryRouter>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText('pending_cube')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('⏳ Pending')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should render green status for recent materializations (< 24h)', () => {
|
|
67
|
+
const freshNode = createMockNode('fresh_cube', 12); // 12 hours ago
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<MemoryRouter>
|
|
71
|
+
<MaterializationsSection nodes={[freshNode]} loading={false} />
|
|
72
|
+
</MemoryRouter>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(screen.getByText('fresh_cube')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByText(/12h ago/)).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should render yellow status for stale materializations (24-72h)', () => {
|
|
80
|
+
const staleNode = createMockNode('stale_cube', 48); // 48 hours ago
|
|
81
|
+
|
|
82
|
+
render(
|
|
83
|
+
<MemoryRouter>
|
|
84
|
+
<MaterializationsSection nodes={[staleNode]} loading={false} />
|
|
85
|
+
</MemoryRouter>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByText('stale_cube')).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText(/2d ago/)).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should render red status for very stale materializations (> 72h)', () => {
|
|
93
|
+
const veryStaleNode = createMockNode('very_stale_cube', 168); // 7 days ago
|
|
94
|
+
|
|
95
|
+
render(
|
|
96
|
+
<MemoryRouter>
|
|
97
|
+
<MaterializationsSection nodes={[veryStaleNode]} loading={false} />
|
|
98
|
+
</MemoryRouter>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText('very_stale_cube')).toBeInTheDocument();
|
|
102
|
+
expect(screen.getByText(/7d ago/)).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should sort nodes by validThroughTs (most recent first)', () => {
|
|
106
|
+
const nodes = [
|
|
107
|
+
createMockNode('old_cube', 72),
|
|
108
|
+
createMockNode('recent_cube', 12),
|
|
109
|
+
createMockNode('middle_cube', 36),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<MemoryRouter>
|
|
114
|
+
<MaterializationsSection nodes={nodes} loading={false} />
|
|
115
|
+
</MemoryRouter>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const allCubes = screen.getAllByText(/cube/);
|
|
119
|
+
// Should be sorted: recent, middle, old
|
|
120
|
+
expect(allCubes[0]).toHaveTextContent('recent_cube');
|
|
121
|
+
expect(allCubes[1]).toHaveTextContent('middle_cube');
|
|
122
|
+
expect(allCubes[2]).toHaveTextContent('old_cube');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should display materialization schedule', () => {
|
|
126
|
+
const node = createMockNode('scheduled_cube', 12, '@hourly');
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<MemoryRouter>
|
|
130
|
+
<MaterializationsSection nodes={[node]} loading={false} />
|
|
131
|
+
</MemoryRouter>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(screen.getByText('🕐 @hourly')).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should display materialization table', () => {
|
|
138
|
+
const node = createMockNode('cube_with_table', 12);
|
|
139
|
+
|
|
140
|
+
render(
|
|
141
|
+
<MemoryRouter>
|
|
142
|
+
<MaterializationsSection nodes={[node]} loading={false} />
|
|
143
|
+
</MemoryRouter>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
screen.getByText('→ warehouse.materialized_table'),
|
|
148
|
+
).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle nodes with no schedule', () => {
|
|
152
|
+
const nodeWithoutSchedule = {
|
|
153
|
+
name: 'no_schedule_cube',
|
|
154
|
+
type: 'CUBE',
|
|
155
|
+
current: {
|
|
156
|
+
displayName: 'no_schedule_cube',
|
|
157
|
+
availability: {
|
|
158
|
+
validThroughTs: now - 12 * 60 * 60 * 1000,
|
|
159
|
+
},
|
|
160
|
+
materializations: [{ name: 'mat1' }], // No schedule
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
render(
|
|
165
|
+
<MemoryRouter>
|
|
166
|
+
<MaterializationsSection
|
|
167
|
+
nodes={[nodeWithoutSchedule]}
|
|
168
|
+
loading={false}
|
|
169
|
+
/>
|
|
170
|
+
</MemoryRouter>,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(screen.getByText('🕐 No schedule')).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should limit display to 5 nodes', () => {
|
|
177
|
+
const manyNodes = Array.from({ length: 10 }, (_, i) =>
|
|
178
|
+
createMockNode(`cube_${i}`, 12),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
render(
|
|
182
|
+
<MemoryRouter>
|
|
183
|
+
<MaterializationsSection nodes={manyNodes} loading={false} />
|
|
184
|
+
</MemoryRouter>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(screen.getByText('cube_0')).toBeInTheDocument();
|
|
188
|
+
expect(screen.getByText('cube_4')).toBeInTheDocument();
|
|
189
|
+
expect(screen.queryByText('cube_5')).not.toBeInTheDocument();
|
|
190
|
+
expect(screen.getByText('+5 more')).toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle undefined nodes gracefully', () => {
|
|
194
|
+
render(
|
|
195
|
+
<MemoryRouter>
|
|
196
|
+
<MaterializationsSection nodes={undefined} loading={false} />
|
|
197
|
+
</MemoryRouter>,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
screen.getByText('No materializations configured.'),
|
|
202
|
+
).toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle nodes without availability', () => {
|
|
206
|
+
const nodeWithoutAvailability = {
|
|
207
|
+
name: 'no_availability_cube',
|
|
208
|
+
type: 'CUBE',
|
|
209
|
+
current: {
|
|
210
|
+
displayName: 'no_availability_cube',
|
|
211
|
+
materializations: [{ name: 'mat1', schedule: '@daily' }],
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
render(
|
|
216
|
+
<MemoryRouter>
|
|
217
|
+
<MaterializationsSection
|
|
218
|
+
nodes={[nodeWithoutAvailability]}
|
|
219
|
+
loading={false}
|
|
220
|
+
/>
|
|
221
|
+
</MemoryRouter>,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(screen.getByText('no_availability_cube')).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByText('⏳ Pending')).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should link to nodes with materialization filter', () => {
|
|
229
|
+
render(
|
|
230
|
+
<MemoryRouter>
|
|
231
|
+
<MaterializationsSection nodes={[]} loading={false} />
|
|
232
|
+
</MemoryRouter>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const viewAllLink = screen.getByText('View All →').closest('a');
|
|
236
|
+
expect(viewAllLink).toHaveAttribute('href', '/?hasMaterialization=true');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { MyNodesSection } from '../MyNodesSection';
|
|
5
|
+
|
|
6
|
+
jest.mock('../MyWorkspacePage.css', () => ({}));
|
|
7
|
+
jest.mock('../NodeList', () => ({
|
|
8
|
+
NodeList: ({ nodes, showUpdatedAt }) => (
|
|
9
|
+
<div data-testid="node-list">
|
|
10
|
+
{nodes.map(node => (
|
|
11
|
+
<div key={node.name}>{node.name}</div>
|
|
12
|
+
))}
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
}));
|
|
16
|
+
jest.mock('../TypeGroupGrid', () => ({
|
|
17
|
+
TypeGroupGrid: ({ groupedData }) => (
|
|
18
|
+
<div data-testid="type-group-grid">
|
|
19
|
+
{groupedData.map(group => (
|
|
20
|
+
<div key={group.type}>
|
|
21
|
+
{group.type}: {group.count} nodes
|
|
22
|
+
</div>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe('<MyNodesSection />', () => {
|
|
29
|
+
const mockOwnedNodes = [
|
|
30
|
+
{
|
|
31
|
+
name: 'default.owned_metric',
|
|
32
|
+
type: 'METRIC',
|
|
33
|
+
current: { displayName: 'Owned Metric', updatedAt: '2024-01-01' },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const mockWatchedNodes = [
|
|
38
|
+
{
|
|
39
|
+
name: 'default.watched_metric',
|
|
40
|
+
type: 'METRIC',
|
|
41
|
+
current: { displayName: 'Watched Metric', updatedAt: '2024-01-02' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'default.owned_metric', // Also owned, should be filtered out
|
|
45
|
+
type: 'METRIC',
|
|
46
|
+
current: { displayName: 'Owned Metric', updatedAt: '2024-01-01' },
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const mockRecentlyEdited = [
|
|
51
|
+
{
|
|
52
|
+
name: 'default.edited_metric',
|
|
53
|
+
type: 'METRIC',
|
|
54
|
+
current: { displayName: 'Edited Metric', updatedAt: '2024-01-03' },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
it('should render loading state', () => {
|
|
59
|
+
render(
|
|
60
|
+
<MemoryRouter>
|
|
61
|
+
<MyNodesSection
|
|
62
|
+
ownedNodes={[]}
|
|
63
|
+
watchedNodes={[]}
|
|
64
|
+
recentlyEdited={[]}
|
|
65
|
+
username="test.user@example.com"
|
|
66
|
+
loading={true}
|
|
67
|
+
/>
|
|
68
|
+
</MemoryRouter>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.queryByText('Owned (0)')).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should render empty state when no nodes', () => {
|
|
75
|
+
render(
|
|
76
|
+
<MemoryRouter>
|
|
77
|
+
<MyNodesSection
|
|
78
|
+
ownedNodes={[]}
|
|
79
|
+
watchedNodes={[]}
|
|
80
|
+
recentlyEdited={[]}
|
|
81
|
+
username="test.user@example.com"
|
|
82
|
+
loading={false}
|
|
83
|
+
/>
|
|
84
|
+
</MemoryRouter>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByText('No nodes yet.')).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText('Create a node')).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText('Claim ownership')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should render tabs with node counts', () => {
|
|
93
|
+
render(
|
|
94
|
+
<MemoryRouter>
|
|
95
|
+
<MyNodesSection
|
|
96
|
+
ownedNodes={mockOwnedNodes}
|
|
97
|
+
watchedNodes={mockWatchedNodes}
|
|
98
|
+
recentlyEdited={mockRecentlyEdited}
|
|
99
|
+
username="test.user@example.com"
|
|
100
|
+
loading={false}
|
|
101
|
+
/>
|
|
102
|
+
</MemoryRouter>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(screen.getByText('Owned (1)')).toBeInTheDocument();
|
|
106
|
+
// Should only count watched nodes that aren't owned (1 watched-only)
|
|
107
|
+
expect(screen.getByText('Watched (1)')).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByText('Recent Edits (1)')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should show owned nodes by default', () => {
|
|
112
|
+
render(
|
|
113
|
+
<MemoryRouter>
|
|
114
|
+
<MyNodesSection
|
|
115
|
+
ownedNodes={mockOwnedNodes}
|
|
116
|
+
watchedNodes={mockWatchedNodes}
|
|
117
|
+
recentlyEdited={mockRecentlyEdited}
|
|
118
|
+
username="test.user@example.com"
|
|
119
|
+
loading={false}
|
|
120
|
+
/>
|
|
121
|
+
</MemoryRouter>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(screen.getByText('default.owned_metric')).toBeInTheDocument();
|
|
125
|
+
expect(
|
|
126
|
+
screen.queryByText('default.watched_metric'),
|
|
127
|
+
).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should switch to watched tab', () => {
|
|
131
|
+
render(
|
|
132
|
+
<MemoryRouter>
|
|
133
|
+
<MyNodesSection
|
|
134
|
+
ownedNodes={mockOwnedNodes}
|
|
135
|
+
watchedNodes={mockWatchedNodes}
|
|
136
|
+
recentlyEdited={mockRecentlyEdited}
|
|
137
|
+
username="test.user@example.com"
|
|
138
|
+
loading={false}
|
|
139
|
+
/>
|
|
140
|
+
</MemoryRouter>,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
fireEvent.click(screen.getByText('Watched (1)'));
|
|
144
|
+
|
|
145
|
+
// Should show watched-only nodes (not owned)
|
|
146
|
+
expect(screen.getByText('default.watched_metric')).toBeInTheDocument();
|
|
147
|
+
expect(screen.queryByText('default.owned_metric')).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should switch to recent edits tab', () => {
|
|
151
|
+
render(
|
|
152
|
+
<MemoryRouter>
|
|
153
|
+
<MyNodesSection
|
|
154
|
+
ownedNodes={mockOwnedNodes}
|
|
155
|
+
watchedNodes={mockWatchedNodes}
|
|
156
|
+
recentlyEdited={mockRecentlyEdited}
|
|
157
|
+
username="test.user@example.com"
|
|
158
|
+
loading={false}
|
|
159
|
+
/>
|
|
160
|
+
</MemoryRouter>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
fireEvent.click(screen.getByText('Recent Edits (1)'));
|
|
164
|
+
|
|
165
|
+
expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should show empty state for active tab with no nodes', () => {
|
|
169
|
+
render(
|
|
170
|
+
<MemoryRouter>
|
|
171
|
+
<MyNodesSection
|
|
172
|
+
ownedNodes={mockOwnedNodes}
|
|
173
|
+
watchedNodes={[]}
|
|
174
|
+
recentlyEdited={[]}
|
|
175
|
+
username="test.user@example.com"
|
|
176
|
+
loading={false}
|
|
177
|
+
/>
|
|
178
|
+
</MemoryRouter>,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
fireEvent.click(screen.getByText('Watched (0)'));
|
|
182
|
+
|
|
183
|
+
expect(screen.getByText('No watched nodes')).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should limit displayed nodes to 8', () => {
|
|
187
|
+
const manyNodes = Array.from({ length: 15 }, (_, i) => ({
|
|
188
|
+
name: `default.metric_${i}`,
|
|
189
|
+
type: 'METRIC',
|
|
190
|
+
current: { displayName: `Metric ${i}`, updatedAt: '2024-01-01' },
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
render(
|
|
194
|
+
<MemoryRouter>
|
|
195
|
+
<MyNodesSection
|
|
196
|
+
ownedNodes={manyNodes}
|
|
197
|
+
watchedNodes={[]}
|
|
198
|
+
recentlyEdited={[]}
|
|
199
|
+
username="test.user@example.com"
|
|
200
|
+
loading={false}
|
|
201
|
+
/>
|
|
202
|
+
</MemoryRouter>,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Should show "+7 more" text
|
|
206
|
+
expect(screen.getByText('+7 more')).toBeInTheDocument();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should link to browse page with username filter', () => {
|
|
210
|
+
render(
|
|
211
|
+
<MemoryRouter>
|
|
212
|
+
<MyNodesSection
|
|
213
|
+
ownedNodes={mockOwnedNodes}
|
|
214
|
+
watchedNodes={[]}
|
|
215
|
+
recentlyEdited={[]}
|
|
216
|
+
username="test.user@example.com"
|
|
217
|
+
loading={false}
|
|
218
|
+
/>
|
|
219
|
+
</MemoryRouter>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// The "View All →" link should have the username filter
|
|
223
|
+
const viewAllLink = screen.getByText('View All →').closest('a');
|
|
224
|
+
expect(viewAllLink).toHaveAttribute(
|
|
225
|
+
'href',
|
|
226
|
+
'/?ownedBy=test.user@example.com',
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should filter watched nodes to exclude owned nodes', () => {
|
|
231
|
+
const ownedNodes = [
|
|
232
|
+
{ name: 'node1', type: 'METRIC', current: {} },
|
|
233
|
+
{ name: 'node2', type: 'METRIC', current: {} },
|
|
234
|
+
];
|
|
235
|
+
const watchedNodes = [
|
|
236
|
+
{ name: 'node1', type: 'METRIC', current: {} }, // Also owned
|
|
237
|
+
{ name: 'node3', type: 'METRIC', current: {} }, // Watched only
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
render(
|
|
241
|
+
<MemoryRouter>
|
|
242
|
+
<MyNodesSection
|
|
243
|
+
ownedNodes={ownedNodes}
|
|
244
|
+
watchedNodes={watchedNodes}
|
|
245
|
+
recentlyEdited={[]}
|
|
246
|
+
username="test.user@example.com"
|
|
247
|
+
loading={false}
|
|
248
|
+
/>
|
|
249
|
+
</MemoryRouter>,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Watched tab should only show 1 (watched-only nodes)
|
|
253
|
+
expect(screen.getByText('Watched (1)')).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Group by Type feature', () => {
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
localStorage.clear();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should show group by type toggle when nodes exist', () => {
|
|
262
|
+
render(
|
|
263
|
+
<MemoryRouter>
|
|
264
|
+
<MyNodesSection
|
|
265
|
+
ownedNodes={mockOwnedNodes}
|
|
266
|
+
watchedNodes={[]}
|
|
267
|
+
recentlyEdited={[]}
|
|
268
|
+
username="test.user@example.com"
|
|
269
|
+
loading={false}
|
|
270
|
+
/>
|
|
271
|
+
</MemoryRouter>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(screen.getByLabelText('Group by Type')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should not show toggle when no nodes', () => {
|
|
278
|
+
render(
|
|
279
|
+
<MemoryRouter>
|
|
280
|
+
<MyNodesSection
|
|
281
|
+
ownedNodes={[]}
|
|
282
|
+
watchedNodes={[]}
|
|
283
|
+
recentlyEdited={[]}
|
|
284
|
+
username="test.user@example.com"
|
|
285
|
+
loading={false}
|
|
286
|
+
/>
|
|
287
|
+
</MemoryRouter>,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(screen.queryByLabelText('Group by Type')).not.toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should toggle between list and grouped view', () => {
|
|
294
|
+
render(
|
|
295
|
+
<MemoryRouter>
|
|
296
|
+
<MyNodesSection
|
|
297
|
+
ownedNodes={mockOwnedNodes}
|
|
298
|
+
watchedNodes={[]}
|
|
299
|
+
recentlyEdited={[]}
|
|
300
|
+
username="test.user@example.com"
|
|
301
|
+
loading={false}
|
|
302
|
+
/>
|
|
303
|
+
</MemoryRouter>,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// Initially should show list view
|
|
307
|
+
expect(screen.getByTestId('node-list')).toBeInTheDocument();
|
|
308
|
+
expect(screen.queryByTestId('type-group-grid')).not.toBeInTheDocument();
|
|
309
|
+
|
|
310
|
+
// Click toggle
|
|
311
|
+
const toggle = screen.getByLabelText('Group by Type');
|
|
312
|
+
fireEvent.click(toggle);
|
|
313
|
+
|
|
314
|
+
// Should now show grouped view
|
|
315
|
+
expect(screen.queryByTestId('node-list')).not.toBeInTheDocument();
|
|
316
|
+
expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should persist toggle state to localStorage', () => {
|
|
320
|
+
render(
|
|
321
|
+
<MemoryRouter>
|
|
322
|
+
<MyNodesSection
|
|
323
|
+
ownedNodes={mockOwnedNodes}
|
|
324
|
+
watchedNodes={[]}
|
|
325
|
+
recentlyEdited={[]}
|
|
326
|
+
username="test.user@example.com"
|
|
327
|
+
loading={false}
|
|
328
|
+
/>
|
|
329
|
+
</MemoryRouter>,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const toggle = screen.getByLabelText('Group by Type');
|
|
333
|
+
fireEvent.click(toggle);
|
|
334
|
+
|
|
335
|
+
expect(localStorage.getItem('workspace_groupByType')).toBe('true');
|
|
336
|
+
|
|
337
|
+
fireEvent.click(toggle);
|
|
338
|
+
expect(localStorage.getItem('workspace_groupByType')).toBe('false');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should load toggle state from localStorage', () => {
|
|
342
|
+
localStorage.setItem('workspace_groupByType', 'true');
|
|
343
|
+
|
|
344
|
+
render(
|
|
345
|
+
<MemoryRouter>
|
|
346
|
+
<MyNodesSection
|
|
347
|
+
ownedNodes={mockOwnedNodes}
|
|
348
|
+
watchedNodes={[]}
|
|
349
|
+
recentlyEdited={[]}
|
|
350
|
+
username="test.user@example.com"
|
|
351
|
+
loading={false}
|
|
352
|
+
/>
|
|
353
|
+
</MemoryRouter>,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Should show grouped view on mount
|
|
357
|
+
expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
|
|
358
|
+
expect(screen.getByLabelText('Group by Type')).toBeChecked();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should group nodes by type correctly', () => {
|
|
362
|
+
const mixedNodes = [
|
|
363
|
+
{ name: 'metric1', type: 'metric', current: {} },
|
|
364
|
+
{ name: 'metric2', type: 'metric', current: {} },
|
|
365
|
+
{ name: 'dim1', type: 'dimension', current: {} },
|
|
366
|
+
{ name: 'source1', type: 'source', current: {} },
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
render(
|
|
370
|
+
<MemoryRouter>
|
|
371
|
+
<MyNodesSection
|
|
372
|
+
ownedNodes={mixedNodes}
|
|
373
|
+
watchedNodes={[]}
|
|
374
|
+
recentlyEdited={[]}
|
|
375
|
+
username="test.user@example.com"
|
|
376
|
+
loading={false}
|
|
377
|
+
/>
|
|
378
|
+
</MemoryRouter>,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const toggle = screen.getByLabelText('Group by Type');
|
|
382
|
+
fireEvent.click(toggle);
|
|
383
|
+
|
|
384
|
+
expect(screen.getByText('metric: 2 nodes')).toBeInTheDocument();
|
|
385
|
+
expect(screen.getByText('dimension: 1 nodes')).toBeInTheDocument();
|
|
386
|
+
expect(screen.getByText('source: 1 nodes')).toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|