Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions ui/apps/pmm/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ const queryClient = new QueryClient({
});

const App = () => (
<ThemeContextProvider
themeOptions={pmmThemeOptions}
saveColorModeOnLocalStorage
>
<ThemeContextProvider themeOptions={pmmThemeOptions}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<SnackbarProvider
maxSnack={3}
Expand Down
50 changes: 29 additions & 21 deletions ui/apps/pmm/src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,39 @@ import { UpdatesProvider } from 'contexts/updates';
import { UserProvider } from 'contexts/user';
import { FC, PropsWithChildren } from 'react';
import { Outlet } from 'react-router-dom';
import { useThemeSync } from 'hooks/useThemeSync';

const ProvidersContent: FC<PropsWithChildren> = ({ children }) => {
useThemeSync();
return <>{children}</>;
};

const Providers: FC<PropsWithChildren> = () => (
<AuthProvider>
<UserProvider>
<SettingsProvider>
<UpdatesProvider>
<GrafanaProvider>
<NavigationProvider>
<TourProvider>
<GlobalStyles
styles={{
'html, body, div#root': {
minHeight: '100vh',
},
'div#root': {
display: 'flex',
},
}}
/>
<Outlet />
</TourProvider>
</NavigationProvider>
</GrafanaProvider>
</UpdatesProvider>
</SettingsProvider>
<ProvidersContent>
<SettingsProvider>
<UpdatesProvider>
<GrafanaProvider>
<NavigationProvider>
<TourProvider>
<GlobalStyles
styles={{
'html, body, div#root': {
minHeight: '100vh',
},
'div#root': {
display: 'flex',
},
}}
/>
<Outlet />
</TourProvider>
</NavigationProvider>
</GrafanaProvider>
</UpdatesProvider>
</SettingsProvider>
</ProvidersContent>
</UserProvider>
</AuthProvider>
);
Expand Down
5 changes: 5 additions & 0 deletions ui/apps/pmm/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export const getCurrentUserOrgs = async () => {
return res.data;
};

export const getUserPreferences = async () => {
const res = await grafanaApi.get<UpdatePreferencesBody>('/user/preferences');
return res.data;
};

export const updatePreferences = async (
preferences: Partial<UpdatePreferencesBody>
) => {
Expand Down
10 changes: 10 additions & 0 deletions ui/apps/pmm/src/hooks/api/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getCurrentUser,
getCurrentUserOrgs,
getUserInfo,
getUserPreferences,
updatePreferences,
updateUserInfo,
} from 'api/user';
Expand Down Expand Up @@ -60,6 +61,15 @@ export const useCurrentUserOrgs = (
...options,
});

export const useUserPreferences = (
options?: Partial<UseQueryOptions<UpdatePreferencesBody>>
) =>
useQuery({
queryKey: ['user:preferences'],
queryFn: () => getUserPreferences(),
...options,
});

export const useUpdatePreferences = (
options?: Partial<UseMutationOptions<void, ApiError, UpdatePreferencesBody>>
) =>
Expand Down
59 changes: 59 additions & 0 deletions ui/apps/pmm/src/hooks/useThemeSync.ts
Original file line number Diff line number Diff line change
@@ -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]);
};
Loading