Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): support auto theme #20080

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions ui/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import applications from './applications';
import help from './help';
import login from './login';
import settings from './settings';
import {Layout} from './shared/components/layout/layout';
import {Layout, ThemeWrapper} from './shared/components/layout/layout';
import {Page} from './shared/components/page/page';
import {VersionPanel} from './shared/components/version-info/version-info-panel';
import {AuthSettingsCtx, Provider} from './shared/context';
Expand Down Expand Up @@ -194,7 +194,7 @@ export class App extends React.Component<
<PageContext.Provider value={{title: 'Argo CD'}}>
<Provider value={{history, popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base}}>
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{pref => <div className={pref.theme ? 'theme-' + pref.theme : 'theme-light'}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</div>}
{pref => <ThemeWrapper theme={pref.theme}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</ThemeWrapper>}
</DataLoader>
<AuthSettingsCtx.Provider value={this.state.authSettings}>
<Router history={history}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
}
border-radius: 4px;
box-shadow: 1px 2px 3px rgba(#000, 0.1);
}
& .row {
justify-content: space-between;
align-items: center;

&__button {
position: absolute;
top: 25%;
right: 30px;
.select {
min-width: 160px;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import {DataLoader, Page} from '../../../shared/components';
import {services} from '../../../shared/services';
import {Select, SelectOption} from 'argo-ui';

require('./appearance-list.scss');

Expand All @@ -16,16 +17,16 @@ export const AppearanceList = () => {
<div className='appearance-list'>
<div className='argo-container'>
<div className='appearance-list__panel'>
<div className='columns'>Dark Theme</div>
<div className='columns'>
<button
className='argo-button argo-button--base appearance-list__button'
onClick={() => {
const targetTheme = pref.theme === 'light' ? 'dark' : 'light';
services.viewPreferences.updatePreferences({theme: targetTheme});
}}>
{pref.theme === 'light' ? 'Enable' : 'Disable'}
</button>
<div className='row'>
<span>Dark Theme</span>
<Select
value={pref.theme}
onChange={(value: SelectOption) => services.viewPreferences.updatePreferences({theme: value.value})}
options={[
{value: 'auto', title: 'Auto'},
{value: 'light', title: 'Light'},
{value: 'dark', title: 'Dark'}
]}></Select>
</div>
</div>
</div>
Expand Down
15 changes: 11 additions & 4 deletions ui/src/app/shared/components/layout/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import {Sidebar} from '../../../sidebar/sidebar';
import {ViewPreferences} from '../../services';
import {useTheme} from '../../utils';

require('./layout.scss');

Expand All @@ -13,15 +14,21 @@ export interface LayoutProps {

const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f');

export const ThemeWrapper = (props: {children: React.ReactNode; theme: string}) => {
const [theme] = useTheme({theme: 'auto'});
return <div className={'theme-' + theme}>{props.children}</div>;
};

export const Layout = (props: LayoutProps) => {
const [theme] = useTheme({theme: props.pref.theme});
React.useEffect(() => {
if (props.pref.theme) {
document.body.style.background = getBGColor(props.pref.theme);
if (theme) {
document.body.style.background = getBGColor(theme);
}
}, [props.pref.theme]);
}, [theme]);

return (
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
<div className={`theme-${theme}`}>
<div className={'cd-layout'}>
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>
Expand Down
18 changes: 17 additions & 1 deletion ui/src/app/shared/components/monaco-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';

import * as monacoEditor from 'monaco-editor';
import {services} from '../services';
import {getTheme, useSystemTheme} from '../utils';

export interface EditorInput {
text: string;
Expand All @@ -28,10 +29,25 @@ const MonacoEditorLazy = React.lazy(() =>
import('monaco-editor').then(monaco => {
const Component = (props: MonacoProps) => {
const [height, setHeight] = React.useState(0);
const [theme, setTheme] = React.useState('dark');

React.useEffect(() => {
const destroySystemThemeListener = useSystemTheme(systemTheme => {
if (theme === 'auto') {
monaco.editor.setTheme(systemTheme === 'dark' ? 'vs-dark' : 'vs');
}
});

return () => {
destroySystemThemeListener();
};
}, [theme]);

React.useEffect(() => {
const subscription = services.viewPreferences.getPreferences().subscribe(preferences => {
monaco.editor.setTheme(preferences.theme === 'dark' ? 'vs-dark' : 'vs');
setTheme(preferences.theme);

monaco.editor.setTheme(getTheme(preferences.theme) === 'dark' ? 'vs-dark' : 'vs');
});

return () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {DataLoader, SlidingPanel, Tooltip} from 'argo-ui';
import * as React from 'react';
import {VersionMessage} from '../../models';
import {services} from '../../services';
import {ThemeWrapper} from '../layout/layout';

interface VersionPanelProps {
isShown: boolean;
Expand All @@ -26,14 +27,14 @@ export class VersionPanel extends React.Component<VersionPanelProps, {copyState:
<DataLoader load={() => this.props.version}>
{version => {
return (
<div className={'theme-' + pref.theme}>
<ThemeWrapper theme={pref.theme}>
<SlidingPanel header={this.header} isShown={this.props.isShown} onClose={() => this.props.onClose()} hasCloseButton={true} isNarrow={true}>
<div className='argo-table-list'>{this.buildVersionTable(version)}</div>
<div>
<Tooltip content='Copy all version info as JSON'>{this.getCopyButton(version)}</Tooltip>
</div>
</SlidingPanel>
</div>
</ThemeWrapper>
);
}}
</DataLoader>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/shared/services/view-preferences-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = {
hideBannerContent: '',
hideSidebar: false,
position: '',
theme: 'light'
theme: 'auto'
};

export class ViewPreferencesService {
Expand Down
72 changes: 72 additions & 0 deletions ui/src/app/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

export function hashCode(str: string) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
Expand Down Expand Up @@ -34,3 +36,73 @@ export function isValidURL(url: string): boolean {
}
}
}

export const colorSchemes = {
light: '(prefers-color-scheme: light)',
dark: '(prefers-color-scheme: dark)'
};

/**
* quick method to check system theme
* @param theme auto, light, dark
* @returns dark or light
*/
export function getTheme(theme: string) {
if (theme !== 'auto') {
return theme;
}

const dark = window.matchMedia(colorSchemes.dark);

return dark.matches ? 'dark' : 'light';
}

/**
* create a listener for system theme
* @param cb callback for theme change
* @returns destroy listener
*/
export const useSystemTheme = (cb: (theme: string) => void) => {
const dark = window.matchMedia(colorSchemes.dark);
const light = window.matchMedia(colorSchemes.light);

const listener = () => {
cb(dark.matches ? 'dark' : 'light');
};

dark.addEventListener('change', listener);
light.addEventListener('change', listener);

return () => {
dark.removeEventListener('change', listener);
light.removeEventListener('change', listener);
};
};

export const useTheme = (props: {theme: string}) => {
const [theme, setTheme] = React.useState(getTheme(props.theme));

React.useEffect(() => {
// change theme by system
const destroyListener = useSystemTheme(systemTheme => {
setTheme(currentTheme => {
if (props.theme === 'auto') {
return systemTheme;
}

return currentTheme;
});
});

// change theme manually
if (props.theme !== theme) {
setTheme(getTheme(props.theme));
}

return () => {
destroyListener();
};
}, [props.theme]);

return [theme];
};