Skip to content

Commit a6294cb

Browse files
adding default theme system
Signed-off-by: Guillaume BERNARD <guillaume.bernard@live.fr>
1 parent f75905b commit a6294cb

File tree

12 files changed

+409
-16
lines changed

12 files changed

+409
-16
lines changed

backend/cmd/headlamp.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ const (
119119
type clientConfig struct {
120120
Clusters []Cluster `json:"clusters"`
121121
IsDynamicClusterEnabled bool `json:"isDynamicClusterEnabled"`
122+
DefaultLightTheme string `json:"defaultLightTheme,omitempty"`
123+
DefaultDarkTheme string `json:"defaultDarkTheme,omitempty"`
124+
ForceTheme string `json:"forceTheme,omitempty"`
122125
}
123126

124127
type OauthConfig struct {
@@ -1767,7 +1770,13 @@ func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []error) {
17671770
func (c *HeadlampConfig) getConfig(w http.ResponseWriter, r *http.Request) {
17681771
w.Header().Set("Content-Type", "application/json")
17691772

1770-
clientConfig := clientConfig{c.getClusters(), c.EnableDynamicClusters}
1773+
clientConfig := clientConfig{
1774+
Clusters: c.getClusters(),
1775+
IsDynamicClusterEnabled: c.EnableDynamicClusters,
1776+
DefaultLightTheme: c.DefaultLightTheme,
1777+
DefaultDarkTheme: c.DefaultDarkTheme,
1778+
ForceTheme: c.ForceTheme,
1779+
}
17711780

17721781
if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
17731782
logger.Log(logger.LevelError, nil, err, "encoding config")

backend/cmd/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextSto
9797
ProxyURLs: strings.Split(conf.ProxyURLs, ","),
9898
TLSCertPath: conf.TLSCertPath,
9999
TLSKeyPath: conf.TLSKeyPath,
100+
DefaultLightTheme: conf.DefaultLightTheme,
101+
DefaultDarkTheme: conf.DefaultDarkTheme,
102+
ForceTheme: conf.ForceTheme,
100103
}
101104
}
102105

backend/cmd/stateless.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,13 @@ func (c *HeadlampConfig) parseKubeConfig(w http.ResponseWriter, r *http.Request)
178178
return
179179
}
180180

181-
clientConfig := clientConfig{contexts, c.EnableDynamicClusters}
181+
clientConfig := clientConfig{
182+
Clusters: contexts,
183+
IsDynamicClusterEnabled: c.EnableDynamicClusters,
184+
DefaultLightTheme: c.DefaultLightTheme,
185+
DefaultDarkTheme: c.DefaultDarkTheme,
186+
ForceTheme: c.ForceTheme,
187+
}
182188

183189
if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
184190
logger.Log(logger.LevelError, nil, err, "encoding config")

backend/pkg/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ type Config struct {
8181
// TLS config
8282
TLSCertPath string `koanf:"tls-cert-path"`
8383
TLSKeyPath string `koanf:"tls-key-path"`
84+
// Theme config
85+
DefaultLightTheme string `koanf:"default-light-theme"`
86+
DefaultDarkTheme string `koanf:"default-dark-theme"`
87+
ForceTheme string `koanf:"force-theme"`
8488
}
8589

8690
func (c *Config) Validate() error {
@@ -430,6 +434,9 @@ func addGeneralFlags(f *flag.FlagSet) {
430434
f.Uint("port", defaultPort, "Port to listen from")
431435
f.String("proxy-urls", "", "Allow proxy requests to specified URLs")
432436
f.Bool("enable-helm", false, "Enable Helm operations")
437+
f.String("default-light-theme", "", "Default theme to use when user prefers light mode")
438+
f.String("default-dark-theme", "", "Default theme to use when user prefers dark mode")
439+
f.String("force-theme", "", "Force a specific theme, overriding user preferences")
433440
}
434441

435442
func addOIDCFlags(f *flag.FlagSet) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package config_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/kubernetes-sigs/headlamp/backend/pkg/config"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestParseThemeConfiguration(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
args []string
16+
env map[string]string
17+
verify func(*testing.T, *config.Config)
18+
}{
19+
{
20+
name: "default_light_theme_from_args",
21+
args: []string{"go run ./cmd", "--default-light-theme=corporate-light"},
22+
verify: func(t *testing.T, conf *config.Config) {
23+
assert.Equal(t, "corporate-light", conf.DefaultLightTheme)
24+
assert.Equal(t, "", conf.DefaultDarkTheme)
25+
assert.Equal(t, "", conf.ForceTheme)
26+
},
27+
},
28+
{
29+
name: "default_dark_theme_from_args",
30+
args: []string{"go run ./cmd", "--default-dark-theme=corporate-dark"},
31+
verify: func(t *testing.T, conf *config.Config) {
32+
assert.Equal(t, "", conf.DefaultLightTheme)
33+
assert.Equal(t, "corporate-dark", conf.DefaultDarkTheme)
34+
assert.Equal(t, "", conf.ForceTheme)
35+
},
36+
},
37+
{
38+
name: "both_default_themes_from_args",
39+
args: []string{"go run ./cmd", "--default-light-theme=corporate-light", "--default-dark-theme=corporate-dark"},
40+
verify: func(t *testing.T, conf *config.Config) {
41+
assert.Equal(t, "corporate-light", conf.DefaultLightTheme)
42+
assert.Equal(t, "corporate-dark", conf.DefaultDarkTheme)
43+
assert.Equal(t, "", conf.ForceTheme)
44+
},
45+
},
46+
{
47+
name: "force_theme_from_args",
48+
args: []string{"go run ./cmd", "--force-theme=corporate-branded"},
49+
verify: func(t *testing.T, conf *config.Config) {
50+
assert.Equal(t, "", conf.DefaultLightTheme)
51+
assert.Equal(t, "", conf.DefaultDarkTheme)
52+
assert.Equal(t, "corporate-branded", conf.ForceTheme)
53+
},
54+
},
55+
{
56+
name: "force_theme_with_defaults",
57+
args: []string{"go run ./cmd", "--default-light-theme=light", "--default-dark-theme=dark", "--force-theme=corporate"},
58+
verify: func(t *testing.T, conf *config.Config) {
59+
assert.Equal(t, "light", conf.DefaultLightTheme)
60+
assert.Equal(t, "dark", conf.DefaultDarkTheme)
61+
assert.Equal(t, "corporate", conf.ForceTheme)
62+
},
63+
},
64+
{
65+
name: "theme_from_env",
66+
args: []string{"go run ./cmd"},
67+
env: map[string]string{
68+
"HEADLAMP_CONFIG_DEFAULT_LIGHT_THEME": "env-light",
69+
"HEADLAMP_CONFIG_DEFAULT_DARK_THEME": "env-dark",
70+
},
71+
verify: func(t *testing.T, conf *config.Config) {
72+
assert.Equal(t, "env-light", conf.DefaultLightTheme)
73+
assert.Equal(t, "env-dark", conf.DefaultDarkTheme)
74+
},
75+
},
76+
{
77+
name: "force_theme_from_env",
78+
args: []string{"go run ./cmd"},
79+
env: map[string]string{
80+
"HEADLAMP_CONFIG_FORCE_THEME": "env-forced",
81+
},
82+
verify: func(t *testing.T, conf *config.Config) {
83+
assert.Equal(t, "env-forced", conf.ForceTheme)
84+
},
85+
},
86+
{
87+
name: "args_override_env",
88+
args: []string{"go run ./cmd", "--default-light-theme=arg-theme"},
89+
env: map[string]string{
90+
"HEADLAMP_CONFIG_DEFAULT_LIGHT_THEME": "env-theme",
91+
},
92+
verify: func(t *testing.T, conf *config.Config) {
93+
assert.Equal(t, "arg-theme", conf.DefaultLightTheme)
94+
},
95+
},
96+
{
97+
name: "no_theme_config",
98+
args: []string{"go run ./cmd"},
99+
verify: func(t *testing.T, conf *config.Config) {
100+
assert.Equal(t, "", conf.DefaultLightTheme)
101+
assert.Equal(t, "", conf.DefaultDarkTheme)
102+
assert.Equal(t, "", conf.ForceTheme)
103+
},
104+
},
105+
}
106+
107+
for _, tt := range tests {
108+
t.Run(tt.name, func(t *testing.T) {
109+
if tt.env != nil {
110+
for key, value := range tt.env {
111+
os.Setenv(key, value)
112+
}
113+
defer func(env map[string]string) {
114+
for key := range env {
115+
os.Unsetenv(key)
116+
}
117+
}(tt.env)
118+
}
119+
120+
conf, err := config.Parse(tt.args)
121+
require.NoError(t, err)
122+
require.NotNil(t, conf)
123+
124+
tt.verify(t, conf)
125+
})
126+
}
127+
}

backend/pkg/headlampconfig/headlampConfig.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ type HeadlampCFG struct {
2828
ProxyURLs []string
2929
TLSCertPath string
3030
TLSKeyPath string
31+
DefaultLightTheme string
32+
DefaultDarkTheme string
33+
ForceTheme string
3134
}

frontend/src/components/App/Layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import AlertNotification from '../common/AlertNotification';
4444
import DetailsDrawer from '../common/Resource/DetailsDrawer';
4545
import Sidebar, { NavigationTabs } from '../Sidebar';
4646
import RouteSwitcher from './RouteSwitcher';
47+
import themeSlice from './themeSlice';
4748
import TopBar from './TopBar';
4849
import VersionDialog from './VersionDialog';
4950

@@ -167,6 +168,17 @@ const fetchConfig = (dispatch: Dispatch<UnknownAction>) => {
167168
}
168169
}
169170

171+
// Apply backend theme configuration if provided
172+
if (config?.defaultLightTheme || config?.defaultDarkTheme || config?.forceTheme) {
173+
dispatch(
174+
themeSlice.actions.applyBackendThemeConfig({
175+
defaultLightTheme: config.defaultLightTheme,
176+
defaultDarkTheme: config.defaultDarkTheme,
177+
forceTheme: config.forceTheme,
178+
})
179+
);
180+
}
181+
170182
/**
171183
* Fetches the stateless cluster config from the indexDB and then sends the backend to parse it
172184
* only if the stateless cluster config is enabled in the backend.

frontend/src/components/App/themeSlice.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,30 @@ const themeSlice = createSlice({
7272
setAppTheme(defaultThemeName);
7373
}
7474
},
75+
/**
76+
* Applies backend theme configuration if set.
77+
* Should be called after config is loaded from backend.
78+
*/
79+
applyBackendThemeConfig(
80+
state,
81+
action: PayloadAction<{
82+
defaultLightTheme?: string;
83+
defaultDarkTheme?: string;
84+
forceTheme?: string;
85+
}>
86+
) {
87+
const backendConfig = action.payload;
88+
const newThemeName = getThemeName(backendConfig);
89+
90+
// Only update if theme has changed
91+
if (newThemeName !== state.name) {
92+
state.name = newThemeName;
93+
// Only persist to localStorage if not forced
94+
if (!backendConfig.forceTheme) {
95+
setAppTheme(newThemeName);
96+
}
97+
}
98+
},
7599
},
76100
});
77101

0 commit comments

Comments
 (0)