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
3 changes: 3 additions & 0 deletions assets/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions docs/operator-manual/argocd-cm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ data:
# - Files may also be loaded from remote locations via fully qualified URLs.
ui.cssurl: "./custom/my-styles.css"

# The default theme for the UI. This theme will be applied when users first access the UI.
# Users can still change their theme preference, which will be stored in their browser localStorage.
# Valid values: "light", "dark", "auto"
# - "light": Light theme (default if not specified)
# - "dark": Dark theme
# - "auto": Automatically match the user's system preference
ui.defaulttheme: "auto"

# An optional user-defined banner message that's displayed at the top of every UI page.
# Every time this is updated, it will clear a user's localStorage telling the UI to hide the banner forever.
ui.bannercontent: "Hello there!"
Expand Down
14 changes: 14 additions & 0 deletions docs/operator-manual/ui-customization.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# UI Customization

## Default Theme

You can configure the default theme for the ArgoCD UI by setting the `ui.defaulttheme` key in the [argocd-cm](argocd-cm-yaml.md) ConfigMap.

The following configuration:
```yaml
ui.defaulttheme: "auto"
```

**Behavior:**
- First-time users will see the theme specified in `ui.defaulttheme`
- Existing users will continue to see their previously selected theme (stored in browser localStorage)
- Users can change their theme preference at any time, which will be persisted in their browser

## Default Application Details View

By default, the Application Details will show the `Tree` view.
Expand Down
222 changes: 138 additions & 84 deletions pkg/apiclient/settings/settings.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions server/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func (s *Server) Get(ctx context.Context, _ *settingspkg.SettingsQuery) (*settin
UserLoginsDisabled: userLoginsDisabled,
KustomizeVersions: kustomizeVersions,
UiCssURL: argoCDSettings.UiCssURL,
UiDefaultTheme: argoCDSettings.UiDefaultTheme,
TrackingMethod: trackingMethod,
InstallationID: installationID,
ExecEnabled: argoCDSettings.ExecEnabled,
Expand Down
1 change: 1 addition & 0 deletions server/settings/settings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ message Settings {
string uiBannerPosition = 20;
string statusBadgeRootUrl = 21;
bool execEnabled = 22;
string uiDefaultTheme = 30;
string controllerNamespace = 23;
bool appsInAnyNamespaceEnabled = 24;
bool impersonationEnabled = 25;
Expand Down
25 changes: 25 additions & 0 deletions server/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,29 @@ func TestSettingsServer(t *testing.T) {
assert.NotNil(t, resp.ResourceOverrides)
assert.NotEmpty(t, resp.ResourceOverrides["*/*"])
})

t.Run("TestGetUiDefaultTheme", func(t *testing.T) {
settingsServer := newServer(map[string]string{
"ui.defaulttheme": "dark",
})
resp, err := settingsServer.Get(t.Context(), nil)
require.NoError(t, err)
assert.Equal(t, "dark", resp.UiDefaultTheme)
})

t.Run("TestGetUiDefaultThemeAuto", func(t *testing.T) {
settingsServer := newServer(map[string]string{
"ui.defaulttheme": "auto",
})
resp, err := settingsServer.Get(t.Context(), nil)
require.NoError(t, err)
assert.Equal(t, "auto", resp.UiDefaultTheme)
})

t.Run("TestGetUiDefaultThemeNotSet", func(t *testing.T) {
settingsServer := newServer(map[string]string{})
resp, err := settingsServer.Get(t.Context(), nil)
require.NoError(t, err)
assert.Empty(t, resp.UiDefaultTheme)
})
}
3 changes: 3 additions & 0 deletions ui/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export class App extends React.Component<{}, {popupProps: PopupProps; showVersio
this.unauthorizedSubscription = subscription;
});
const authSettings = await services.authService.settings();
if (authSettings.uiDefaultTheme) {
services.viewPreferences.setBackendDefaultTheme(authSettings.uiDefaultTheme);
}
const {trackingID, anonymizeUsers} = authSettings.googleAnalytics || {trackingID: '', anonymizeUsers: true};
const {loggedIn, username} = await services.users.get();
if (trackingID) {
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/shared/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ export interface AuthSettings {
userLoginsDisabled: boolean;
kustomizeVersions: string[];
uiCssURL: string;
uiDefaultTheme: string;
uiBannerContent: string;
uiBannerURL: string;
uiBannerPermanent: boolean;
Expand Down
14 changes: 13 additions & 1 deletion ui/src/app/shared/services/view-preferences-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = {

export class ViewPreferencesService {
private preferencesSubj: BehaviorSubject<ViewPreferences>;
private backendDefaultTheme: string = '';

public init() {
if (!this.preferencesSubj) {
Expand All @@ -214,6 +215,12 @@ export class ViewPreferencesService {
}
}

public setBackendDefaultTheme(theme: string) {
this.backendDefaultTheme = theme;
// Reload preferences with the new backend default
this.preferencesSubj.next(this.loadPreferences());
}

public getPreferences(): Observable<ViewPreferences> {
return this.preferencesSubj;
}
Expand All @@ -239,6 +246,11 @@ export class ViewPreferencesService {
} else {
preferences = DEFAULT_PREFERENCES;
}
return deepMerge(DEFAULT_PREFERENCES, preferences);
const merged = deepMerge(DEFAULT_PREFERENCES, preferences);
// If backend default theme is set and user hasn't explicitly set a theme, use backend default
if (this.backendDefaultTheme && (!preferencesStr || !JSON.parse(preferencesStr).theme)) {
merged.theme = this.backendDefaultTheme;
}
return merged;
}
}
5 changes: 5 additions & 0 deletions util/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ type ArgoCDSettings struct {
UiBannerPermanent bool `json:"uiBannerPermanent,omitempty"` //nolint:revive //FIXME(var-naming)
// Position of UI Banner
UiBannerPosition string `json:"uiBannerPosition,omitempty"` //nolint:revive //FIXME(var-naming)
// UiDefaultTheme holds the default theme for the UI (light, dark, auto)
UiDefaultTheme string `json:"uiDefaultTheme,omitempty"` //nolint:revive //FIXME(var-naming)
// PasswordPattern for password regular expression
PasswordPattern string `json:"passwordPattern,omitempty"`
// BinaryUrls contains the URLs for downloading argocd binaries
Expand Down Expand Up @@ -505,6 +507,8 @@ const (
settingUIBannerPermanentKey = "ui.bannerpermanent"
// settingUIBannerPositionKey designates the key for the position of the banner
settingUIBannerPositionKey = "ui.bannerposition"
// settingUIDefaultThemeKey designates the key for the default theme (light, dark, auto)
settingUIDefaultThemeKey = "ui.defaulttheme"
// settingsBinaryUrlsKey designates the key for the argocd binary URLs
settingsBinaryUrlsKey = "help.download"
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
Expand Down Expand Up @@ -1473,6 +1477,7 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *corev1.Conf
settings.UiBannerContent = argoCDCM.Data[settingUIBannerContentKey]
settings.UiBannerPermanent = argoCDCM.Data[settingUIBannerPermanentKey] == "true"
settings.UiBannerPosition = argoCDCM.Data[settingUIBannerPositionKey]
settings.UiDefaultTheme = argoCDCM.Data[settingUIDefaultThemeKey]
settings.BinaryUrls = getDownloadBinaryUrlsFromConfigMap(argoCDCM)
if err := ValidateExternalURL(argoCDCM.Data[settingURLKey]); err != nil {
log.Warnf("Failed to validate URL in configmap: %v", err)
Expand Down
38 changes: 38 additions & 0 deletions util/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,44 @@ func TestSettingsManager_GetSettings(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, time.Hour*10, s.UserSessionDuration)
})
t.Run("UiDefaultThemeNotProvided", func(t *testing.T) {
_, settingsManager := fixtures(t.Context(), map[string]string{}, func(secret *corev1.Secret) {
secret.Data["server.secretkey"] = []byte("test")
})
s, err := settingsManager.GetSettings()
require.NoError(t, err)
assert.Empty(t, s.UiDefaultTheme)
})
t.Run("UiDefaultThemeDark", func(t *testing.T) {
_, settingsManager := fixtures(t.Context(), map[string]string{
"ui.defaulttheme": "dark",
}, func(secret *corev1.Secret) {
secret.Data["server.secretkey"] = []byte("test")
})
s, err := settingsManager.GetSettings()
require.NoError(t, err)
assert.Equal(t, "dark", s.UiDefaultTheme)
})
t.Run("UiDefaultThemeLight", func(t *testing.T) {
_, settingsManager := fixtures(t.Context(), map[string]string{
"ui.defaulttheme": "light",
}, func(secret *corev1.Secret) {
secret.Data["server.secretkey"] = []byte("test")
})
s, err := settingsManager.GetSettings()
require.NoError(t, err)
assert.Equal(t, "light", s.UiDefaultTheme)
})
t.Run("UiDefaultThemeAuto", func(t *testing.T) {
_, settingsManager := fixtures(t.Context(), map[string]string{
"ui.defaulttheme": "auto",
}, func(secret *corev1.Secret) {
secret.Data["server.secretkey"] = []byte("test")
})
s, err := settingsManager.GetSettings()
require.NoError(t, err)
assert.Equal(t, "auto", s.UiDefaultTheme)
})
}

func TestGetOIDCConfig(t *testing.T) {
Expand Down
Loading