diff --git a/ui/apps/pmm/src/App.tsx b/ui/apps/pmm/src/App.tsx index bea3a456fed..6136167f385 100644 --- a/ui/apps/pmm/src/App.tsx +++ b/ui/apps/pmm/src/App.tsx @@ -17,10 +17,7 @@ const queryClient = new QueryClient({ }); const App = () => ( - + = ({ children }) => { + useThemeSync(); + return <>{children}; +}; const Providers: FC = () => ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); diff --git a/ui/apps/pmm/src/api/user.ts b/ui/apps/pmm/src/api/user.ts index 206eac7c4ef..5ad32fe407a 100644 --- a/ui/apps/pmm/src/api/user.ts +++ b/ui/apps/pmm/src/api/user.ts @@ -1,4 +1,5 @@ import { + GetPreferenceResponse, GetUserResponse, UpdatePreferencesBody, UpdateUserInfoPayload, @@ -17,6 +18,11 @@ export const getCurrentUserOrgs = async () => { return res.data; }; +export const getUserPreferences = async () => { + const res = await grafanaApi.get('/user/preferences'); + return res.data; +}; + export const updatePreferences = async ( preferences: Partial ) => { diff --git a/ui/apps/pmm/src/hooks/api/useUser.ts b/ui/apps/pmm/src/hooks/api/useUser.ts index 8220c4ace42..28c576ede5e 100644 --- a/ui/apps/pmm/src/hooks/api/useUser.ts +++ b/ui/apps/pmm/src/hooks/api/useUser.ts @@ -9,11 +9,13 @@ import { getCurrentUser, getCurrentUserOrgs, getUserInfo, + getUserPreferences, updatePreferences, updateUserInfo, } from 'api/user'; import { ApiError } from 'types/api.types'; import { + GetPreferenceResponse, GetUserResponse, UpdatePreferencesBody, UpdateUserInfoPayload, @@ -60,6 +62,15 @@ export const useCurrentUserOrgs = ( ...options, }); +export const useUserPreferences = ( + options?: Partial> +) => + useQuery({ + queryKey: ['user:preferences'], + queryFn: () => getUserPreferences(), + ...options, + }); + export const useUpdatePreferences = ( options?: Partial> ) => diff --git a/ui/apps/pmm/src/hooks/useThemeSync.ts b/ui/apps/pmm/src/hooks/useThemeSync.ts new file mode 100644 index 00000000000..716c1ad9a65 --- /dev/null +++ b/ui/apps/pmm/src/hooks/useThemeSync.ts @@ -0,0 +1,59 @@ +import { useEffect, useRef } from 'react'; +import { useUserPreferences } from './api/useUser'; +import { useColorMode } from './theme'; +import { useAuth } from 'contexts/auth'; +import { ColorMode } from '@pmm/shared'; + +const DEFAULT_THEME: ColorMode = 'dark'; + +/** + * Synchronizes user theme from Grafana API on initial load. + * Fixes PMM-14624 where new users inherit theme from localStorage. + * + * Problem: /api/user returns empty theme field, and ThemeContextProvider + * was using localStorage causing theme inheritance between users. + * + * Solution: Load theme from /api/user/preferences on login and apply + * via setFromGrafana (without broadcast/persist to avoid loops). + */ +export const useThemeSync = () => { + const auth = useAuth(); + const { + data: preferences, + isLoading, + error, + } = useUserPreferences({ + enabled: auth.isLoggedIn, + }); + const { setFromGrafana } = useColorMode(); + const syncedRef = useRef(false); + + // Reset synced flag when user logs out + useEffect(() => { + if (!auth.isLoggedIn) { + syncedRef.current = false; + } + }, [auth.isLoggedIn]); + + useEffect(() => { + if (!auth.isLoggedIn || isLoading || syncedRef.current || !preferences) { + return; + } + + if (error) { + console.error('[useThemeSync] Failed to load user preferences:', error); + return; + } + + const themeToApply = preferences.theme || DEFAULT_THEME; + + // Apply theme from preferences + setFromGrafana(themeToApply) + .then(() => { + syncedRef.current = true; + }) + .catch((err: unknown) => { + console.error('[useThemeSync] Failed to apply theme:', err); + }); + }, [preferences, isLoading, error, setFromGrafana, auth.isLoggedIn]); +}; diff --git a/ui/apps/pmm/src/types/user.types.ts b/ui/apps/pmm/src/types/user.types.ts index bcb1f76c2b6..3fb62092e8e 100644 --- a/ui/apps/pmm/src/types/user.types.ts +++ b/ui/apps/pmm/src/types/user.types.ts @@ -48,6 +48,8 @@ export interface UpdatePreferencesBody { theme: ColorMode; } +export type GetPreferenceResponse = UpdatePreferencesBody; + export interface UserInfo { userId: number; alertingTourCompleted: boolean;