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,150 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { ServiceAccountsSection } from '../ServiceAccountsSection';
|
|
4
|
+
|
|
5
|
+
describe('ServiceAccountsSection', () => {
|
|
6
|
+
const mockOnCreate = jest.fn();
|
|
7
|
+
const mockOnDelete = jest.fn();
|
|
8
|
+
|
|
9
|
+
const mockAccounts = [
|
|
10
|
+
{
|
|
11
|
+
id: 1,
|
|
12
|
+
name: 'my-pipeline',
|
|
13
|
+
client_id: 'abc-123-xyz',
|
|
14
|
+
created_at: '2024-12-01T00:00:00Z',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 2,
|
|
18
|
+
name: 'etl-job',
|
|
19
|
+
client_id: 'def-456-uvw',
|
|
20
|
+
created_at: '2024-12-02T00:00:00Z',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders empty state when no accounts', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ServiceAccountsSection
|
|
31
|
+
accounts={[]}
|
|
32
|
+
onCreate={mockOnCreate}
|
|
33
|
+
onDelete={mockOnDelete}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders accounts list', () => {
|
|
41
|
+
render(
|
|
42
|
+
<ServiceAccountsSection
|
|
43
|
+
accounts={mockAccounts}
|
|
44
|
+
onCreate={mockOnCreate}
|
|
45
|
+
onDelete={mockOnDelete}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(screen.getByText('my-pipeline')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('etl-job')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('abc-123-xyz')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('def-456-uvw')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders section title and create button', () => {
|
|
56
|
+
render(
|
|
57
|
+
<ServiceAccountsSection
|
|
58
|
+
accounts={[]}
|
|
59
|
+
onCreate={mockOnCreate}
|
|
60
|
+
onDelete={mockOnDelete}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText('Service Accounts')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('+ Create')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('opens create modal when create button is clicked', () => {
|
|
69
|
+
render(
|
|
70
|
+
<ServiceAccountsSection
|
|
71
|
+
accounts={[]}
|
|
72
|
+
onCreate={mockOnCreate}
|
|
73
|
+
onDelete={mockOnDelete}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
fireEvent.click(screen.getByText('+ Create'));
|
|
78
|
+
|
|
79
|
+
expect(screen.getByText('Create Service Account')).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls onDelete when delete is confirmed', async () => {
|
|
83
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
84
|
+
mockOnDelete.mockResolvedValue();
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<ServiceAccountsSection
|
|
88
|
+
accounts={mockAccounts}
|
|
89
|
+
onCreate={mockOnCreate}
|
|
90
|
+
onDelete={mockOnDelete}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const deleteButtons = screen.getAllByTitle('Delete service account');
|
|
95
|
+
fireEvent.click(deleteButtons[0]);
|
|
96
|
+
|
|
97
|
+
expect(window.confirm).toHaveBeenCalledWith(
|
|
98
|
+
'Delete service account "my-pipeline"?\n\nThis will revoke all access for this account and cannot be undone.',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(mockOnDelete).toHaveBeenCalledWith('abc-123-xyz');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not call onDelete when delete is cancelled', () => {
|
|
107
|
+
window.confirm = jest.fn().mockReturnValue(false);
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<ServiceAccountsSection
|
|
111
|
+
accounts={mockAccounts}
|
|
112
|
+
onCreate={mockOnCreate}
|
|
113
|
+
onDelete={mockOnDelete}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const deleteButtons = screen.getAllByTitle('Delete service account');
|
|
118
|
+
fireEvent.click(deleteButtons[0]);
|
|
119
|
+
|
|
120
|
+
expect(mockOnDelete).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders description text', () => {
|
|
124
|
+
render(
|
|
125
|
+
<ServiceAccountsSection
|
|
126
|
+
accounts={[]}
|
|
127
|
+
onCreate={mockOnCreate}
|
|
128
|
+
onDelete={mockOnDelete}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
screen.getByText(/Service accounts allow programmatic access/i),
|
|
134
|
+
).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('shows table headers when accounts exist', () => {
|
|
138
|
+
render(
|
|
139
|
+
<ServiceAccountsSection
|
|
140
|
+
accounts={mockAccounts}
|
|
141
|
+
onCreate={mockOnCreate}
|
|
142
|
+
onDelete={mockOnDelete}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
147
|
+
expect(screen.getByText('Client ID')).toBeInTheDocument();
|
|
148
|
+
expect(screen.getByText('Created')).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { SettingsPage } from '../index';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
|
|
6
|
+
describe('SettingsPage', () => {
|
|
7
|
+
const mockDjClient = {
|
|
8
|
+
whoami: jest.fn(),
|
|
9
|
+
getNotificationPreferences: jest.fn(),
|
|
10
|
+
getNodesByNames: jest.fn(),
|
|
11
|
+
listServiceAccounts: jest.fn(),
|
|
12
|
+
subscribeToNotifications: jest.fn(),
|
|
13
|
+
unsubscribeFromNotifications: jest.fn(),
|
|
14
|
+
createServiceAccount: jest.fn(),
|
|
15
|
+
deleteServiceAccount: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const renderWithContext = () => {
|
|
19
|
+
return render(
|
|
20
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
21
|
+
<SettingsPage />
|
|
22
|
+
</DJClientContext.Provider>,
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
|
|
29
|
+
// Default mock implementations
|
|
30
|
+
mockDjClient.whoami.mockResolvedValue({
|
|
31
|
+
username: 'testuser',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
name: 'Test User',
|
|
34
|
+
});
|
|
35
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([]);
|
|
36
|
+
mockDjClient.getNodesByNames.mockResolvedValue([]);
|
|
37
|
+
mockDjClient.listServiceAccounts.mockResolvedValue([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows loading state initially', () => {
|
|
41
|
+
// Make whoami hang
|
|
42
|
+
mockDjClient.whoami.mockImplementation(() => new Promise(() => {}));
|
|
43
|
+
|
|
44
|
+
renderWithContext();
|
|
45
|
+
|
|
46
|
+
// Should show loading icon (or some loading indicator)
|
|
47
|
+
expect(document.querySelector('.settings-page')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders all sections after loading', async () => {
|
|
51
|
+
renderWithContext();
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Profile')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('Notification Subscriptions')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Service Accounts')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fetches and displays user profile', async () => {
|
|
63
|
+
mockDjClient.whoami.mockResolvedValue({
|
|
64
|
+
username: 'alice',
|
|
65
|
+
email: 'alice@example.com',
|
|
66
|
+
name: 'Alice Smith',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
renderWithContext();
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByText('AS')).toBeInTheDocument(); // initials
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fetches and displays subscriptions', async () => {
|
|
80
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
81
|
+
{
|
|
82
|
+
entity_name: 'default.my_metric',
|
|
83
|
+
entity_type: 'node',
|
|
84
|
+
activity_types: ['update'],
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
89
|
+
{
|
|
90
|
+
name: 'default.my_metric',
|
|
91
|
+
type: 'METRIC',
|
|
92
|
+
current: {
|
|
93
|
+
displayName: 'My Metric',
|
|
94
|
+
status: 'VALID',
|
|
95
|
+
mode: 'PUBLISHED',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
renderWithContext();
|
|
101
|
+
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(screen.getByText('default.my_metric')).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('fetches and displays service accounts', async () => {
|
|
108
|
+
mockDjClient.listServiceAccounts.mockResolvedValue([
|
|
109
|
+
{
|
|
110
|
+
id: 1,
|
|
111
|
+
name: 'my-pipeline',
|
|
112
|
+
client_id: 'abc-123',
|
|
113
|
+
created_at: '2024-12-01T00:00:00Z',
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
renderWithContext();
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(screen.getByText('my-pipeline')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('abc-123')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles service accounts API error gracefully', async () => {
|
|
127
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
128
|
+
mockDjClient.listServiceAccounts.mockRejectedValue(
|
|
129
|
+
new Error('Not available'),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
renderWithContext();
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Page should still render without service accounts
|
|
139
|
+
expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
|
|
140
|
+
|
|
141
|
+
consoleSpy.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles fetch error gracefully', async () => {
|
|
145
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
146
|
+
mockDjClient.whoami.mockRejectedValue(new Error('Network error'));
|
|
147
|
+
|
|
148
|
+
renderWithContext();
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
// Page should render even after error
|
|
152
|
+
expect(document.querySelector('.settings-page')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
consoleSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('enriches subscriptions with node info from GraphQL', async () => {
|
|
159
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
160
|
+
{
|
|
161
|
+
entity_name: 'default.orders',
|
|
162
|
+
entity_type: 'node',
|
|
163
|
+
activity_types: ['update'],
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
168
|
+
{
|
|
169
|
+
name: 'default.orders',
|
|
170
|
+
type: 'SOURCE',
|
|
171
|
+
current: {
|
|
172
|
+
displayName: 'Orders Table',
|
|
173
|
+
status: 'VALID',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
renderWithContext();
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(screen.getByText('SOURCE')).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
4
|
+
import ProfileSection from './ProfileSection';
|
|
5
|
+
import NotificationSubscriptionsSection from './NotificationSubscriptionsSection';
|
|
6
|
+
import ServiceAccountsSection from './ServiceAccountsSection';
|
|
7
|
+
import '../../../styles/settings.css';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main Settings page that orchestrates all settings sections.
|
|
11
|
+
*/
|
|
12
|
+
export function SettingsPage() {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const [currentUser, setCurrentUser] = useState(null);
|
|
15
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
16
|
+
const [serviceAccounts, setServiceAccounts] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
async function fetchData() {
|
|
21
|
+
try {
|
|
22
|
+
// Fetch user profile
|
|
23
|
+
const user = await djClient.whoami();
|
|
24
|
+
setCurrentUser(user);
|
|
25
|
+
|
|
26
|
+
// Fetch notification subscriptions
|
|
27
|
+
const prefs = await djClient.getNotificationPreferences();
|
|
28
|
+
|
|
29
|
+
// Fetch node details for subscriptions via GraphQL
|
|
30
|
+
const nodeNames = (prefs || [])
|
|
31
|
+
.filter(p => p.entity_type === 'node')
|
|
32
|
+
.map(p => p.entity_name);
|
|
33
|
+
|
|
34
|
+
let nodeInfoMap = {};
|
|
35
|
+
if (nodeNames.length > 0) {
|
|
36
|
+
const nodes = await djClient.getNodesByNames(nodeNames);
|
|
37
|
+
nodeInfoMap = Object.fromEntries(
|
|
38
|
+
nodes.map(n => [
|
|
39
|
+
n.name,
|
|
40
|
+
{
|
|
41
|
+
node_type: n.type?.toLowerCase(),
|
|
42
|
+
display_name: n.current?.displayName,
|
|
43
|
+
status: n.current?.status?.toLowerCase(),
|
|
44
|
+
mode: n.current?.mode?.toLowerCase(),
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Merge node info into subscriptions
|
|
51
|
+
const enrichedPrefs = (prefs || []).map(pref => ({
|
|
52
|
+
...pref,
|
|
53
|
+
...(nodeInfoMap[pref.entity_name] || {}),
|
|
54
|
+
}));
|
|
55
|
+
setSubscriptions(enrichedPrefs);
|
|
56
|
+
|
|
57
|
+
// Fetch service accounts
|
|
58
|
+
try {
|
|
59
|
+
const accounts = await djClient.listServiceAccounts();
|
|
60
|
+
setServiceAccounts(accounts || []);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Service accounts may not be available, ignore error
|
|
63
|
+
console.log('Service accounts not available:', err);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error fetching settings data:', error);
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fetchData();
|
|
72
|
+
}, [djClient]);
|
|
73
|
+
|
|
74
|
+
// Subscription handlers
|
|
75
|
+
const handleUpdateSubscription = async (sub, activityTypes) => {
|
|
76
|
+
await djClient.subscribeToNotifications({
|
|
77
|
+
entity_type: sub.entity_type,
|
|
78
|
+
entity_name: sub.entity_name,
|
|
79
|
+
activity_types: activityTypes,
|
|
80
|
+
alert_types: sub.alert_types || ['web'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Update local state
|
|
84
|
+
setSubscriptions(
|
|
85
|
+
subscriptions.map(s =>
|
|
86
|
+
s.entity_name === sub.entity_name
|
|
87
|
+
? { ...s, activity_types: activityTypes }
|
|
88
|
+
: s,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleUnsubscribe = async sub => {
|
|
94
|
+
await djClient.unsubscribeFromNotifications({
|
|
95
|
+
entity_type: sub.entity_type,
|
|
96
|
+
entity_name: sub.entity_name,
|
|
97
|
+
});
|
|
98
|
+
setSubscriptions(
|
|
99
|
+
subscriptions.filter(s => s.entity_name !== sub.entity_name),
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Service account handlers
|
|
104
|
+
const handleCreateServiceAccount = async name => {
|
|
105
|
+
const result = await djClient.createServiceAccount(name);
|
|
106
|
+
if (result.client_id) {
|
|
107
|
+
setServiceAccounts([...serviceAccounts, result]);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleDeleteServiceAccount = async clientId => {
|
|
113
|
+
await djClient.deleteServiceAccount(clientId);
|
|
114
|
+
setServiceAccounts(serviceAccounts.filter(a => a.client_id !== clientId));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (loading) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="settings-page">
|
|
120
|
+
<div className="settings-container">
|
|
121
|
+
<LoadingIcon />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="settings-page">
|
|
129
|
+
<div className="settings-container">
|
|
130
|
+
<h1 className="settings-title">Settings</h1>
|
|
131
|
+
|
|
132
|
+
<ProfileSection user={currentUser} />
|
|
133
|
+
|
|
134
|
+
<NotificationSubscriptionsSection
|
|
135
|
+
subscriptions={subscriptions}
|
|
136
|
+
onUpdate={handleUpdateSubscription}
|
|
137
|
+
onUnsubscribe={handleUnsubscribe}
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<ServiceAccountsSection
|
|
141
|
+
accounts={serviceAccounts}
|
|
142
|
+
onCreate={handleCreateServiceAccount}
|
|
143
|
+
onDelete={handleDeleteServiceAccount}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -323,6 +323,41 @@ export const DataJunctionAPI = {
|
|
|
323
323
|
return results.data.findNodes[0];
|
|
324
324
|
},
|
|
325
325
|
|
|
326
|
+
// Fetch basic node info for multiple nodes by name (for Settings page)
|
|
327
|
+
getNodesByNames: async function (names) {
|
|
328
|
+
if (!names || names.length === 0) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const query = `
|
|
332
|
+
query GetNodesByNames($names: [String!]) {
|
|
333
|
+
findNodes(names: $names) {
|
|
334
|
+
name
|
|
335
|
+
type
|
|
336
|
+
current {
|
|
337
|
+
displayName
|
|
338
|
+
status
|
|
339
|
+
mode
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const results = await (
|
|
346
|
+
await fetch(DJ_GQL, {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: {
|
|
349
|
+
'Content-Type': 'application/json',
|
|
350
|
+
},
|
|
351
|
+
credentials: 'include',
|
|
352
|
+
body: JSON.stringify({
|
|
353
|
+
query,
|
|
354
|
+
variables: { names },
|
|
355
|
+
}),
|
|
356
|
+
})
|
|
357
|
+
).json();
|
|
358
|
+
return results.data?.findNodes || [];
|
|
359
|
+
},
|
|
360
|
+
|
|
326
361
|
getMetric: async function (name) {
|
|
327
362
|
const query = `
|
|
328
363
|
query GetMetric($name: String!) {
|
|
@@ -1463,4 +1498,50 @@ export const DataJunctionAPI = {
|
|
|
1463
1498
|
json: await response.json(),
|
|
1464
1499
|
};
|
|
1465
1500
|
},
|
|
1501
|
+
|
|
1502
|
+
// GET /history/ with only_subscribed filter
|
|
1503
|
+
getSubscribedHistory: async function (limit = 10) {
|
|
1504
|
+
return await (
|
|
1505
|
+
await fetch(`${DJ_URL}/history/?only_subscribed=true&limit=${limit}`, {
|
|
1506
|
+
credentials: 'include',
|
|
1507
|
+
})
|
|
1508
|
+
).json();
|
|
1509
|
+
},
|
|
1510
|
+
|
|
1511
|
+
// POST /notifications/mark-read
|
|
1512
|
+
markNotificationsRead: async function () {
|
|
1513
|
+
const response = await fetch(`${DJ_URL}/notifications/mark-read`, {
|
|
1514
|
+
method: 'POST',
|
|
1515
|
+
credentials: 'include',
|
|
1516
|
+
});
|
|
1517
|
+
return await response.json();
|
|
1518
|
+
},
|
|
1519
|
+
|
|
1520
|
+
// Service Account APIs
|
|
1521
|
+
listServiceAccounts: async function () {
|
|
1522
|
+
const response = await fetch(`${DJ_URL}/service-accounts`, {
|
|
1523
|
+
credentials: 'include',
|
|
1524
|
+
});
|
|
1525
|
+
return await response.json();
|
|
1526
|
+
},
|
|
1527
|
+
|
|
1528
|
+
createServiceAccount: async function (name) {
|
|
1529
|
+
const response = await fetch(`${DJ_URL}/service-accounts`, {
|
|
1530
|
+
method: 'POST',
|
|
1531
|
+
headers: {
|
|
1532
|
+
'Content-Type': 'application/json',
|
|
1533
|
+
},
|
|
1534
|
+
credentials: 'include',
|
|
1535
|
+
body: JSON.stringify({ name }),
|
|
1536
|
+
});
|
|
1537
|
+
return await response.json();
|
|
1538
|
+
},
|
|
1539
|
+
|
|
1540
|
+
deleteServiceAccount: async function (clientId) {
|
|
1541
|
+
const response = await fetch(`${DJ_URL}/service-accounts/${clientId}`, {
|
|
1542
|
+
method: 'DELETE',
|
|
1543
|
+
credentials: 'include',
|
|
1544
|
+
});
|
|
1545
|
+
return await response.json();
|
|
1546
|
+
},
|
|
1466
1547
|
};
|