datajunction-ui 0.0.22 → 0.0.23
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 +11 -5
- package/src/app/components/UserMenu.tsx +3 -11
- package/src/app/components/__tests__/NotificationBell.test.tsx +17 -6
- package/src/app/components/__tests__/UserMenu.test.tsx +10 -3
- package/src/app/index.tsx +92 -85
- package/src/app/pages/AddEditNodePage/OwnersField.jsx +2 -3
- package/src/app/pages/NamespacePage/index.jsx +2 -4
- package/src/app/pages/NodePage/ClientCodePopover.jsx +27 -5
- package/src/app/pages/NodePage/NodeInfoTab.jsx +73 -15
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +23 -13
- package/src/app/pages/NodePage/index.jsx +14 -19
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +4 -1
- package/src/app/pages/SettingsPage/index.jsx +6 -6
- package/src/app/providers/UserProvider.tsx +78 -0
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
import DJClientContext from '../providers/djclient';
|
|
3
|
+
import { useCurrentUser } from '../providers/UserProvider';
|
|
3
4
|
import NotificationIcon from '../icons/NotificationIcon';
|
|
4
5
|
import SettingsIcon from '../icons/SettingsIcon';
|
|
5
6
|
import LoadingIcon from '../icons/LoadingIcon';
|
|
@@ -68,6 +69,7 @@ export default function NotificationBell({
|
|
|
68
69
|
forceClose,
|
|
69
70
|
}: NotificationBellProps) {
|
|
70
71
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
72
|
+
const { currentUser, loading: userLoading } = useCurrentUser();
|
|
71
73
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
72
74
|
|
|
73
75
|
// Close when forceClose becomes true
|
|
@@ -82,14 +84,15 @@ export default function NotificationBell({
|
|
|
82
84
|
const [loading, setLoading] = useState(false);
|
|
83
85
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
84
86
|
|
|
85
|
-
// Fetch notifications
|
|
87
|
+
// Fetch notifications when user data is available
|
|
86
88
|
useEffect(() => {
|
|
89
|
+
if (userLoading) return;
|
|
90
|
+
|
|
87
91
|
async function fetchNotifications() {
|
|
88
92
|
setLoading(true);
|
|
89
93
|
try {
|
|
90
|
-
const current = await djClient.whoami();
|
|
91
94
|
const history: HistoryEntry[] =
|
|
92
|
-
(await djClient.getSubscribedHistory(
|
|
95
|
+
(await djClient.getSubscribedHistory(5)) || [];
|
|
93
96
|
|
|
94
97
|
// Get unique entity names and fetch their info via GraphQL
|
|
95
98
|
// (some may not be nodes, but GraphQL will just not return them)
|
|
@@ -101,7 +104,10 @@ export default function NotificationBell({
|
|
|
101
104
|
const enriched = enrichWithNodeInfo(history, nodes);
|
|
102
105
|
setNotifications(enriched);
|
|
103
106
|
setUnreadCount(
|
|
104
|
-
calculateUnreadCount(
|
|
107
|
+
calculateUnreadCount(
|
|
108
|
+
history,
|
|
109
|
+
currentUser?.last_viewed_notifications_at,
|
|
110
|
+
),
|
|
105
111
|
);
|
|
106
112
|
} catch (error) {
|
|
107
113
|
console.error('Error fetching notifications:', error);
|
|
@@ -110,7 +116,7 @@ export default function NotificationBell({
|
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
fetchNotifications();
|
|
113
|
-
}, [djClient]);
|
|
119
|
+
}, [djClient, currentUser, userLoading]);
|
|
114
120
|
|
|
115
121
|
// Close dropdown when clicking outside
|
|
116
122
|
useEffect(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
import DJClientContext from '../providers/djclient';
|
|
3
|
+
import { useCurrentUser } from '../providers/UserProvider';
|
|
3
4
|
|
|
4
5
|
interface User {
|
|
5
6
|
id: number;
|
|
@@ -32,7 +33,7 @@ export default function UserMenu({
|
|
|
32
33
|
forceClose,
|
|
33
34
|
}: UserMenuProps) {
|
|
34
35
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
35
|
-
const
|
|
36
|
+
const { currentUser } = useCurrentUser();
|
|
36
37
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
37
38
|
|
|
38
39
|
// Close when forceClose becomes true
|
|
@@ -42,15 +43,6 @@ export default function UserMenu({
|
|
|
42
43
|
}
|
|
43
44
|
}, [forceClose, showDropdown]);
|
|
44
45
|
|
|
45
|
-
// Fetch current user on mount
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
async function fetchUser() {
|
|
48
|
-
const user = await djClient.whoami();
|
|
49
|
-
setCurrentUser(user);
|
|
50
|
-
}
|
|
51
|
-
fetchUser();
|
|
52
|
-
}, [djClient]);
|
|
53
|
-
|
|
54
46
|
// Close dropdown when clicking outside
|
|
55
47
|
useEffect(() => {
|
|
56
48
|
const handleClickOutside = (event: MouseEvent) => {
|
|
@@ -79,7 +71,7 @@ export default function UserMenu({
|
|
|
79
71
|
return (
|
|
80
72
|
<div className="nav-dropdown user-menu-dropdown">
|
|
81
73
|
<button className="avatar-button" onClick={handleToggle}>
|
|
82
|
-
{getInitials(currentUser)}
|
|
74
|
+
{getInitials(currentUser as User | null)}
|
|
83
75
|
</button>
|
|
84
76
|
{showDropdown && (
|
|
85
77
|
<div className="nav-dropdown-menu">
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import NotificationBell from '../NotificationBell';
|
|
4
4
|
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import { UserProvider } from '../../providers/UserProvider';
|
|
5
6
|
|
|
6
7
|
describe('<NotificationBell />', () => {
|
|
7
8
|
const mockNotifications = [
|
|
@@ -52,10 +53,12 @@ describe('<NotificationBell />', () => {
|
|
|
52
53
|
...overrides,
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
const renderWithContext = (mockDjClient: any) => {
|
|
56
|
+
const renderWithContext = (mockDjClient: any, props = {}) => {
|
|
56
57
|
return render(
|
|
57
58
|
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
58
|
-
<
|
|
59
|
+
<UserProvider>
|
|
60
|
+
<NotificationBell {...props} />
|
|
61
|
+
</UserProvider>
|
|
59
62
|
</DJClientContext.Provider>,
|
|
60
63
|
);
|
|
61
64
|
};
|
|
@@ -218,7 +221,9 @@ describe('<NotificationBell />', () => {
|
|
|
218
221
|
<DJClientContext.Provider
|
|
219
222
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
220
223
|
>
|
|
221
|
-
<
|
|
224
|
+
<UserProvider>
|
|
225
|
+
<NotificationBell onDropdownToggle={onDropdownToggle} />
|
|
226
|
+
</UserProvider>
|
|
222
227
|
</DJClientContext.Provider>,
|
|
223
228
|
);
|
|
224
229
|
|
|
@@ -239,7 +244,9 @@ describe('<NotificationBell />', () => {
|
|
|
239
244
|
<DJClientContext.Provider
|
|
240
245
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
241
246
|
>
|
|
242
|
-
<
|
|
247
|
+
<UserProvider>
|
|
248
|
+
<NotificationBell forceClose={false} />
|
|
249
|
+
</UserProvider>
|
|
243
250
|
</DJClientContext.Provider>,
|
|
244
251
|
);
|
|
245
252
|
|
|
@@ -259,7 +266,9 @@ describe('<NotificationBell />', () => {
|
|
|
259
266
|
<DJClientContext.Provider
|
|
260
267
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
261
268
|
>
|
|
262
|
-
<
|
|
269
|
+
<UserProvider>
|
|
270
|
+
<NotificationBell forceClose={true} />
|
|
271
|
+
</UserProvider>
|
|
263
272
|
</DJClientContext.Provider>,
|
|
264
273
|
);
|
|
265
274
|
|
|
@@ -275,7 +284,9 @@ describe('<NotificationBell />', () => {
|
|
|
275
284
|
<DJClientContext.Provider
|
|
276
285
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
277
286
|
>
|
|
278
|
-
<
|
|
287
|
+
<UserProvider>
|
|
288
|
+
<NotificationBell onDropdownToggle={onDropdownToggle} />
|
|
289
|
+
</UserProvider>
|
|
279
290
|
</DJClientContext.Provider>,
|
|
280
291
|
);
|
|
281
292
|
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import UserMenu from '../UserMenu';
|
|
4
4
|
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import { UserProvider } from '../../providers/UserProvider';
|
|
5
6
|
|
|
6
7
|
describe('<UserMenu />', () => {
|
|
7
8
|
const createMockDjClient = (overrides = {}) => ({
|
|
@@ -17,7 +18,9 @@ describe('<UserMenu />', () => {
|
|
|
17
18
|
const renderWithContext = (mockDjClient: any, props = {}) => {
|
|
18
19
|
return render(
|
|
19
20
|
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
20
|
-
<
|
|
21
|
+
<UserProvider>
|
|
22
|
+
<UserMenu {...props} />
|
|
23
|
+
</UserProvider>
|
|
21
24
|
</DJClientContext.Provider>,
|
|
22
25
|
);
|
|
23
26
|
};
|
|
@@ -164,7 +167,9 @@ describe('<UserMenu />', () => {
|
|
|
164
167
|
<DJClientContext.Provider
|
|
165
168
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
166
169
|
>
|
|
167
|
-
<
|
|
170
|
+
<UserProvider>
|
|
171
|
+
<UserMenu forceClose={false} />
|
|
172
|
+
</UserProvider>
|
|
168
173
|
</DJClientContext.Provider>,
|
|
169
174
|
);
|
|
170
175
|
|
|
@@ -184,7 +189,9 @@ describe('<UserMenu />', () => {
|
|
|
184
189
|
<DJClientContext.Provider
|
|
185
190
|
value={{ DataJunctionAPI: mockDjClient as any }}
|
|
186
191
|
>
|
|
187
|
-
<
|
|
192
|
+
<UserProvider>
|
|
193
|
+
<UserMenu forceClose={true} />
|
|
194
|
+
</UserProvider>
|
|
188
195
|
</DJClientContext.Provider>,
|
|
189
196
|
);
|
|
190
197
|
|
package/src/app/index.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { NamespacePage } from './pages/NamespacePage/Loadable';
|
|
|
11
11
|
import { OverviewPage } from './pages/OverviewPage/Loadable';
|
|
12
12
|
import { SettingsPage } from './pages/SettingsPage/Loadable';
|
|
13
13
|
import { NotificationsPage } from './pages/NotificationsPage/Loadable';
|
|
14
|
-
import { NodePage } from './pages/NodePage
|
|
14
|
+
import { NodePage } from './pages/NodePage';
|
|
15
15
|
import RevisionDiff from './pages/NodePage/RevisionDiff';
|
|
16
16
|
import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
|
|
17
17
|
import { CubeBuilderPage } from './pages/CubeBuilderPage/Loadable';
|
|
@@ -21,8 +21,9 @@ import { AddEditTagPage } from './pages/AddEditTagPage/Loadable';
|
|
|
21
21
|
import { NotFoundPage } from './pages/NotFoundPage/Loadable';
|
|
22
22
|
import { LoginPage } from './pages/LoginPage';
|
|
23
23
|
import { RegisterTablePage } from './pages/RegisterTablePage';
|
|
24
|
-
import { Root } from './pages/Root
|
|
24
|
+
import { Root } from './pages/Root';
|
|
25
25
|
import DJClientContext from './providers/djclient';
|
|
26
|
+
import { UserProvider } from './providers/UserProvider';
|
|
26
27
|
import { DataJunctionAPI } from './services/DJService';
|
|
27
28
|
import { CookiesProvider, useCookies } from 'react-cookie';
|
|
28
29
|
import * as Constants from './constants';
|
|
@@ -44,101 +45,107 @@ export function App() {
|
|
|
44
45
|
/>
|
|
45
46
|
</Helmet>
|
|
46
47
|
<DJClientContext.Provider value={{ DataJunctionAPI }}>
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<Route path="
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
48
|
+
<UserProvider>
|
|
49
|
+
<Routes>
|
|
50
|
+
<Route
|
|
51
|
+
path="/"
|
|
52
|
+
element={<Root />}
|
|
53
|
+
children={
|
|
54
|
+
<>
|
|
55
|
+
<Route path="nodes" key="nodes">
|
|
56
|
+
<Route path=":name" element={<NodePage />} />
|
|
57
|
+
<Route
|
|
58
|
+
path=":name/edit"
|
|
59
|
+
key="edit"
|
|
60
|
+
element={<AddEditNodePage />}
|
|
61
|
+
/>
|
|
62
|
+
<Route
|
|
63
|
+
path=":name/edit-cube"
|
|
64
|
+
key="edit-cube"
|
|
65
|
+
element={<CubeBuilderPage />}
|
|
66
|
+
/>
|
|
67
|
+
<Route
|
|
68
|
+
path=":name/revisions/:revision"
|
|
69
|
+
element={<RevisionDiff />}
|
|
70
|
+
/>
|
|
71
|
+
<Route path=":name/:tab" element={<NodePage />} />
|
|
72
|
+
</Route>
|
|
71
73
|
|
|
72
|
-
<Route path="/" element={<NamespacePage />} key="index" />
|
|
73
|
-
<Route path="namespaces">
|
|
74
74
|
<Route
|
|
75
|
-
path="
|
|
75
|
+
path="/"
|
|
76
76
|
element={<NamespacePage />}
|
|
77
|
-
key="
|
|
77
|
+
key="index"
|
|
78
78
|
/>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
<Route path="namespaces">
|
|
80
|
+
<Route
|
|
81
|
+
path=":namespace"
|
|
82
|
+
element={<NamespacePage />}
|
|
83
|
+
key="namespaces"
|
|
84
|
+
/>
|
|
85
|
+
</Route>
|
|
86
|
+
<Route
|
|
87
|
+
path="create/tag"
|
|
88
|
+
key="createtag"
|
|
89
|
+
element={<AddEditTagPage />}
|
|
90
|
+
></Route>
|
|
91
|
+
<Route
|
|
92
|
+
path="create/source"
|
|
93
|
+
key="register"
|
|
94
|
+
element={<RegisterTablePage />}
|
|
95
|
+
></Route>
|
|
96
|
+
<Route path="/create/cube">
|
|
97
|
+
<Route
|
|
98
|
+
path=":initialNamespace"
|
|
99
|
+
key="create"
|
|
100
|
+
element={<CubeBuilderPage />}
|
|
101
|
+
/>
|
|
102
|
+
<Route
|
|
103
|
+
path=""
|
|
104
|
+
key="create"
|
|
105
|
+
element={<CubeBuilderPage />}
|
|
106
|
+
/>
|
|
107
|
+
</Route>
|
|
108
|
+
<Route path="create/:nodeType">
|
|
109
|
+
<Route
|
|
110
|
+
path=":initialNamespace"
|
|
111
|
+
key="create"
|
|
112
|
+
element={<AddEditNodePage />}
|
|
113
|
+
/>
|
|
114
|
+
<Route
|
|
115
|
+
path=""
|
|
116
|
+
key="create"
|
|
117
|
+
element={<AddEditNodePage />}
|
|
118
|
+
/>
|
|
119
|
+
</Route>
|
|
91
120
|
<Route
|
|
92
|
-
path="
|
|
93
|
-
key="
|
|
94
|
-
element={<
|
|
121
|
+
path="sql"
|
|
122
|
+
key="sql"
|
|
123
|
+
element={<SQLBuilderPage />}
|
|
95
124
|
/>
|
|
125
|
+
<Route path="tags" key="tags">
|
|
126
|
+
<Route path=":name" element={<TagPage />} />
|
|
127
|
+
</Route>
|
|
96
128
|
<Route
|
|
97
|
-
path=""
|
|
98
|
-
key="
|
|
99
|
-
element={<
|
|
129
|
+
path="overview"
|
|
130
|
+
key="overview"
|
|
131
|
+
element={<OverviewPage />}
|
|
100
132
|
/>
|
|
101
|
-
</Route>
|
|
102
|
-
<Route path="create/:nodeType">
|
|
103
133
|
<Route
|
|
104
|
-
path="
|
|
105
|
-
key="
|
|
106
|
-
element={<
|
|
134
|
+
path="settings"
|
|
135
|
+
key="settings"
|
|
136
|
+
element={<SettingsPage />}
|
|
107
137
|
/>
|
|
108
138
|
<Route
|
|
109
|
-
path=""
|
|
110
|
-
key="
|
|
111
|
-
element={<
|
|
139
|
+
path="notifications"
|
|
140
|
+
key="notifications"
|
|
141
|
+
element={<NotificationsPage />}
|
|
112
142
|
/>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<Route path="tags" key="tags">
|
|
120
|
-
<Route path=":name" element={<TagPage />} />
|
|
121
|
-
</Route>
|
|
122
|
-
<Route
|
|
123
|
-
path="overview"
|
|
124
|
-
key="overview"
|
|
125
|
-
element={<OverviewPage />}
|
|
126
|
-
/>
|
|
127
|
-
<Route
|
|
128
|
-
path="settings"
|
|
129
|
-
key="settings"
|
|
130
|
-
element={<SettingsPage />}
|
|
131
|
-
/>
|
|
132
|
-
<Route
|
|
133
|
-
path="notifications"
|
|
134
|
-
key="notifications"
|
|
135
|
-
element={<NotificationsPage />}
|
|
136
|
-
/>
|
|
137
|
-
</>
|
|
138
|
-
}
|
|
139
|
-
/>
|
|
140
|
-
<Route path="*" element={<NotFoundPage />} />
|
|
141
|
-
</Routes>
|
|
143
|
+
</>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
147
|
+
</Routes>
|
|
148
|
+
</UserProvider>
|
|
142
149
|
</DJClientContext.Provider>
|
|
143
150
|
</>
|
|
144
151
|
) : (
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
import { ErrorMessage } from 'formik';
|
|
5
5
|
import { useContext, useEffect, useState } from 'react';
|
|
6
6
|
import DJClientContext from '../../providers/djclient';
|
|
7
|
+
import { useCurrentUser } from '../../providers/UserProvider';
|
|
7
8
|
import { FormikSelect } from './FormikSelect';
|
|
8
9
|
|
|
9
10
|
export const OwnersField = ({ defaultValue }) => {
|
|
10
11
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
12
|
+
const { currentUser } = useCurrentUser();
|
|
11
13
|
|
|
12
14
|
const [availableUsers, setAvailableUsers] = useState([]);
|
|
13
|
-
const [currentUser, setCurrentUser] = useState(null);
|
|
14
15
|
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
async function fetchData() {
|
|
@@ -23,8 +24,6 @@ export const OwnersField = ({ defaultValue }) => {
|
|
|
23
24
|
};
|
|
24
25
|
}),
|
|
25
26
|
);
|
|
26
|
-
const current = await djClient.whoami();
|
|
27
|
-
setCurrentUser(current);
|
|
28
27
|
}
|
|
29
28
|
fetchData();
|
|
30
29
|
}, [djClient]);
|
|
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
|
|
|
3
3
|
import { useContext, useEffect, useState } from 'react';
|
|
4
4
|
import NodeStatus from '../NodePage/NodeStatus';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
|
+
import { useCurrentUser } from '../../providers/UserProvider';
|
|
6
7
|
import Explorer from '../NamespacePage/Explorer';
|
|
7
8
|
import AddNodeDropdown from '../../components/AddNodeDropdown';
|
|
8
9
|
import NodeListActions from '../../components/NodeListActions';
|
|
@@ -24,6 +25,7 @@ export function NamespacePage() {
|
|
|
24
25
|
const fields = ['name', 'displayName', 'type', 'status', 'mode', 'updatedAt'];
|
|
25
26
|
|
|
26
27
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
28
|
+
const { currentUser } = useCurrentUser();
|
|
27
29
|
var { namespace } = useParams();
|
|
28
30
|
|
|
29
31
|
const [state, setState] = useState({
|
|
@@ -31,7 +33,6 @@ export function NamespacePage() {
|
|
|
31
33
|
nodes: [],
|
|
32
34
|
});
|
|
33
35
|
const [retrieved, setRetrieved] = useState(false);
|
|
34
|
-
const [currentUser, setCurrentUser] = useState(null);
|
|
35
36
|
|
|
36
37
|
const [filters, setFilters] = useState({
|
|
37
38
|
tags: [],
|
|
@@ -105,9 +106,6 @@ export function NamespacePage() {
|
|
|
105
106
|
const namespaces = await djClient.namespaces();
|
|
106
107
|
const hierarchy = createNamespaceHierarchy(namespaces);
|
|
107
108
|
setNamespaceHierarchy(hierarchy);
|
|
108
|
-
const currentUser = await djClient.whoami();
|
|
109
|
-
// setFilters({...filters, edited_by: currentUser?.username});
|
|
110
|
-
setCurrentUser(currentUser);
|
|
111
109
|
};
|
|
112
110
|
fetchData().catch(console.error);
|
|
113
111
|
}, [djClient, djClient.namespaces]);
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
import DJClientContext from '../../providers/djclient';
|
|
1
2
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState, useContext } from 'react';
|
|
3
4
|
import { nightOwl } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
4
5
|
import PythonIcon from '../../icons/PythonIcon';
|
|
6
|
+
import LoadingIcon from 'app/icons/LoadingIcon';
|
|
5
7
|
|
|
6
|
-
export default function ClientCodePopover({
|
|
8
|
+
export default function ClientCodePopover({ nodeName }) {
|
|
9
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
7
10
|
const [showModal, setShowModal] = useState(false);
|
|
8
11
|
const modalRef = useRef(null);
|
|
12
|
+
const [code, setCode] = useState(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
async function fetchCode() {
|
|
16
|
+
try {
|
|
17
|
+
const code = await djClient.clientCode(nodeName);
|
|
18
|
+
setCode(code);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log(err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
fetchCode();
|
|
24
|
+
}, [nodeName, djClient]);
|
|
9
25
|
|
|
10
26
|
useEffect(() => {
|
|
11
27
|
const handleClickOutside = event => {
|
|
@@ -83,9 +99,15 @@ export default function ClientCodePopover({ code }) {
|
|
|
83
99
|
×
|
|
84
100
|
</button>
|
|
85
101
|
<h2>Python Client Code</h2>
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
|
|
102
|
+
{code ? (
|
|
103
|
+
<SyntaxHighlighter language="python" style={nightOwl}>
|
|
104
|
+
{code}
|
|
105
|
+
</SyntaxHighlighter>
|
|
106
|
+
) : (
|
|
107
|
+
<>
|
|
108
|
+
<LoadingIcon />
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
89
111
|
</div>
|
|
90
112
|
</div>
|
|
91
113
|
)}
|
|
@@ -11,9 +11,31 @@ import { labelize } from '../../../utils/form';
|
|
|
11
11
|
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
12
12
|
foundation.hljs['padding'] = '2rem';
|
|
13
13
|
|
|
14
|
+
// interface MetricInfo {
|
|
15
|
+
// name: string;
|
|
16
|
+
// current: MetricRevision;
|
|
17
|
+
// }
|
|
18
|
+
|
|
19
|
+
// interface MetricRevision {
|
|
20
|
+
// parents: array<string>;
|
|
21
|
+
// metricMetadata:
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
// interface MetricMetadata {
|
|
25
|
+
// direction: string;
|
|
26
|
+
// unit: string;
|
|
27
|
+
// expression: string;
|
|
28
|
+
// significantDigits: string;
|
|
29
|
+
// incompatibleDruidFunctions: array<string>;
|
|
30
|
+
// }
|
|
31
|
+
|
|
14
32
|
export default function NodeInfoTab({ node }) {
|
|
15
33
|
const [compiledSQL, setCompiledSQL] = useState('');
|
|
16
34
|
const [checked, setChecked] = useState(false);
|
|
35
|
+
|
|
36
|
+
// For metrics
|
|
37
|
+
const [metricInfo, setMetricInfo] = useState(null);
|
|
38
|
+
|
|
17
39
|
const nodeTags = node?.tags.map(tag => (
|
|
18
40
|
<div className={'badge tag_value'}>
|
|
19
41
|
<a href={`/tags/${tag.name}`}>{tag.display_name}</a>
|
|
@@ -36,17 +58,49 @@ export default function NodeInfoTab({ node }) {
|
|
|
36
58
|
};
|
|
37
59
|
fetchData().catch(console.error);
|
|
38
60
|
}, [node, djClient, checked]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const fetchData = async () => {
|
|
64
|
+
const metric = await djClient.getMetric(node.name);
|
|
65
|
+
setMetricInfo({
|
|
66
|
+
metric_metadata: metric.current.metricMetadata,
|
|
67
|
+
required_dimensions: metric.current.requiredDimensions,
|
|
68
|
+
upstream_node: metric.current.parents[0]?.name,
|
|
69
|
+
expression: metric.current.metricMetadata?.expression,
|
|
70
|
+
incompatible_druid_functions:
|
|
71
|
+
metric.current.metricMetadata?.incompatibleDruidFunctions || [],
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
if (node.type === 'metric') {
|
|
75
|
+
fetchData().catch(console.error);
|
|
76
|
+
}
|
|
77
|
+
}, [node, djClient]);
|
|
78
|
+
|
|
79
|
+
// For cubes
|
|
80
|
+
const [cubeElements, setCubeElements] = useState(null);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const fetchData = async () => {
|
|
84
|
+
const cube = await djClient.cube(node.name);
|
|
85
|
+
setCubeElements(cube.cube_elements);
|
|
86
|
+
};
|
|
87
|
+
if (node.type === 'cube') {
|
|
88
|
+
fetchData().catch(console.error);
|
|
89
|
+
}
|
|
90
|
+
}, [node, djClient]);
|
|
91
|
+
|
|
39
92
|
function toggle(value) {
|
|
40
93
|
return !value;
|
|
41
94
|
}
|
|
42
95
|
const metricsWarning =
|
|
43
|
-
node?.type === 'metric' &&
|
|
96
|
+
node?.type === 'metric' &&
|
|
97
|
+
metricInfo?.incompatible_druid_functions?.length > 0 ? (
|
|
44
98
|
<div className="message warning" style={{ marginTop: '0.7rem' }}>
|
|
45
99
|
⚠{' '}
|
|
46
100
|
<small>
|
|
47
101
|
The following functions used in the metric definition may not be
|
|
48
102
|
compatible with Druid SQL:{' '}
|
|
49
|
-
{
|
|
103
|
+
{metricInfo?.incompatible_druid_functions.map(func => (
|
|
50
104
|
<li
|
|
51
105
|
style={{ listStyleType: 'none', margin: '0.7rem 0.7rem' }}
|
|
52
106
|
key={func}
|
|
@@ -79,15 +133,15 @@ export default function NodeInfoTab({ node }) {
|
|
|
79
133
|
<div style={{ marginBottom: '30px' }}>
|
|
80
134
|
<h6 className="mb-0 w-100">Upstream Node</h6>
|
|
81
135
|
<p>
|
|
82
|
-
<a href={`/nodes/${
|
|
83
|
-
{
|
|
136
|
+
<a href={`/nodes/${metricInfo?.upstream_node}`}>
|
|
137
|
+
{metricInfo?.upstream_node}
|
|
84
138
|
</a>
|
|
85
139
|
</p>
|
|
86
140
|
</div>
|
|
87
141
|
<div>
|
|
88
142
|
<h6 className="mb-0 w-100">Aggregate Expression</h6>
|
|
89
143
|
<SyntaxHighlighter language="sql" style={foundation}>
|
|
90
|
-
{
|
|
144
|
+
{metricInfo?.expression}
|
|
91
145
|
</SyntaxHighlighter>
|
|
92
146
|
</div>
|
|
93
147
|
</div>
|
|
@@ -163,8 +217,10 @@ export default function NodeInfoTab({ node }) {
|
|
|
163
217
|
aria-hidden="false"
|
|
164
218
|
aria-label="MetricDirection"
|
|
165
219
|
>
|
|
166
|
-
{
|
|
167
|
-
? labelize(
|
|
220
|
+
{metricInfo?.metric_metadata?.direction
|
|
221
|
+
? labelize(
|
|
222
|
+
metricInfo?.metric_metadata?.direction?.toLowerCase(),
|
|
223
|
+
)
|
|
168
224
|
: 'None'}
|
|
169
225
|
</p>
|
|
170
226
|
</div>
|
|
@@ -176,8 +232,10 @@ export default function NodeInfoTab({ node }) {
|
|
|
176
232
|
aria-hidden="false"
|
|
177
233
|
aria-label="MetricUnit"
|
|
178
234
|
>
|
|
179
|
-
{
|
|
180
|
-
? labelize(
|
|
235
|
+
{metricInfo?.metric_metadata?.unit?.name
|
|
236
|
+
? labelize(
|
|
237
|
+
metricInfo?.metric_metadata?.unit?.name?.toLowerCase(),
|
|
238
|
+
)
|
|
181
239
|
: 'None'}
|
|
182
240
|
</p>
|
|
183
241
|
</div>
|
|
@@ -189,7 +247,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
189
247
|
aria-hidden="false"
|
|
190
248
|
aria-label="SignificantDigits"
|
|
191
249
|
>
|
|
192
|
-
{
|
|
250
|
+
{metricInfo?.metric_metadata?.significantDigits || 'None'}
|
|
193
251
|
</p>
|
|
194
252
|
</div>
|
|
195
253
|
</div>
|
|
@@ -218,7 +276,7 @@ export default function NodeInfoTab({ node }) {
|
|
|
218
276
|
''
|
|
219
277
|
);
|
|
220
278
|
|
|
221
|
-
const cubeElementsDiv =
|
|
279
|
+
const cubeElementsDiv = cubeElements ? (
|
|
222
280
|
<div className="list-group-item d-flex">
|
|
223
281
|
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
224
282
|
<div
|
|
@@ -228,10 +286,10 @@ export default function NodeInfoTab({ node }) {
|
|
|
228
286
|
>
|
|
229
287
|
<h6 className="mb-0 w-100">Cube Elements</h6>
|
|
230
288
|
<div className={`list-group-item`}>
|
|
231
|
-
{
|
|
289
|
+
{cubeElements.map(cubeElem =>
|
|
232
290
|
cubeElem.type === 'metric' ? displayCubeElement(cubeElem) : '',
|
|
233
291
|
)}
|
|
234
|
-
{
|
|
292
|
+
{cubeElements.map(cubeElem =>
|
|
235
293
|
cubeElem.type !== 'metric' ? displayCubeElement(cubeElem) : '',
|
|
236
294
|
)}
|
|
237
295
|
</div>
|
|
@@ -375,8 +433,8 @@ export default function NodeInfoTab({ node }) {
|
|
|
375
433
|
</div>
|
|
376
434
|
</div>
|
|
377
435
|
{metricMetadataDiv}
|
|
378
|
-
{node
|
|
379
|
-
{node
|
|
436
|
+
{node.type !== 'cube' && node.type !== 'metric' ? queryDiv : ''}
|
|
437
|
+
{node.type === 'metric' ? metricQueryDiv : ''}
|
|
380
438
|
{customMetadataDiv}
|
|
381
439
|
{cubeElementsDiv}
|
|
382
440
|
</div>
|
|
@@ -26,7 +26,7 @@ describe('<NodePage />', () => {
|
|
|
26
26
|
getMetric: jest.fn(),
|
|
27
27
|
revalidate: jest.fn().mockReturnValue({ status: 'valid' }),
|
|
28
28
|
node_dag: jest.fn().mockReturnValue(mocks.mockNodeDAG),
|
|
29
|
-
clientCode: jest.fn().
|
|
29
|
+
clientCode: jest.fn().mockResolvedValue('dj_client = DJClient()'),
|
|
30
30
|
columns: jest.fn(),
|
|
31
31
|
history: jest.fn(),
|
|
32
32
|
revisions: jest.fn(),
|
|
@@ -298,7 +298,7 @@ describe('<NodePage />', () => {
|
|
|
298
298
|
it('renders the NodeInfo tab correctly for a metric node', async () => {
|
|
299
299
|
const djClient = mockDJClient();
|
|
300
300
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
|
|
301
|
-
djClient.DataJunctionAPI.getMetric.
|
|
301
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
302
302
|
mocks.mockMetricNodeJson,
|
|
303
303
|
);
|
|
304
304
|
const element = (
|
|
@@ -351,17 +351,27 @@ describe('<NodePage />', () => {
|
|
|
351
351
|
expect(
|
|
352
352
|
screen.getByRole('dialog', { name: 'NodeType' }),
|
|
353
353
|
).toHaveTextContent('metric');
|
|
354
|
+
});
|
|
354
355
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
).
|
|
356
|
+
// Wait separately for getMetric to be called and data to render
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(djClient.DataJunctionAPI.getMetric).toHaveBeenCalledWith(
|
|
359
|
+
'default.num_repair_orders',
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Wait for metric expression to appear (SyntaxHighlighter may split text)
|
|
364
|
+
await waitFor(() => {
|
|
365
|
+
expect(screen.getByText(/count/)).toBeInTheDocument();
|
|
358
366
|
});
|
|
367
|
+
|
|
368
|
+
expect(container.getElementsByClassName('language-sql')).toMatchSnapshot();
|
|
359
369
|
}, 60000);
|
|
360
370
|
|
|
361
371
|
it('renders the NodeInfo tab correctly for cube nodes', async () => {
|
|
362
372
|
const djClient = mockDJClient();
|
|
363
373
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockCubeNode);
|
|
364
|
-
djClient.DataJunctionAPI.cube.
|
|
374
|
+
djClient.DataJunctionAPI.cube.mockResolvedValue(mocks.mockCubesCube);
|
|
365
375
|
const element = (
|
|
366
376
|
<DJClientContext.Provider value={djClient}>
|
|
367
377
|
<NodePage {...defaultProps} />
|
|
@@ -420,7 +430,7 @@ describe('<NodePage />', () => {
|
|
|
420
430
|
it('renders the NodeColumns tab correctly', async () => {
|
|
421
431
|
const djClient = mockDJClient();
|
|
422
432
|
djClient.DataJunctionAPI.node.mockResolvedValue(mocks.mockMetricNode);
|
|
423
|
-
djClient.DataJunctionAPI.getMetric.
|
|
433
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
424
434
|
mocks.mockMetricNodeJson,
|
|
425
435
|
);
|
|
426
436
|
djClient.DataJunctionAPI.columns.mockResolvedValue(mocks.metricNodeColumns);
|
|
@@ -448,7 +458,7 @@ describe('<NodePage />', () => {
|
|
|
448
458
|
);
|
|
449
459
|
await waitFor(() => {
|
|
450
460
|
expect(djClient.DataJunctionAPI.columns).toHaveBeenCalledWith(
|
|
451
|
-
mocks.mockMetricNode,
|
|
461
|
+
expect.objectContaining({ name: mocks.mockMetricNode.name }),
|
|
452
462
|
);
|
|
453
463
|
expect(
|
|
454
464
|
screen.getByRole('columnheader', { name: 'ColumnName' }),
|
|
@@ -498,7 +508,7 @@ describe('<NodePage />', () => {
|
|
|
498
508
|
it('renders the NodeHistory tab correctly', async () => {
|
|
499
509
|
const djClient = mockDJClient();
|
|
500
510
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
|
|
501
|
-
djClient.DataJunctionAPI.getMetric.
|
|
511
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
502
512
|
mocks.mockMetricNodeJson,
|
|
503
513
|
);
|
|
504
514
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
@@ -600,7 +610,7 @@ describe('<NodePage />', () => {
|
|
|
600
610
|
it('renders an empty NodeMaterialization tab correctly', async () => {
|
|
601
611
|
const djClient = mockDJClient();
|
|
602
612
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
|
|
603
|
-
djClient.DataJunctionAPI.getMetric.
|
|
613
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
604
614
|
mocks.mockMetricNodeJson,
|
|
605
615
|
);
|
|
606
616
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
@@ -640,7 +650,7 @@ describe('<NodePage />', () => {
|
|
|
640
650
|
it('renders the NodeMaterialization tab with materializations correctly', async () => {
|
|
641
651
|
const djClient = mockDJClient();
|
|
642
652
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
|
|
643
|
-
djClient.DataJunctionAPI.getMetric.
|
|
653
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
644
654
|
mocks.mockMetricNodeJson,
|
|
645
655
|
);
|
|
646
656
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
@@ -795,7 +805,7 @@ describe('<NodePage />', () => {
|
|
|
795
805
|
it('renders a NodeColumnLineage tab correctly', async () => {
|
|
796
806
|
const djClient = mockDJClient();
|
|
797
807
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
|
|
798
|
-
djClient.DataJunctionAPI.getMetric.
|
|
808
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
799
809
|
mocks.mockMetricNodeJson,
|
|
800
810
|
);
|
|
801
811
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
@@ -828,7 +838,7 @@ describe('<NodePage />', () => {
|
|
|
828
838
|
it('renders a NodeGraph tab correctly', async () => {
|
|
829
839
|
const djClient = mockDJClient();
|
|
830
840
|
djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
|
|
831
|
-
djClient.DataJunctionAPI.getMetric.
|
|
841
|
+
djClient.DataJunctionAPI.getMetric.mockResolvedValue(
|
|
832
842
|
mocks.mockMetricNodeJson,
|
|
833
843
|
);
|
|
834
844
|
djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
|
|
@@ -17,6 +17,7 @@ import NodesWithDimension from './NodesWithDimension';
|
|
|
17
17
|
import NodeColumnLineage from './NodeLineageTab';
|
|
18
18
|
import EditIcon from '../../icons/EditIcon';
|
|
19
19
|
import AlertIcon from '../../icons/AlertIcon';
|
|
20
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
20
21
|
import NodeDependenciesTab from './NodeDependenciesTab';
|
|
21
22
|
import { useNavigate } from 'react-router-dom';
|
|
22
23
|
|
|
@@ -30,7 +31,7 @@ export function NodePage() {
|
|
|
30
31
|
selectedTab: tab || 'info',
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
const [node, setNode] = useState();
|
|
34
|
+
const [node, setNode] = useState(null);
|
|
34
35
|
|
|
35
36
|
const onClickTab = id => () => {
|
|
36
37
|
navigate(`/nodes/${name}/${id}`);
|
|
@@ -52,21 +53,12 @@ export function NodePage() {
|
|
|
52
53
|
useEffect(() => {
|
|
53
54
|
const fetchData = async () => {
|
|
54
55
|
const data = await djClient.node(name);
|
|
55
|
-
data.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
data.required_dimensions = metric.current.requiredDimensions;
|
|
60
|
-
data.upstream_node = metric.current.parents[0].name;
|
|
61
|
-
data.expression = metric.current.metricMetadata.expression;
|
|
62
|
-
data.incompatible_druid_functions =
|
|
63
|
-
metric.current.metricMetadata.incompatibleDruidFunctions;
|
|
56
|
+
if (data.message !== undefined) {
|
|
57
|
+
// Error response
|
|
58
|
+
setNode(data);
|
|
59
|
+
return;
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
const cube = await djClient.cube(name);
|
|
67
|
-
data.cube_elements = cube.cube_elements;
|
|
68
|
-
}
|
|
69
|
-
setNode(data);
|
|
61
|
+
setNode({ ...data });
|
|
70
62
|
};
|
|
71
63
|
fetchData().catch(console.error);
|
|
72
64
|
}, [djClient, name]);
|
|
@@ -124,8 +116,7 @@ export function NodePage() {
|
|
|
124
116
|
|
|
125
117
|
switch (state.selectedTab) {
|
|
126
118
|
case 'info':
|
|
127
|
-
tabToDisplay =
|
|
128
|
-
node && node.message === undefined ? <NodeInfoTab node={node} /> : '';
|
|
119
|
+
tabToDisplay = node ? <NodeInfoTab node={node} /> : '';
|
|
129
120
|
break;
|
|
130
121
|
case 'columns':
|
|
131
122
|
tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
|
|
@@ -168,7 +159,7 @@ export function NodePage() {
|
|
|
168
159
|
|
|
169
160
|
<WatchButton node={node} />
|
|
170
161
|
|
|
171
|
-
<ClientCodePopover
|
|
162
|
+
<ClientCodePopover nodeName={name} />
|
|
172
163
|
{node?.type === 'cube' && <NotebookDownload node={node} />}
|
|
173
164
|
</div>
|
|
174
165
|
);
|
|
@@ -179,7 +170,11 @@ export function NodePage() {
|
|
|
179
170
|
<div className="node__header">
|
|
180
171
|
<NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
|
|
181
172
|
<div className="card">
|
|
182
|
-
{node
|
|
173
|
+
{node === undefined ? (
|
|
174
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
175
|
+
<LoadingIcon />
|
|
176
|
+
</div>
|
|
177
|
+
) : node?.message === undefined ? (
|
|
183
178
|
<div className="card-header" style={{}}>
|
|
184
179
|
<div
|
|
185
180
|
style={{
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { SettingsPage } from '../index';
|
|
4
4
|
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
import { UserProvider } from '../../../providers/UserProvider';
|
|
5
6
|
|
|
6
7
|
describe('SettingsPage', () => {
|
|
7
8
|
const mockDjClient = {
|
|
@@ -18,7 +19,9 @@ describe('SettingsPage', () => {
|
|
|
18
19
|
const renderWithContext = () => {
|
|
19
20
|
return render(
|
|
20
21
|
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
21
|
-
<
|
|
22
|
+
<UserProvider>
|
|
23
|
+
<SettingsPage />
|
|
24
|
+
</UserProvider>
|
|
22
25
|
</DJClientContext.Provider>,
|
|
23
26
|
);
|
|
24
27
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useState } from 'react';
|
|
2
2
|
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import { useCurrentUser } from '../../providers/UserProvider';
|
|
3
4
|
import LoadingIcon from '../../icons/LoadingIcon';
|
|
4
5
|
import ProfileSection from './ProfileSection';
|
|
5
6
|
import NotificationSubscriptionsSection from './NotificationSubscriptionsSection';
|
|
@@ -11,18 +12,17 @@ import '../../../styles/settings.css';
|
|
|
11
12
|
*/
|
|
12
13
|
export function SettingsPage() {
|
|
13
14
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
-
const
|
|
15
|
+
const { currentUser, loading: userLoading } = useCurrentUser();
|
|
15
16
|
const [subscriptions, setSubscriptions] = useState([]);
|
|
16
17
|
const [serviceAccounts, setServiceAccounts] = useState([]);
|
|
17
18
|
const [loading, setLoading] = useState(true);
|
|
18
19
|
|
|
19
20
|
useEffect(() => {
|
|
21
|
+
// Wait for user to be loaded from context
|
|
22
|
+
if (userLoading) return;
|
|
23
|
+
|
|
20
24
|
async function fetchData() {
|
|
21
25
|
try {
|
|
22
|
-
// Fetch user profile
|
|
23
|
-
const user = await djClient.whoami();
|
|
24
|
-
setCurrentUser(user);
|
|
25
|
-
|
|
26
26
|
// Fetch notification subscriptions
|
|
27
27
|
const prefs = await djClient.getNotificationPreferences();
|
|
28
28
|
|
|
@@ -69,7 +69,7 @@ export function SettingsPage() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
fetchData();
|
|
72
|
-
}, [djClient]);
|
|
72
|
+
}, [djClient, userLoading]);
|
|
73
73
|
|
|
74
74
|
// Subscription handlers
|
|
75
75
|
const handleUpdateSubscription = async (sub, activityTypes) => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import DJClientContext from './djclient';
|
|
10
|
+
|
|
11
|
+
interface User {
|
|
12
|
+
id?: number;
|
|
13
|
+
username?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
last_viewed_notifications_at?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UserContextType {
|
|
20
|
+
currentUser: User | null;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
refetchUser: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const UserContext = createContext<UserContextType>({
|
|
27
|
+
currentUser: null,
|
|
28
|
+
loading: true,
|
|
29
|
+
error: null,
|
|
30
|
+
refetchUser: async () => {},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export function UserProvider({ children }: { children: React.ReactNode }) {
|
|
34
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
35
|
+
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<Error | null>(null);
|
|
38
|
+
|
|
39
|
+
const fetchUser = useCallback(async () => {
|
|
40
|
+
try {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
const user = await djClient.whoami();
|
|
44
|
+
setCurrentUser(user);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch user'));
|
|
47
|
+
console.error('Error fetching user:', err);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, [djClient]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
fetchUser();
|
|
55
|
+
}, [fetchUser]);
|
|
56
|
+
|
|
57
|
+
const value = useMemo(
|
|
58
|
+
() => ({
|
|
59
|
+
currentUser,
|
|
60
|
+
loading,
|
|
61
|
+
error,
|
|
62
|
+
refetchUser: fetchUser,
|
|
63
|
+
}),
|
|
64
|
+
[currentUser, loading, error, fetchUser],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useCurrentUser() {
|
|
71
|
+
const context = useContext(UserContext);
|
|
72
|
+
if (context === undefined) {
|
|
73
|
+
throw new Error('useCurrentUser must be used within a UserProvider');
|
|
74
|
+
}
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default UserContext;
|