diff --git a/app/portainer/services/api/userService.js b/app/portainer/services/api/userService.js index 4201c9b38..705c3f065 100644 --- a/app/portainer/services/api/userService.js +++ b/app/portainer/services/api/userService.js @@ -1,6 +1,8 @@ import _ from 'lodash-es'; + import { UserTokenModel, UserViewModel } from '@/portainer/models/user'; -import { getUser, getUsers } from '@/portainer/users/user.service'; +import { getUsers } from '@/portainer/users/user.service'; +import { getUser } from '@/portainer/users/queries/useUser'; import { TeamMembershipModel } from '../../models/teamMembership'; @@ -15,8 +17,8 @@ export function UserService($q, Users, TeamService, TeamMembershipService) { return users.map((u) => new UserViewModel(u)); }; - service.user = async function (includeAdministrators) { - const user = await getUser(includeAdministrators); + service.user = async function (userId) { + const user = await getUser(userId); return new UserViewModel(user); }; diff --git a/app/portainer/users/queries/useUser.ts b/app/portainer/users/queries/useUser.ts new file mode 100644 index 000000000..ec1915f5b --- /dev/null +++ b/app/portainer/users/queries/useUser.ts @@ -0,0 +1,27 @@ +import { useQuery } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { withError } from '@/react-tools/react-query'; + +import { buildUrl } from '../user.service'; +import { User, UserId } from '../types'; + +export function useUser( + id: UserId, + { staleTime }: { staleTime?: number } = {} +) { + return useQuery(['users', id], () => getUser(id), { + ...withError('Unable to retrieve user details'), + staleTime, + }); +} + +export async function getUser(id: UserId) { + try { + const { data: user } = await axios.get(buildUrl(id)); + + return user; + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to retrieve user details'); + } +} diff --git a/app/portainer/users/user.service.ts b/app/portainer/users/user.service.ts index bef14691f..de64058dd 100644 --- a/app/portainer/users/user.service.ts +++ b/app/portainer/users/user.service.ts @@ -19,16 +19,6 @@ export async function getUsers( } } -export async function getUser(id: UserId) { - try { - const { data: user } = await axios.get(buildUrl(id)); - - return user; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to retrieve user details'); - } -} - export async function getUserMemberships(id: UserId) { try { const { data } = await axios.get( @@ -40,7 +30,7 @@ export async function getUserMemberships(id: UserId) { } } -function buildUrl(id?: UserId, entity?: string) { +export function buildUrl(id?: UserId, entity?: string) { let url = '/users'; if (id) { diff --git a/app/react-tools/withCurrentUser.tsx b/app/react-tools/withCurrentUser.tsx index ca8bd3391..bd0daf42f 100644 --- a/app/react-tools/withCurrentUser.tsx +++ b/app/react-tools/withCurrentUser.tsx @@ -2,6 +2,8 @@ import { ComponentType } from 'react'; import { UserProvider } from '@/react/hooks/useUser'; +import { withReactQuery } from './withReactQuery'; + export function withCurrentUser( WrappedComponent: ComponentType ): ComponentType { @@ -12,13 +14,14 @@ export function withCurrentUser( function WrapperComponent(props: T) { return ( - {/* eslint-disable-next-line react/jsx-props-no-spreading */} ); } - WrapperComponent.displayName = displayName; + WrapperComponent.displayName = `withCurrentUser(${displayName})`; - return WrapperComponent; + // User provider makes a call to the API to get the current user. + // We need to wrap it with React Query to make that call. + return withReactQuery(WrapperComponent); } diff --git a/app/react-tools/withI18nSuspense.tsx b/app/react-tools/withI18nSuspense.tsx index 747773e2b..b0e12304e 100644 --- a/app/react-tools/withI18nSuspense.tsx +++ b/app/react-tools/withI18nSuspense.tsx @@ -10,13 +10,12 @@ export function withI18nSuspense( function WrapperComponent(props: T) { return ( - {/* eslint-disable-next-line react/jsx-props-no-spreading */} ); } - WrapperComponent.displayName = displayName; + WrapperComponent.displayName = `withI18nSuspense(${displayName})`; return WrapperComponent; } diff --git a/app/react-tools/withReactQuery.tsx b/app/react-tools/withReactQuery.tsx index 0502f47a2..1cc326c4a 100644 --- a/app/react-tools/withReactQuery.tsx +++ b/app/react-tools/withReactQuery.tsx @@ -14,13 +14,12 @@ export function withReactQuery( function WrapperComponent(props: T) { return ( - {/* eslint-disable-next-line react/jsx-props-no-spreading */} ); } - WrapperComponent.displayName = displayName; + WrapperComponent.displayName = `withReactQuery(${displayName})`; return WrapperComponent; } diff --git a/app/react-tools/withUIRouter.tsx b/app/react-tools/withUIRouter.tsx index 761b8b442..b85b0c3a3 100644 --- a/app/react-tools/withUIRouter.tsx +++ b/app/react-tools/withUIRouter.tsx @@ -11,13 +11,12 @@ export function withUIRouter( function WrapperComponent(props: T) { return ( - {/* eslint-disable-next-line react/jsx-props-no-spreading */} ); } - WrapperComponent.displayName = displayName; + WrapperComponent.displayName = `withUIRouter(${displayName})`; return WrapperComponent; } diff --git a/app/react/hooks/useUser.tsx b/app/react/hooks/useUser.tsx index 97e6ad06b..430568a48 100644 --- a/app/react/hooks/useUser.tsx +++ b/app/react/hooks/useUser.tsx @@ -4,16 +4,14 @@ import { createContext, ReactNode, useContext, - useEffect, - useState, useMemo, PropsWithChildren, } from 'react'; import { isAdmin } from '@/portainer/users/user.helpers'; import { EnvironmentId } from '@/react/portainer/environments/types'; -import { getUser } from '@/portainer/users/user.service'; -import { User, UserId } from '@/portainer/users/types'; +import { User } from '@/portainer/users/types'; +import { useUser as useLoadUser } from '@/portainer/users/queries/useUser'; import { useLocalStorage } from './useLocalStorage'; @@ -24,7 +22,12 @@ interface State { export const UserContext = createContext(null); UserContext.displayName = 'UserContext'; -export function useUser() { +/** + * @deprecated use `useCurrentUser` instead + */ +export const useUser = useCurrentUser; + +export function useCurrentUser() { const context = useContext(UserContext); if (context === null) { @@ -147,23 +150,19 @@ interface UserProviderProps { export function UserProvider({ children }: UserProviderProps) { const [jwt] = useLocalStorage('JWT', ''); - const [user, setUser] = useState(); - - useEffect(() => { - if (jwt !== '') { - const tokenPayload = jwtDecode(jwt) as { id: number }; - loadUser(tokenPayload.id); - } - }, [jwt]); + const tokenPayload = useMemo(() => jwtDecode(jwt) as { id: number }, [jwt]); - const providerState = useMemo(() => ({ user }), [user]); + const userQuery = useLoadUser(tokenPayload.id, { + staleTime: Infinity, // should reload te user details only on page load + }); - if (jwt === '') { - return null; - } + const providerState = useMemo( + () => ({ user: userQuery.data }), + [userQuery.data] + ); - if (!providerState.user) { + if (jwt === '' || !providerState.user) { return null; } @@ -172,9 +171,4 @@ export function UserProvider({ children }: UserProviderProps) { {children} ); - - async function loadUser(id: UserId) { - const user = await getUser(id); - setUser(user); - } }