datajunction-ui 0.0.18 → 0.0.20
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/NotificationBell.tsx +223 -0
- package/src/app/components/UserMenu.tsx +100 -0
- package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
- package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
- package/src/app/icons/NotificationIcon.jsx +27 -0
- package/src/app/icons/SettingsIcon.jsx +28 -0
- package/src/app/index.tsx +12 -0
- package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
- package/src/app/pages/NotificationsPage/index.jsx +136 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +18 -53
- package/src/app/pages/Root/index.tsx +23 -19
- package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
- package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
- package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
- package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
- package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
- package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
- package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
- package/src/app/pages/SettingsPage/index.jsx +148 -0
- package/src/app/services/DJService.js +81 -0
- package/src/app/utils/__tests__/date.test.js +198 -0
- package/src/app/utils/date.js +65 -0
- package/src/styles/index.css +1 -1
- package/src/styles/nav-bar.css +274 -0
- package/src/styles/settings.css +787 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import CreateServiceAccountModal from './CreateServiceAccountModal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Displays and manages service accounts.
|
|
6
|
+
*/
|
|
7
|
+
export function ServiceAccountsSection({ accounts, onCreate, onDelete }) {
|
|
8
|
+
const [showModal, setShowModal] = useState(false);
|
|
9
|
+
|
|
10
|
+
const handleDelete = async account => {
|
|
11
|
+
const confirmed = window.confirm(
|
|
12
|
+
`Delete service account "${account.name}"?\n\nThis will revoke all access for this account and cannot be undone.`,
|
|
13
|
+
);
|
|
14
|
+
if (!confirmed) return;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await onDelete(account.client_id);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error deleting service account:', error);
|
|
20
|
+
alert('Failed to delete service account');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleCreate = async name => {
|
|
25
|
+
const result = await onCreate(name);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section className="settings-section" id="service-accounts">
|
|
31
|
+
<div className="section-title-row">
|
|
32
|
+
<h2 className="settings-section-title">Service Accounts</h2>
|
|
33
|
+
<button className="btn-create" onClick={() => setShowModal(true)}>
|
|
34
|
+
+ Create
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="settings-card">
|
|
38
|
+
<p className="section-description">
|
|
39
|
+
Service accounts allow programmatic access to the DJ API. Create
|
|
40
|
+
accounts for your applications, scripts, or CI/CD pipelines.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
{accounts.length > 0 ? (
|
|
44
|
+
<div className="service-accounts-list">
|
|
45
|
+
<table className="service-accounts-table">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Name</th>
|
|
49
|
+
<th>Client ID</th>
|
|
50
|
+
<th>Created</th>
|
|
51
|
+
<th></th>
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
{accounts.map(account => (
|
|
56
|
+
<tr key={account.id}>
|
|
57
|
+
<td>{account.name}</td>
|
|
58
|
+
<td>
|
|
59
|
+
<code className="client-id">{account.client_id}</code>
|
|
60
|
+
</td>
|
|
61
|
+
<td className="created-date">
|
|
62
|
+
{new Date(account.created_at).toLocaleDateString()}
|
|
63
|
+
</td>
|
|
64
|
+
<td className="actions-cell">
|
|
65
|
+
<button
|
|
66
|
+
className="btn-icon btn-delete-account"
|
|
67
|
+
onClick={() => handleDelete(account)}
|
|
68
|
+
title="Delete service account"
|
|
69
|
+
>
|
|
70
|
+
×
|
|
71
|
+
</button>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
))}
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<p className="empty-state">
|
|
80
|
+
No service accounts yet. Create one to enable programmatic API
|
|
81
|
+
access.
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<CreateServiceAccountModal
|
|
87
|
+
isOpen={showModal}
|
|
88
|
+
onClose={() => setShowModal(false)}
|
|
89
|
+
onCreate={handleCreate}
|
|
90
|
+
/>
|
|
91
|
+
</section>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default ServiceAccountsSection;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { CreateServiceAccountModal } from '../CreateServiceAccountModal';
|
|
4
|
+
|
|
5
|
+
describe('CreateServiceAccountModal', () => {
|
|
6
|
+
const mockOnClose = jest.fn();
|
|
7
|
+
const mockOnCreate = jest.fn();
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does not render when isOpen is false', () => {
|
|
14
|
+
render(
|
|
15
|
+
<CreateServiceAccountModal
|
|
16
|
+
isOpen={false}
|
|
17
|
+
onClose={mockOnClose}
|
|
18
|
+
onCreate={mockOnCreate}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
screen.queryByText('Create Service Account'),
|
|
24
|
+
).not.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders modal when isOpen is true', () => {
|
|
28
|
+
render(
|
|
29
|
+
<CreateServiceAccountModal
|
|
30
|
+
isOpen={true}
|
|
31
|
+
onClose={mockOnClose}
|
|
32
|
+
onCreate={mockOnCreate}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('Create Service Account')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByLabelText('Name')).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText('Create')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('calls onClose when close button is clicked', () => {
|
|
43
|
+
render(
|
|
44
|
+
<CreateServiceAccountModal
|
|
45
|
+
isOpen={true}
|
|
46
|
+
onClose={mockOnClose}
|
|
47
|
+
onCreate={mockOnCreate}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
fireEvent.click(screen.getByTitle('Close'));
|
|
52
|
+
|
|
53
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calls onClose when Cancel button is clicked', () => {
|
|
57
|
+
render(
|
|
58
|
+
<CreateServiceAccountModal
|
|
59
|
+
isOpen={true}
|
|
60
|
+
onClose={mockOnClose}
|
|
61
|
+
onCreate={mockOnCreate}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
66
|
+
|
|
67
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('calls onClose when overlay is clicked', () => {
|
|
71
|
+
render(
|
|
72
|
+
<CreateServiceAccountModal
|
|
73
|
+
isOpen={true}
|
|
74
|
+
onClose={mockOnClose}
|
|
75
|
+
onCreate={mockOnCreate}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Click on overlay (modal-overlay class)
|
|
80
|
+
const overlay = document.querySelector('.modal-overlay');
|
|
81
|
+
fireEvent.click(overlay);
|
|
82
|
+
|
|
83
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not close when modal content is clicked', () => {
|
|
87
|
+
render(
|
|
88
|
+
<CreateServiceAccountModal
|
|
89
|
+
isOpen={true}
|
|
90
|
+
onClose={mockOnClose}
|
|
91
|
+
onCreate={mockOnCreate}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const content = document.querySelector('.modal-content');
|
|
96
|
+
fireEvent.click(content);
|
|
97
|
+
|
|
98
|
+
expect(mockOnClose).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('disables Create button when name is empty', () => {
|
|
102
|
+
render(
|
|
103
|
+
<CreateServiceAccountModal
|
|
104
|
+
isOpen={true}
|
|
105
|
+
onClose={mockOnClose}
|
|
106
|
+
onCreate={mockOnCreate}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const createButton = screen.getByText('Create');
|
|
111
|
+
expect(createButton).toBeDisabled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('enables Create button when name is entered', () => {
|
|
115
|
+
render(
|
|
116
|
+
<CreateServiceAccountModal
|
|
117
|
+
isOpen={true}
|
|
118
|
+
onClose={mockOnClose}
|
|
119
|
+
onCreate={mockOnCreate}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const input = screen.getByLabelText('Name');
|
|
124
|
+
fireEvent.change(input, { target: { value: 'my-new-account' } });
|
|
125
|
+
|
|
126
|
+
const createButton = screen.getByText('Create');
|
|
127
|
+
expect(createButton).not.toBeDisabled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('calls onCreate with trimmed name on submit', async () => {
|
|
131
|
+
mockOnCreate.mockResolvedValue({ client_id: 'test-id' });
|
|
132
|
+
|
|
133
|
+
render(
|
|
134
|
+
<CreateServiceAccountModal
|
|
135
|
+
isOpen={true}
|
|
136
|
+
onClose={mockOnClose}
|
|
137
|
+
onCreate={mockOnCreate}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const input = screen.getByLabelText('Name');
|
|
142
|
+
fireEvent.change(input, { target: { value: ' my-account ' } });
|
|
143
|
+
fireEvent.click(screen.getByText('Create'));
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(mockOnCreate).toHaveBeenCalledWith('my-account');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('shows credentials after successful creation', async () => {
|
|
151
|
+
const credentials = {
|
|
152
|
+
name: 'my-account',
|
|
153
|
+
client_id: 'abc-123',
|
|
154
|
+
client_secret: 'secret-xyz',
|
|
155
|
+
};
|
|
156
|
+
mockOnCreate.mockResolvedValue(credentials);
|
|
157
|
+
|
|
158
|
+
render(
|
|
159
|
+
<CreateServiceAccountModal
|
|
160
|
+
isOpen={true}
|
|
161
|
+
onClose={mockOnClose}
|
|
162
|
+
onCreate={mockOnCreate}
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const input = screen.getByLabelText('Name');
|
|
167
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
168
|
+
fireEvent.click(screen.getByText('Create'));
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(screen.getByText('Service Account Created!')).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(screen.getByText('abc-123')).toBeInTheDocument();
|
|
175
|
+
expect(screen.getByText('secret-xyz')).toBeInTheDocument();
|
|
176
|
+
expect(
|
|
177
|
+
screen.getByText(/client secret will not be shown again/i),
|
|
178
|
+
).toBeInTheDocument();
|
|
179
|
+
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('shows alert when creation returns error message', async () => {
|
|
183
|
+
window.alert = jest.fn();
|
|
184
|
+
mockOnCreate.mockResolvedValue({ message: 'Account already exists' });
|
|
185
|
+
|
|
186
|
+
render(
|
|
187
|
+
<CreateServiceAccountModal
|
|
188
|
+
isOpen={true}
|
|
189
|
+
onClose={mockOnClose}
|
|
190
|
+
onCreate={mockOnCreate}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const input = screen.getByLabelText('Name');
|
|
195
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
196
|
+
fireEvent.click(screen.getByText('Create'));
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(window.alert).toHaveBeenCalledWith('Account already exists');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('shows alert when creation throws error', async () => {
|
|
204
|
+
window.alert = jest.fn();
|
|
205
|
+
mockOnCreate.mockRejectedValue(new Error('Network error'));
|
|
206
|
+
|
|
207
|
+
render(
|
|
208
|
+
<CreateServiceAccountModal
|
|
209
|
+
isOpen={true}
|
|
210
|
+
onClose={mockOnClose}
|
|
211
|
+
onCreate={mockOnCreate}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const input = screen.getByLabelText('Name');
|
|
216
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
217
|
+
fireEvent.click(screen.getByText('Create'));
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(window.alert).toHaveBeenCalledWith(
|
|
221
|
+
'Failed to create service account',
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('shows Creating... while request is in progress', async () => {
|
|
227
|
+
let resolveCreate;
|
|
228
|
+
mockOnCreate.mockImplementation(
|
|
229
|
+
() => new Promise(resolve => (resolveCreate = resolve)),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
render(
|
|
233
|
+
<CreateServiceAccountModal
|
|
234
|
+
isOpen={true}
|
|
235
|
+
onClose={mockOnClose}
|
|
236
|
+
onCreate={mockOnCreate}
|
|
237
|
+
/>,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const input = screen.getByLabelText('Name');
|
|
241
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
242
|
+
fireEvent.click(screen.getByText('Create'));
|
|
243
|
+
|
|
244
|
+
expect(screen.getByText('Creating...')).toBeInTheDocument();
|
|
245
|
+
|
|
246
|
+
// Resolve the promise
|
|
247
|
+
resolveCreate({ client_id: 'test' });
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(screen.queryByText('Creating...')).not.toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('closes and resets state when Done is clicked after creation', async () => {
|
|
255
|
+
const credentials = {
|
|
256
|
+
name: 'my-account',
|
|
257
|
+
client_id: 'abc-123',
|
|
258
|
+
client_secret: 'secret-xyz',
|
|
259
|
+
};
|
|
260
|
+
mockOnCreate.mockResolvedValue(credentials);
|
|
261
|
+
|
|
262
|
+
render(
|
|
263
|
+
<CreateServiceAccountModal
|
|
264
|
+
isOpen={true}
|
|
265
|
+
onClose={mockOnClose}
|
|
266
|
+
onCreate={mockOnCreate}
|
|
267
|
+
/>,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const input = screen.getByLabelText('Name');
|
|
271
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
272
|
+
fireEvent.click(screen.getByText('Create'));
|
|
273
|
+
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
fireEvent.click(screen.getByText('Done'));
|
|
279
|
+
|
|
280
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('copies client ID to clipboard when copy button is clicked', async () => {
|
|
284
|
+
const credentials = {
|
|
285
|
+
name: 'my-account',
|
|
286
|
+
client_id: 'abc-123',
|
|
287
|
+
client_secret: 'secret-xyz',
|
|
288
|
+
};
|
|
289
|
+
mockOnCreate.mockResolvedValue(credentials);
|
|
290
|
+
|
|
291
|
+
Object.assign(navigator, {
|
|
292
|
+
clipboard: {
|
|
293
|
+
writeText: jest.fn().mockResolvedValue(),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
render(
|
|
298
|
+
<CreateServiceAccountModal
|
|
299
|
+
isOpen={true}
|
|
300
|
+
onClose={mockOnClose}
|
|
301
|
+
onCreate={mockOnCreate}
|
|
302
|
+
/>,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const input = screen.getByLabelText('Name');
|
|
306
|
+
fireEvent.change(input, { target: { value: 'my-account' } });
|
|
307
|
+
fireEvent.click(screen.getByText('Create'));
|
|
308
|
+
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
expect(screen.getByText('abc-123')).toBeInTheDocument();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const copyButtons = screen.getAllByTitle('Copy');
|
|
314
|
+
fireEvent.click(copyButtons[0]);
|
|
315
|
+
|
|
316
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('abc-123');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { NotificationSubscriptionsSection } from '../NotificationSubscriptionsSection';
|
|
4
|
+
|
|
5
|
+
describe('NotificationSubscriptionsSection', () => {
|
|
6
|
+
const mockOnUpdate = jest.fn();
|
|
7
|
+
const mockOnUnsubscribe = jest.fn();
|
|
8
|
+
|
|
9
|
+
const mockSubscriptions = [
|
|
10
|
+
{
|
|
11
|
+
entity_name: 'default.orders_count',
|
|
12
|
+
entity_type: 'node',
|
|
13
|
+
node_type: 'metric',
|
|
14
|
+
activity_types: ['update', 'delete'],
|
|
15
|
+
status: 'valid',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
entity_name: 'default.dim_customers',
|
|
19
|
+
entity_type: 'node',
|
|
20
|
+
node_type: 'dimension',
|
|
21
|
+
activity_types: ['create'],
|
|
22
|
+
status: 'invalid',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders empty state when no subscriptions', () => {
|
|
31
|
+
render(
|
|
32
|
+
<NotificationSubscriptionsSection
|
|
33
|
+
subscriptions={[]}
|
|
34
|
+
onUpdate={mockOnUpdate}
|
|
35
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText(/not watching any nodes/i)).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders subscriptions list', () => {
|
|
43
|
+
render(
|
|
44
|
+
<NotificationSubscriptionsSection
|
|
45
|
+
subscriptions={mockSubscriptions}
|
|
46
|
+
onUpdate={mockOnUpdate}
|
|
47
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByText('default.orders_count')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('default.dim_customers')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText('METRIC')).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByText('DIMENSION')).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('shows invalid badge for invalid status', () => {
|
|
58
|
+
render(
|
|
59
|
+
<NotificationSubscriptionsSection
|
|
60
|
+
subscriptions={mockSubscriptions}
|
|
61
|
+
onUpdate={mockOnUpdate}
|
|
62
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(screen.getByText('INVALID')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('displays activity types as badges', () => {
|
|
70
|
+
render(
|
|
71
|
+
<NotificationSubscriptionsSection
|
|
72
|
+
subscriptions={mockSubscriptions}
|
|
73
|
+
onUpdate={mockOnUpdate}
|
|
74
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(screen.getByText('update')).toBeInTheDocument();
|
|
79
|
+
expect(screen.getByText('delete')).toBeInTheDocument();
|
|
80
|
+
expect(screen.getByText('create')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('enters edit mode when edit button is clicked', () => {
|
|
84
|
+
render(
|
|
85
|
+
<NotificationSubscriptionsSection
|
|
86
|
+
subscriptions={mockSubscriptions}
|
|
87
|
+
onUpdate={mockOnUpdate}
|
|
88
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const editButtons = screen.getAllByTitle('Edit subscription');
|
|
93
|
+
fireEvent.click(editButtons[0]);
|
|
94
|
+
|
|
95
|
+
expect(screen.getByText('Save')).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByLabelText('Update')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('cancels editing when cancel button is clicked', () => {
|
|
101
|
+
render(
|
|
102
|
+
<NotificationSubscriptionsSection
|
|
103
|
+
subscriptions={mockSubscriptions}
|
|
104
|
+
onUpdate={mockOnUpdate}
|
|
105
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const editButtons = screen.getAllByTitle('Edit subscription');
|
|
110
|
+
fireEvent.click(editButtons[0]);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
113
|
+
|
|
114
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
115
|
+
|
|
116
|
+
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('calls onUnsubscribe when unsubscribe is confirmed', async () => {
|
|
120
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
121
|
+
mockOnUnsubscribe.mockResolvedValue();
|
|
122
|
+
|
|
123
|
+
render(
|
|
124
|
+
<NotificationSubscriptionsSection
|
|
125
|
+
subscriptions={mockSubscriptions}
|
|
126
|
+
onUpdate={mockOnUpdate}
|
|
127
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
|
|
132
|
+
fireEvent.click(unsubscribeButtons[0]);
|
|
133
|
+
|
|
134
|
+
expect(window.confirm).toHaveBeenCalledWith(
|
|
135
|
+
'Unsubscribe from notifications for "default.orders_count"?',
|
|
136
|
+
);
|
|
137
|
+
expect(mockOnUnsubscribe).toHaveBeenCalledWith(mockSubscriptions[0]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('does not call onUnsubscribe when unsubscribe is cancelled', () => {
|
|
141
|
+
window.confirm = jest.fn().mockReturnValue(false);
|
|
142
|
+
|
|
143
|
+
render(
|
|
144
|
+
<NotificationSubscriptionsSection
|
|
145
|
+
subscriptions={mockSubscriptions}
|
|
146
|
+
onUpdate={mockOnUpdate}
|
|
147
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
|
|
152
|
+
fireEvent.click(unsubscribeButtons[0]);
|
|
153
|
+
|
|
154
|
+
expect(mockOnUnsubscribe).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('shows alert when trying to save with no activity types', () => {
|
|
158
|
+
window.alert = jest.fn();
|
|
159
|
+
|
|
160
|
+
render(
|
|
161
|
+
<NotificationSubscriptionsSection
|
|
162
|
+
subscriptions={mockSubscriptions}
|
|
163
|
+
onUpdate={mockOnUpdate}
|
|
164
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const editButtons = screen.getAllByTitle('Edit subscription');
|
|
169
|
+
fireEvent.click(editButtons[0]);
|
|
170
|
+
|
|
171
|
+
// Uncheck all activity types
|
|
172
|
+
const updateCheckbox = screen.getByLabelText('Update');
|
|
173
|
+
const deleteCheckbox = screen.getByLabelText('Delete');
|
|
174
|
+
fireEvent.click(updateCheckbox);
|
|
175
|
+
fireEvent.click(deleteCheckbox);
|
|
176
|
+
|
|
177
|
+
fireEvent.click(screen.getByText('Save'));
|
|
178
|
+
|
|
179
|
+
expect(window.alert).toHaveBeenCalledWith(
|
|
180
|
+
'Please select at least one activity type',
|
|
181
|
+
);
|
|
182
|
+
expect(mockOnUpdate).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('calls onUpdate with new activity types when saved', async () => {
|
|
186
|
+
mockOnUpdate.mockResolvedValue();
|
|
187
|
+
|
|
188
|
+
render(
|
|
189
|
+
<NotificationSubscriptionsSection
|
|
190
|
+
subscriptions={mockSubscriptions}
|
|
191
|
+
onUpdate={mockOnUpdate}
|
|
192
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
193
|
+
/>,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const editButtons = screen.getAllByTitle('Edit subscription');
|
|
197
|
+
fireEvent.click(editButtons[0]);
|
|
198
|
+
|
|
199
|
+
// Add 'create' activity type
|
|
200
|
+
const createCheckbox = screen.getByLabelText('Create');
|
|
201
|
+
fireEvent.click(createCheckbox);
|
|
202
|
+
|
|
203
|
+
fireEvent.click(screen.getByText('Save'));
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(mockOnUpdate).toHaveBeenCalledWith(mockSubscriptions[0], [
|
|
207
|
+
'update',
|
|
208
|
+
'delete',
|
|
209
|
+
'create',
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('renders entity type badge when node_type is not available', () => {
|
|
215
|
+
const subscriptionsWithoutNodeType = [
|
|
216
|
+
{
|
|
217
|
+
entity_name: 'some_entity',
|
|
218
|
+
entity_type: 'namespace',
|
|
219
|
+
activity_types: ['create'],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
render(
|
|
224
|
+
<NotificationSubscriptionsSection
|
|
225
|
+
subscriptions={subscriptionsWithoutNodeType}
|
|
226
|
+
onUpdate={mockOnUpdate}
|
|
227
|
+
onUnsubscribe={mockOnUnsubscribe}
|
|
228
|
+
/>,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText('namespace')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ProfileSection } from '../ProfileSection';
|
|
4
|
+
|
|
5
|
+
describe('ProfileSection', () => {
|
|
6
|
+
it('renders user initials from name', () => {
|
|
7
|
+
const user = {
|
|
8
|
+
name: 'John Doe',
|
|
9
|
+
username: 'johndoe',
|
|
10
|
+
email: 'john@example.com',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
render(<ProfileSection user={user} />);
|
|
14
|
+
|
|
15
|
+
expect(screen.getByText('JD')).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText('johndoe')).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders initials from username when name is not available', () => {
|
|
21
|
+
const user = {
|
|
22
|
+
username: 'alice',
|
|
23
|
+
email: 'alice@example.com',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
render(<ProfileSection user={user} />);
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText('AL')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders fallback when no user info available', () => {
|
|
32
|
+
render(<ProfileSection user={null} />);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByText('?')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getAllByText('-')).toHaveLength(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders section title', () => {
|
|
39
|
+
render(<ProfileSection user={null} />);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('Profile')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('handles single name', () => {
|
|
45
|
+
const user = {
|
|
46
|
+
name: 'Alice',
|
|
47
|
+
username: 'alice',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
render(<ProfileSection user={user} />);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByText('A')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles multi-word name and takes only first two initials', () => {
|
|
56
|
+
const user = {
|
|
57
|
+
name: 'John Paul Smith',
|
|
58
|
+
username: 'jps',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
render(<ProfileSection user={user} />);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('JP')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
});
|