Skip to content

Commit

Permalink
Add a dark theme
Browse files Browse the repository at this point in the history
This implements dark mode theme using https://github.com/ForEvolve/bootstrap-dark.
User is able to set theme to:
a) light - same as current theme and the default one
b) dark - dark version using bootstrap-dark
c) auto - either light or dark version will be used depending on browser preference, if browser doesn't support media queries for prefers-color-scheme light theme will be used

To make all of this work existing CSS rules were moved into themes folder and converted to scss, both light and dark styles will import that shared CSS file but each might pass different variables to customise generated CSS. Some parts of the shared CSS file use bootstrap and bootstrap-dark variables directly, to avoid redefining what's already defined there.

Signed-off-by: Łukasz Mierzwa <l.mierzwa@gmail.com>
  • Loading branch information
prymitive committed Mar 15, 2021
1 parent 3c7d90c commit 19a9571
Show file tree
Hide file tree
Showing 23 changed files with 732 additions and 102 deletions.
10 changes: 7 additions & 3 deletions web/ui/react-app/package.json
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@forevolve/bootstrap-dark": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.14",
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
Expand All @@ -17,7 +18,8 @@
"@types/react-dom": "^16.8.0",
"@types/react-resize-detector": "^5.0.0",
"@types/sanitize-html": "^1.20.2",
"bootstrap": "^4.2.1",
"bootstrap": "4.6.0",
"bootstrap-dark": "^1.0.3",
"css.escape": "^1.5.1",
"downshift": "^3.4.8",
"enzyme-to-json": "^3.4.3",
Expand All @@ -29,18 +31,20 @@
"jsdom": "^16.4.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"node-sass": "4.14.1",
"popper.js": "^1.14.3",
"react": "^16.7.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.7.0",
"react-resize-detector": "^5.0.7",
"react-scripts": "3.4.4",
"react-test-renderer": "^16.9.0",
"reactstrap": "^8.0.1",
"reactstrap": "^8.9.0",
"sanitize-html": "^1.20.1",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.0.3",
"typescript": "^3.3.3"
"typescript": "^3.3.3",
"use-media": "^1.4.0"
},
"scripts": {
"start": "react-scripts start",
Expand Down
57 changes: 38 additions & 19 deletions web/ui/react-app/src/App.tsx
Expand Up @@ -2,10 +2,13 @@ import React, { FC } from 'react';
import Navigation from './Navbar';
import { Container } from 'reactstrap';

import './App.css';
import { Router, Redirect } from '@reach/router';
import useMedia from 'use-media';
import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages';
import { PathPrefixContext } from './contexts/PathPrefixContext';
import { ThemeContext, themeName, themeSetting } from './contexts/ThemeContext';
import { Theme, themeLocalStorageKey } from './Theme';
import { useLocalStorage } from './hooks/useLocalStorage';

interface AppProps {
consolesLink: string | null;
Expand Down Expand Up @@ -39,28 +42,44 @@ const App: FC<AppProps> = ({ consolesLink }) => {
}
}

const [userTheme, setUserTheme] = useLocalStorage<themeSetting>(themeLocalStorageKey, 'auto');
const browserHasThemes = useMedia('(prefers-color-scheme)');
const browserWantsDarkTheme = useMedia('(prefers-color-scheme: dark)');

let theme: themeName;
if (userTheme !== 'auto') {
theme = userTheme;
} else {
theme = browserHasThemes ? (browserWantsDarkTheme ? 'dark' : 'light') : 'light';
}

return (
<PathPrefixContext.Provider value={basePath}>
<Navigation consolesLink={consolesLink} />
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath={`${basePath}`}>
<Redirect from="/" to={`graph`} noThrow />
{/*
<ThemeContext.Provider
value={{ theme: theme, userPreference: userTheme, setTheme: (t: themeSetting) => setUserTheme(t) }}
>
<Theme />
<PathPrefixContext.Provider value={basePath}>
<Navigation consolesLink={consolesLink} />
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath={`${basePath}`}>
<Redirect from="/" to={`graph`} noThrow />
{/*
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<PanelList path="/graph" />
<Alerts path="/alerts" />
<Config path="/config" />
<Flags path="/flags" />
<Rules path="/rules" />
<ServiceDiscovery path="/service-discovery" />
<Status path="/status" />
<TSDBStatus path="/tsdb-status" />
<Targets path="/targets" />
</Router>
</Container>
</PathPrefixContext.Provider>
<PanelList path="/graph" />
<Alerts path="/alerts" />
<Config path="/config" />
<Flags path="/flags" />
<Rules path="/rules" />
<ServiceDiscovery path="/service-discovery" />
<Status path="/status" />
<TSDBStatus path="/tsdb-status" />
<Targets path="/targets" />
</Router>
</Container>
</PathPrefixContext.Provider>
</ThemeContext.Provider>
);
};

Expand Down
2 changes: 2 additions & 0 deletions web/ui/react-app/src/Navbar.tsx
Expand Up @@ -13,6 +13,7 @@ import {
DropdownItem,
} from 'reactstrap';
import { usePathPrefix } from './contexts/PathPrefixContext';
import { ThemeToggle } from './Theme';

interface NavbarProps {
consolesLink: string | null;
Expand Down Expand Up @@ -81,6 +82,7 @@ const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
</NavItem>
</Nav>
</Collapse>
<ThemeToggle />
</Navbar>
);
};
Expand Down
77 changes: 77 additions & 0 deletions web/ui/react-app/src/Theme.tsx
@@ -0,0 +1,77 @@
import React, { FC, useEffect, lazy } from 'react';
import { Form, Button, ButtonGroup } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner, faMoon, faSun, faAdjust } from '@fortawesome/free-solid-svg-icons';
import { useTheme } from './contexts/ThemeContext';

const DarkTheme = lazy(() => import('./themes/dark'));
const LightTheme = lazy(() => import('./themes/light'));

export const themeLocalStorageKey = 'user-prefers-color-scheme';

const ThemeSpinner: FC = () => (
<div
style={{
zIndex: 2000,
backgroundColor: '#eee',
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<FontAwesomeIcon
size="8x"
icon={faSpinner}
spin
className="position-absolute"
style={{ transform: 'translate(-50%, -50%)', top: '50%', left: '50%' }}
/>
</div>
);

export const Theme: FC = () => {
const { theme } = useTheme();

useEffect(() => {
document.body.classList.toggle('bootstrap-dark', theme === 'dark');
document.body.classList.toggle('bootstrap', theme === 'light');
}, [theme]);

return <React.Suspense fallback={<ThemeSpinner />}>{theme === 'dark' ? <DarkTheme /> : <LightTheme />}</React.Suspense>;
};

export const ThemeToggle: FC = () => {
const { userPreference, setTheme } = useTheme();

return (
<Form className="ml-auto" inline>
<ButtonGroup size="sm">
<Button
color="secondary"
title="Use light theme"
active={userPreference === 'light'}
onClick={() => setTheme('light')}
>
<FontAwesomeIcon icon={faSun} />
</Button>
<Button color="secondary" title="Use dark theme" active={userPreference === 'dark'} onClick={() => setTheme('dark')}>
<FontAwesomeIcon icon={faMoon} />
</Button>
<Button
color="secondary"
title="Use browser preferred theme"
active={userPreference === 'auto'}
onClick={() => setTheme('auto')}
>
<FontAwesomeIcon icon={faAdjust} />
</Button>
</ButtonGroup>
</Form>
);
};
20 changes: 20 additions & 0 deletions web/ui/react-app/src/contexts/ThemeContext.tsx
@@ -0,0 +1,20 @@
import React from 'react';

export type themeName = 'light' | 'dark';
export type themeSetting = themeName | 'auto';

export interface themeCtx {
theme: themeName;
userPreference: themeSetting;
setTheme: (t: themeSetting) => void;
}

export const ThemeContext = React.createContext<themeCtx>({
theme: 'light',
userPreference: 'auto',
setTheme: (s: themeSetting) => {},
});

export const useTheme = () => {
return React.useContext(ThemeContext);
};
1 change: 0 additions & 1 deletion web/ui/react-app/src/index.tsx
Expand Up @@ -2,7 +2,6 @@ import './globals';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'bootstrap/dist/css/bootstrap.min.css';
import { isPresent } from './utils';

// Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
Expand Down
Expand Up @@ -28,7 +28,7 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
<strong>{rule.name}</strong> ({`${rule.alerts.length} active`})
</Alert>
<Collapse isOpen={open} className="mb-2">
<pre style={{ background: '#f5f5f5', padding: 15 }}>
<pre className="alert_cell">
<code>
<div>
name: <Link to={createExpressionLink(`ALERTS{alertname="${rule.name}"}`)}>{rule.name}</Link>
Expand Down
10 changes: 0 additions & 10 deletions web/ui/react-app/src/pages/config/Config.css

This file was deleted.

1 change: 0 additions & 1 deletion web/ui/react-app/src/pages/config/Config.tsx
Expand Up @@ -3,7 +3,6 @@ import { RouteComponentProps } from '@reach/router';
import { Button } from 'reactstrap';
import CopyToClipboard from 'react-copy-to-clipboard';

import './Config.css';
import { withStatusIndicator } from '../../components/withStatusIndicator';
import { useFetch } from '../../hooks/useFetch';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
Expand Down
4 changes: 1 addition & 3 deletions web/ui/react-app/src/pages/graph/ExpressionInput.tsx
Expand Up @@ -101,9 +101,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
key: original,
index,
item: original,
style: {
backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
},
className: `autosuggest-dropdown-item ${highlightedIndex === index++ ? 'active' : ''}`,
});
return (
<li
Expand Down
14 changes: 0 additions & 14 deletions web/ui/react-app/src/pages/graph/MetricsExplorer.css

This file was deleted.

1 change: 0 additions & 1 deletion web/ui/react-app/src/pages/graph/MetricsExplorer.tsx
@@ -1,6 +1,5 @@
import React, { Component } from 'react';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import './MetricsExplorer.css';

interface Props {
show: boolean;
Expand Down
5 changes: 0 additions & 5 deletions web/ui/react-app/src/pages/graph/QueryStatsView.css

This file was deleted.

1 change: 0 additions & 1 deletion web/ui/react-app/src/pages/graph/QueryStatsView.tsx
@@ -1,5 +1,4 @@
import React, { FC } from 'react';
import './QueryStatsView.css';

export interface QueryStats {
loadTime: number;
Expand Down
2 changes: 1 addition & 1 deletion web/ui/react-app/src/pages/rules/RulesContent.tsx
Expand Up @@ -83,7 +83,7 @@ export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ resp
{g.rules.map((r, i) => {
return (
<tr key={i}>
<td style={{ backgroundColor: '#F5F5F5' }} className="rule_cell">
<td className="rule_cell">
{r.alerts ? (
<GraphExpressionLink title="alert" text={r.name} expr={`ALERTS{alertname="${r.name}"}`} />
) : (
Expand Down
4 changes: 0 additions & 4 deletions web/ui/react-app/src/pages/targets/Filter.module.css

This file was deleted.

7 changes: 3 additions & 4 deletions web/ui/react-app/src/pages/targets/Filter.tsx
@@ -1,6 +1,5 @@
import React, { Dispatch, FC, SetStateAction } from 'react';
import { Button, ButtonGroup } from 'reactstrap';
import styles from './Filter.module.css';

export interface FilterData {
showHealthy: boolean;
Expand All @@ -17,19 +16,19 @@ const Filter: FC<FilterProps> = ({ filter, setFilter }) => {
const btnProps = {
all: {
active: showHealthy,
className: `all ${styles.btn}`,
className: 'all',
color: 'primary',
onClick: (): void => setFilter({ ...filter, showHealthy: true }),
},
unhealthy: {
active: !showHealthy,
className: `unhealthy ${styles.btn}`,
className: 'unhealthy',
color: 'primary',
onClick: (): void => setFilter({ ...filter, showHealthy: false }),
},
};
return (
<ButtonGroup>
<ButtonGroup className="mt-3 mb-4">
<Button {...btnProps.all}>All</Button>
<Button {...btnProps.unhealthy}>Unhealthy</Button>
</ButtonGroup>
Expand Down
16 changes: 16 additions & 0 deletions web/ui/react-app/src/themes/dark.scss
@@ -0,0 +1,16 @@
@import '~@forevolve/bootstrap-dark/dist/css/toggle-bootstrap-dark.min.css';

@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import '~@forevolve/bootstrap-dark/scss/_dark-variables.scss';

$config-yaml-color: $white;
$config-yaml-bg: $jumbotron-bg;
$config-yaml-border: $gray-700;

$alert_cell-color: $white;
$rule_cell-bg: $gray-900;

.bootstrap-dark {
@import './shared.scss';
}
7 changes: 7 additions & 0 deletions web/ui/react-app/src/themes/dark.tsx
@@ -0,0 +1,7 @@
import { FC } from 'react';

import './dark.scss';

const DarkTheme: FC = () => null;

export default DarkTheme;
15 changes: 15 additions & 0 deletions web/ui/react-app/src/themes/light.scss
@@ -0,0 +1,15 @@
@import '~@forevolve/bootstrap-dark/dist/css/toggle-bootstrap.min.css';

@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';

$config-yaml-color: #333;
$config-yaml-bg: $jumbotron-bg;
$config-yaml-border: #ccc;

$alert_cell-color: inherit;
$rule_cell-bg: #f5f5f5;

.bootstrap {
@import './shared.scss';
}

0 comments on commit 19a9571

Please sign in to comment.