refactor(auth): cache user data [EE-4935] (#8380)

pull/8407/head
Chaim Lev-Ari 2023-01-26 07:40:05 +05:30 committed by GitHub
parent a748e15c16
commit 00bbf4ac63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 59 additions and 46 deletions

View File

@ -1,6 +1,8 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
import { UserTokenModel, UserViewModel } from '@/portainer/models/user'; 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'; import { TeamMembershipModel } from '../../models/teamMembership';
@ -15,8 +17,8 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
return users.map((u) => new UserViewModel(u)); return users.map((u) => new UserViewModel(u));
}; };
service.user = async function (includeAdministrators) { service.user = async function (userId) {
const user = await getUser(includeAdministrators); const user = await getUser(userId);
return new UserViewModel(user); return new UserViewModel(user);
}; };

View File

@ -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<User>(buildUrl(id));
return user;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve user details');
}
}

View File

@ -19,16 +19,6 @@ export async function getUsers(
} }
} }
export async function getUser(id: UserId) {
try {
const { data: user } = await axios.get<User>(buildUrl(id));
return user;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve user details');
}
}
export async function getUserMemberships(id: UserId) { export async function getUserMemberships(id: UserId) {
try { try {
const { data } = await axios.get<TeamMembership[]>( const { data } = await axios.get<TeamMembership[]>(
@ -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'; let url = '/users';
if (id) { if (id) {

View File

@ -2,6 +2,8 @@ import { ComponentType } from 'react';
import { UserProvider } from '@/react/hooks/useUser'; import { UserProvider } from '@/react/hooks/useUser';
import { withReactQuery } from './withReactQuery';
export function withCurrentUser<T>( export function withCurrentUser<T>(
WrappedComponent: ComponentType<T> WrappedComponent: ComponentType<T>
): ComponentType<T> { ): ComponentType<T> {
@ -12,13 +14,14 @@ export function withCurrentUser<T>(
function WrapperComponent(props: T) { function WrapperComponent(props: T) {
return ( return (
<UserProvider> <UserProvider>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} /> <WrappedComponent {...props} />
</UserProvider> </UserProvider>
); );
} }
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);
} }

View File

@ -10,13 +10,12 @@ export function withI18nSuspense<T>(
function WrapperComponent(props: T) { function WrapperComponent(props: T) {
return ( return (
<Suspense fallback="Loading translations..."> <Suspense fallback="Loading translations...">
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} /> <WrappedComponent {...props} />
</Suspense> </Suspense>
); );
} }
WrapperComponent.displayName = displayName; WrapperComponent.displayName = `withI18nSuspense(${displayName})`;
return WrapperComponent; return WrapperComponent;
} }

View File

@ -14,13 +14,12 @@ export function withReactQuery<T>(
function WrapperComponent(props: T) { function WrapperComponent(props: T) {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} /> <WrappedComponent {...props} />
</QueryClientProvider> </QueryClientProvider>
); );
} }
WrapperComponent.displayName = displayName; WrapperComponent.displayName = `withReactQuery(${displayName})`;
return WrapperComponent; return WrapperComponent;
} }

View File

@ -11,13 +11,12 @@ export function withUIRouter<T>(
function WrapperComponent(props: T) { function WrapperComponent(props: T) {
return ( return (
<UIRouterContextComponent> <UIRouterContextComponent>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} /> <WrappedComponent {...props} />
</UIRouterContextComponent> </UIRouterContextComponent>
); );
} }
WrapperComponent.displayName = displayName; WrapperComponent.displayName = `withUIRouter(${displayName})`;
return WrapperComponent; return WrapperComponent;
} }

View File

@ -4,16 +4,14 @@ import {
createContext, createContext,
ReactNode, ReactNode,
useContext, useContext,
useEffect,
useState,
useMemo, useMemo,
PropsWithChildren, PropsWithChildren,
} from 'react'; } from 'react';
import { isAdmin } from '@/portainer/users/user.helpers'; import { isAdmin } from '@/portainer/users/user.helpers';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { getUser } from '@/portainer/users/user.service'; import { User } from '@/portainer/users/types';
import { User, UserId } from '@/portainer/users/types'; import { useUser as useLoadUser } from '@/portainer/users/queries/useUser';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
@ -24,7 +22,12 @@ interface State {
export const UserContext = createContext<State | null>(null); export const UserContext = createContext<State | null>(null);
UserContext.displayName = 'UserContext'; UserContext.displayName = 'UserContext';
export function useUser() { /**
* @deprecated use `useCurrentUser` instead
*/
export const useUser = useCurrentUser;
export function useCurrentUser() {
const context = useContext(UserContext); const context = useContext(UserContext);
if (context === null) { if (context === null) {
@ -147,23 +150,19 @@ interface UserProviderProps {
export function UserProvider({ children }: UserProviderProps) { export function UserProvider({ children }: UserProviderProps) {
const [jwt] = useLocalStorage('JWT', ''); const [jwt] = useLocalStorage('JWT', '');
const [user, setUser] = useState<User>();
useEffect(() => { const tokenPayload = useMemo(() => jwtDecode(jwt) as { id: number }, [jwt]);
if (jwt !== '') {
const tokenPayload = jwtDecode(jwt) as { id: number };
loadUser(tokenPayload.id); const userQuery = useLoadUser(tokenPayload.id, {
} staleTime: Infinity, // should reload te user details only on page load
}, [jwt]); });
const providerState = useMemo(() => ({ user }), [user]); const providerState = useMemo(
() => ({ user: userQuery.data }),
[userQuery.data]
);
if (jwt === '') { if (jwt === '' || !providerState.user) {
return null;
}
if (!providerState.user) {
return null; return null;
} }
@ -172,9 +171,4 @@ export function UserProvider({ children }: UserProviderProps) {
{children} {children}
</UserContext.Provider> </UserContext.Provider>
); );
async function loadUser(id: UserId) {
const user = await getUser(id);
setUser(user);
}
} }