11import { css } from '@emotion/css' ;
22import { PureComponent } from 'react' ;
33import * as React from 'react' ;
4+ import type { Unsubscribable } from 'rxjs' ;
45
5- import { FeatureState , getBuiltInThemes , ThemeRegistryItem } from '@grafana/data' ;
6+ import { FeatureState , getBuiltInThemes , ThemeRegistryItem , type GrafanaTheme2 } from '@grafana/data' ;
67import { selectors } from '@grafana/e2e-selectors' ;
7- import { config , reportInteraction } from '@grafana/runtime' ;
8+ import { config , reportInteraction , getAppEvents , ThemeChangedEvent , type BusEventWithPayload } from '@grafana/runtime' ;
89import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen' ;
910import {
1011 Button ,
@@ -26,6 +27,7 @@ import { t, Trans } from 'app/core/internationalization';
2627import { LANGUAGES , PSEUDO_LOCALE } from 'app/core/internationalization/constants' ;
2728import { PreferencesService } from 'app/core/services/PreferencesService' ;
2829import { changeTheme } from 'app/core/services/theme' ;
30+
2931export interface Props {
3032 resourceUri : string ;
3133 disabled ?: boolean ;
@@ -67,6 +69,7 @@ function getLanguageOptions(): ComboboxOption[] {
6769export class SharedPreferences extends PureComponent < Props , State > {
6870 service : PreferencesService ;
6971 themeOptions : ComboboxOption [ ] ;
72+ private themeChangedSub ?: Unsubscribable ;
7073
7174 constructor ( props : Props ) {
7275 super ( props ) ;
@@ -121,6 +124,33 @@ export class SharedPreferences extends PureComponent<Props, State> {
121124 queryHistory : prefs . queryHistory ,
122125 navbar : prefs . navbar ,
123126 } ) ;
127+
128+ // Subscribe to theme changes to keep the dropdown in sync with the actual theme.
129+ // This ensures the dropdown reflects theme changes from any source (system preferences,
130+ // other UI components, etc.), not just from this component's dropdown selection.
131+ const eventBus = getAppEvents ( ) ;
132+ if ( eventBus && typeof eventBus . subscribe === 'function' ) {
133+ this . themeChangedSub = eventBus . subscribe ( ThemeChangedEvent , ( evt : BusEventWithPayload < GrafanaTheme2 > ) => {
134+ try {
135+ const newTheme = evt . payload ;
136+ const mode = newTheme . colors . mode ;
137+
138+ if ( this . state . theme !== mode ) {
139+ this . setState ( { theme : mode } ) ;
140+ }
141+ } catch ( err ) {
142+ console . warn ( '[SharedPreferences] Failed to sync theme from ThemeChangedEvent:' , err ) ;
143+ }
144+ } ) ;
145+ }
146+ }
147+
148+ componentWillUnmount ( ) {
149+ try {
150+ this . themeChangedSub ?. unsubscribe ( ) ;
151+ } catch ( err ) {
152+ console . warn ( '[SharedPreferences] Failed to unsubscribe ThemeChangedEvent:' , err ) ;
153+ }
124154 }
125155
126156 onSubmitForm = async ( event : React . FormEvent < HTMLFormElement > ) => {
@@ -135,7 +165,9 @@ export class SharedPreferences extends PureComponent<Props, State> {
135165 } ;
136166
137167 onThemeChanged = ( value : ComboboxOption < string > ) => {
168+ // Update state immediately so the form has the correct value when saving
138169 this . setState ( { theme : value . value } ) ;
170+
139171 reportInteraction ( 'grafana_preferences_theme_changed' , {
140172 toTheme : value . value ,
141173 preferenceType : this . props . preferenceType ,
@@ -144,6 +176,9 @@ export class SharedPreferences extends PureComponent<Props, State> {
144176 if ( value . value ) {
145177 changeTheme ( value . value , true ) ;
146178 }
179+ // Note: setState is called twice - once here for immediate form state update,
180+ // and again via ThemeChangedEvent subscription after CSS loads. This is intentional
181+ // to ensure form correctness while also maintaining sync with rendered theme.
147182 } ;
148183
149184 onTimeZoneChanged = ( timezone ?: string ) => {
0 commit comments