From b88a0d82201ba4a0bc15e0f30f4c226e5d9a08cc Mon Sep 17 00:00:00 2001 From: dmitri-saricev-3pillargloball Date: Thu, 11 Dec 2025 16:17:43 +0200 Subject: [PATCH 1/4] PMM-14624: Fix theme inheritance between users Remove localStorage theme persistence and sync theme from Grafana API on login to prevent new users from inheriting previous user's theme preferences. --- ui/apps/pmm/src/App.tsx | 5 +-- ui/apps/pmm/src/Providers.tsx | 50 +++++++++++++---------- ui/apps/pmm/src/api/user.ts | 5 +++ ui/apps/pmm/src/hooks/api/useUser.ts | 10 +++++ ui/apps/pmm/src/hooks/useThemeSync.ts | 59 +++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 ui/apps/pmm/src/hooks/useThemeSync.ts 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..b7ed2dec367 100644 --- a/ui/apps/pmm/src/api/user.ts +++ b/ui/apps/pmm/src/api/user.ts @@ -17,6 +17,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..50249babe74 100644 --- a/ui/apps/pmm/src/hooks/api/useUser.ts +++ b/ui/apps/pmm/src/hooks/api/useUser.ts @@ -9,6 +9,7 @@ import { getCurrentUser, getCurrentUserOrgs, getUserInfo, + getUserPreferences, updatePreferences, updateUserInfo, } from 'api/user'; @@ -60,6 +61,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]); +}; From 018427dd54fc8ee79345bf0cef1633329686b2a7 Mon Sep 17 00:00:00 2001 From: dmitri-saricev-3pillargloball Date: Wed, 17 Dec 2025 15:47:43 +0200 Subject: [PATCH 2/4] change provider name --- ui/apps/pmm/src/Providers.tsx | 6 +++--- ui/apps/pmm/src/api/user.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/apps/pmm/src/Providers.tsx b/ui/apps/pmm/src/Providers.tsx index 52dc0ffe0c4..cd461c978a0 100644 --- a/ui/apps/pmm/src/Providers.tsx +++ b/ui/apps/pmm/src/Providers.tsx @@ -10,7 +10,7 @@ import { FC, PropsWithChildren } from 'react'; import { Outlet } from 'react-router-dom'; import { useThemeSync } from 'hooks/useThemeSync'; -const ProvidersContent: FC = ({ children }) => { +const ThemedyProvider: FC = ({ children }) => { useThemeSync(); return <>{children}; }; @@ -18,7 +18,7 @@ const ProvidersContent: FC = ({ children }) => { const Providers: FC = () => ( - + @@ -40,7 +40,7 @@ const Providers: FC = () => ( - + ); diff --git a/ui/apps/pmm/src/api/user.ts b/ui/apps/pmm/src/api/user.ts index b7ed2dec367..4a227fcac99 100644 --- a/ui/apps/pmm/src/api/user.ts +++ b/ui/apps/pmm/src/api/user.ts @@ -7,6 +7,8 @@ import { } from 'types/user.types'; import { api, grafanaApi } from './api'; +export type GetPreferenceResponse = UpdatePreferencesBody; + export const getCurrentUser = async () => { const res = await grafanaApi.get('/user'); return res.data; From bce2068ba0ba9d7ad935313a93ed75960218b70c Mon Sep 17 00:00:00 2001 From: dmitri-saricev-3pillargloball Date: Wed, 17 Dec 2025 16:01:04 +0200 Subject: [PATCH 3/4] Fix PR review comments - Move GetPreferenceResponse type to user.types.ts - Update useUser.ts to use GetPreferenceResponse type - Rename ThemedyProvider to ThemeSyncProvider --- ui/apps/pmm/src/Providers.tsx | 6 +++--- ui/apps/pmm/src/api/user.ts | 2 -- ui/apps/pmm/src/hooks/api/useUser.ts | 3 ++- ui/apps/pmm/src/types/user.types.ts | 2 ++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui/apps/pmm/src/Providers.tsx b/ui/apps/pmm/src/Providers.tsx index cd461c978a0..2750979fc6a 100644 --- a/ui/apps/pmm/src/Providers.tsx +++ b/ui/apps/pmm/src/Providers.tsx @@ -10,7 +10,7 @@ import { FC, PropsWithChildren } from 'react'; import { Outlet } from 'react-router-dom'; import { useThemeSync } from 'hooks/useThemeSync'; -const ThemedyProvider: FC = ({ children }) => { +const ThemeSyncProvider: FC = ({ children }) => { useThemeSync(); return <>{children}; }; @@ -18,7 +18,7 @@ const ThemedyProvider: FC = ({ children }) => { const Providers: FC = () => ( - + @@ -40,7 +40,7 @@ const Providers: FC = () => ( - + ); diff --git a/ui/apps/pmm/src/api/user.ts b/ui/apps/pmm/src/api/user.ts index 4a227fcac99..b7ed2dec367 100644 --- a/ui/apps/pmm/src/api/user.ts +++ b/ui/apps/pmm/src/api/user.ts @@ -7,8 +7,6 @@ import { } from 'types/user.types'; import { api, grafanaApi } from './api'; -export type GetPreferenceResponse = UpdatePreferencesBody; - export const getCurrentUser = async () => { const res = await grafanaApi.get('/user'); return res.data; diff --git a/ui/apps/pmm/src/hooks/api/useUser.ts b/ui/apps/pmm/src/hooks/api/useUser.ts index 50249babe74..28c576ede5e 100644 --- a/ui/apps/pmm/src/hooks/api/useUser.ts +++ b/ui/apps/pmm/src/hooks/api/useUser.ts @@ -15,6 +15,7 @@ import { } from 'api/user'; import { ApiError } from 'types/api.types'; import { + GetPreferenceResponse, GetUserResponse, UpdatePreferencesBody, UpdateUserInfoPayload, @@ -62,7 +63,7 @@ export const useCurrentUserOrgs = ( }); export const useUserPreferences = ( - options?: Partial> + options?: Partial> ) => useQuery({ queryKey: ['user:preferences'], 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; From 597a3e47dec77167c11051e2737990ee84d7be6b Mon Sep 17 00:00:00 2001 From: dmitri-saricev-3pillargloball Date: Thu, 18 Dec 2025 10:09:45 +0200 Subject: [PATCH 4/4] Use GetPreferenceResponse in getUserPreferences --- ui/apps/pmm/src/api/user.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/apps/pmm/src/api/user.ts b/ui/apps/pmm/src/api/user.ts index b7ed2dec367..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, @@ -18,7 +19,7 @@ export const getCurrentUserOrgs = async () => { }; export const getUserPreferences = async () => { - const res = await grafanaApi.get('/user/preferences'); + const res = await grafanaApi.get('/user/preferences'); return res.data; };