Skip to content

Commit ef62d68

Browse files
committed
UI: add dark theme
Signed-off-by: Aman Dwivedi <[email protected]>
1 parent 4c3f85c commit ef62d68

27 files changed

+867
-108
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ We use _breaking :warning:_ to mark changes that are not backward compatible (re
2020
- [#4176](https://github.com/thanos-io/thanos/pull/4176) Query API: Adds optional `Stats param` to return stats for query APIs
2121
- [#4125](https://github.com/thanos-io/thanos/pull/4125) Rule: Add `--alert.relabel-config` / `--alert.relabel-config-file` allowing to specify alert relabel configurations like [Prometheus](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config)
2222
- [#4211](https://github.com/thanos-io/thanos/pull/4211) Add TLS and basic authentication to Thanos APIs
23+
- [#4249](https://github.com/thanos-io/thanos/pull/4249) UI: add dark theme
2324

2425
### Fixed
2526
-

pkg/ui/react-app/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@codemirror/search": "^0.18.2",
1616
"@codemirror/state": "^0.18.2",
1717
"@codemirror/view": "^0.18.3",
18+
"@forevolve/bootstrap-dark": "^1.0.0",
1819
"@fortawesome/fontawesome-svg-core": "^1.2.34",
1920
"@fortawesome/free-solid-svg-icons": "^5.15.2",
2021
"@fortawesome/react-fontawesome": "^0.1.14",
@@ -44,9 +45,11 @@
4445
"react-test-renderer": "^16.14.0",
4546
"reactstrap": "^8.9.0",
4647
"sanitize-html": "^2.3.2",
48+
"sass": "^1.32.13",
4749
"tempusdominus-bootstrap-4": "^5.39.0",
4850
"tempusdominus-core": "^5.19.0",
4951
"typescript": "3.9.9",
52+
"use-media": "^1.4.0",
5053
"use-query-params": "^1.1.9"
5154
},
5255
"scripts": {
@@ -79,12 +82,12 @@
7982
"@types/moment-timezone": "^0.5.30",
8083
"@types/node": "^14.14.30",
8184
"@types/reach__router": "^1.3.7",
82-
"@types/reactstrap": "^8.7.2",
8385
"@types/react": "^17.0.2",
8486
"@types/react-copy-to-clipboard": "^5.0.0",
8587
"@types/react-dom": "^17.0.1",
8688
"@types/react-resize-detector": "^4.0.2",
8789
"@types/react-select": "^4.0.13",
90+
"@types/reactstrap": "^8.7.2",
8891
"@types/sanitize-html": "^1.27.1",
8992
"@types/sinon": "^9.0.10",
9093
"@typescript-eslint/eslint-plugin": "^4.15.1",

pkg/ui/react-app/public/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
-->
3737
<title>Thanos | Highly available Prometheus setup</title>
3838
</head>
39-
<body>
39+
<body class="bootstrap">
4040
<noscript>You need to enable JavaScript to run this app.</noscript>
4141
<div id="root"></div>
4242
<!--

pkg/ui/react-app/src/App.tsx

+47-29
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import React, { FC } from 'react';
22
import { Container } from 'reactstrap';
33
import { Router, Redirect, globalHistory } from '@reach/router';
44
import { QueryParamProvider } from 'use-query-params';
5+
import useMedia from 'use-media';
56

67
import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList, NotFound } from './pages';
78
import PathPrefixProps from './types/PathPrefixProps';
89
import ThanosComponentProps from './thanos/types/ThanosComponentProps';
910
import Navigation from './thanos/Navbar';
1011
import { Stores, ErrorBoundary, Blocks } from './thanos/pages';
11-
12-
import './App.css';
12+
import { ThemeContext, themeName, themeSetting } from './contexts/ThemeContext';
13+
import { Theme, themeLocalStorageKey } from './Theme';
14+
import { useLocalStorage } from './hooks/useLocalStorage';
1315

1416
const defaultRouteConfig: { [component: string]: string } = {
1517
query: '/graph',
@@ -20,35 +22,51 @@ const defaultRouteConfig: { [component: string]: string } = {
2022
};
2123

2224
const App: FC<PathPrefixProps & ThanosComponentProps> = ({ pathPrefix, thanosComponent }) => {
25+
const [userTheme, setUserTheme] = useLocalStorage<themeSetting>(themeLocalStorageKey, 'auto');
26+
const browserHasThemes = useMedia('(prefers-color-scheme)');
27+
const browserWantsDarkTheme = useMedia('(prefers-color-scheme: dark)');
28+
29+
let theme: themeName;
30+
if (userTheme !== 'auto') {
31+
theme = userTheme;
32+
} else {
33+
theme = browserHasThemes ? (browserWantsDarkTheme ? 'dark' : 'light') : 'light';
34+
}
35+
2336
return (
24-
<ErrorBoundary>
25-
<Navigation
26-
pathPrefix={pathPrefix}
27-
thanosComponent={thanosComponent}
28-
defaultRoute={defaultRouteConfig[thanosComponent]}
29-
/>
30-
<Container fluid style={{ paddingTop: 70 }}>
31-
<QueryParamProvider reachHistory={globalHistory}>
32-
<Router basepath={`${pathPrefix}`}>
33-
<Redirect from="/" to={`${pathPrefix}${defaultRouteConfig[thanosComponent]}`} />
37+
<ThemeContext.Provider
38+
value={{ theme: theme, userPreference: userTheme, setTheme: (t: themeSetting) => setUserTheme(t) }}
39+
>
40+
<Theme />
41+
<ErrorBoundary>
42+
<Navigation
43+
pathPrefix={pathPrefix}
44+
thanosComponent={thanosComponent}
45+
defaultRoute={defaultRouteConfig[thanosComponent]}
46+
/>
47+
<Container fluid style={{ paddingTop: 70 }}>
48+
<QueryParamProvider reachHistory={globalHistory}>
49+
<Router basepath={`${pathPrefix}`}>
50+
<Redirect from="/" to={`${pathPrefix}${defaultRouteConfig[thanosComponent]}`} />
3451

35-
<PanelList path="/graph" pathPrefix={pathPrefix} />
36-
<Alerts path="/alerts" pathPrefix={pathPrefix} />
37-
<Config path="/config" pathPrefix={pathPrefix} />
38-
<Flags path="/flags" pathPrefix={pathPrefix} />
39-
<Rules path="/rules" pathPrefix={pathPrefix} />
40-
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} />
41-
<Status path="/status" pathPrefix={pathPrefix} />
42-
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
43-
<Targets path="/targets" pathPrefix={pathPrefix} />
44-
<Stores path="/stores" pathPrefix={pathPrefix} />
45-
<Blocks path="/blocks" pathPrefix={pathPrefix} />
46-
<Blocks path="/loaded" pathPrefix={pathPrefix} view="loaded" />
47-
<NotFound pathPrefix={pathPrefix} default defaultRoute={defaultRouteConfig[thanosComponent]} />
48-
</Router>
49-
</QueryParamProvider>
50-
</Container>
51-
</ErrorBoundary>
52+
<PanelList path="/graph" pathPrefix={pathPrefix} />
53+
<Alerts path="/alerts" pathPrefix={pathPrefix} />
54+
<Config path="/config" pathPrefix={pathPrefix} />
55+
<Flags path="/flags" pathPrefix={pathPrefix} />
56+
<Rules path="/rules" pathPrefix={pathPrefix} />
57+
<ServiceDiscovery path="/service-discovery" pathPrefix={pathPrefix} />
58+
<Status path="/status" pathPrefix={pathPrefix} />
59+
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
60+
<Targets path="/targets" pathPrefix={pathPrefix} />
61+
<Stores path="/stores" pathPrefix={pathPrefix} />
62+
<Blocks path="/blocks" pathPrefix={pathPrefix} />
63+
<Blocks path="/loaded" pathPrefix={pathPrefix} view="loaded" />
64+
<NotFound pathPrefix={pathPrefix} default defaultRoute={defaultRouteConfig[thanosComponent]} />
65+
</Router>
66+
</QueryParamProvider>
67+
</Container>
68+
</ErrorBoundary>
69+
</ThemeContext.Provider>
5270
);
5371
};
5472

pkg/ui/react-app/src/Navbar.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DropdownItem,
1414
} from 'reactstrap';
1515
import PathPrefixProps from './types/PathPrefixProps';
16+
import { ThemeToggle } from './Theme';
1617

1718
interface NavbarProps {
1819
consolesLink: string | null;
@@ -23,7 +24,7 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
2324
const toggle = () => setIsOpen(!isOpen);
2425
return (
2526
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
26-
<NavbarToggler onClick={toggle} />
27+
<NavbarToggler onClick={toggle} className="mr-2" />
2728
<Link className="pt-0 navbar-brand" to={`${pathPrefix}/graph`}>
2829
Prometheus
2930
</Link>
@@ -80,6 +81,7 @@ const Navigation: FC<PathPrefixProps & NavbarProps> = ({ pathPrefix, consolesLin
8081
</NavItem>
8182
</Nav>
8283
</Collapse>
84+
<ThemeToggle />
8385
</Navbar>
8486
);
8587
};

pkg/ui/react-app/src/Theme.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
*
3+
* THIS FILE WAS COPIED INTO THANOS FROM PROMETHEUS
4+
* (LIVING AT https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/src/Theme.tsx),
5+
* THE ORIGINAL CODE WAS LICENSED UNDER AN APACHE 2.0 LICENSE, SEE
6+
* https://github.com/prometheus/prometheus/blob/main/LICENSE.
7+
*
8+
*/
9+
10+
import React, { FC, useEffect } from 'react';
11+
import { Form, Button, ButtonGroup } from 'reactstrap';
12+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13+
import { faMoon, faSun, faAdjust } from '@fortawesome/free-solid-svg-icons';
14+
import { useTheme } from './contexts/ThemeContext';
15+
16+
export const themeLocalStorageKey = 'user-prefers-color-scheme';
17+
18+
export const Theme: FC = () => {
19+
const { theme } = useTheme();
20+
21+
useEffect(() => {
22+
document.body.classList.toggle('bootstrap-dark', theme === 'dark');
23+
document.body.classList.toggle('bootstrap', theme === 'light');
24+
}, [theme]);
25+
26+
return null;
27+
};
28+
29+
export const ThemeToggle: FC = () => {
30+
const { userPreference, setTheme } = useTheme();
31+
32+
return (
33+
<Form className="ml-auto" inline>
34+
<ButtonGroup size="sm">
35+
<Button
36+
color="secondary"
37+
title="Use light theme"
38+
active={userPreference === 'light'}
39+
onClick={() => setTheme('light')}
40+
>
41+
<FontAwesomeIcon icon={faSun} className={userPreference === 'light' ? 'text-white' : 'text-dark'} />
42+
</Button>
43+
<Button color="secondary" title="Use dark theme" active={userPreference === 'dark'} onClick={() => setTheme('dark')}>
44+
<FontAwesomeIcon icon={faMoon} className={userPreference === 'dark' ? 'text-white' : 'text-dark'} />
45+
</Button>
46+
<Button
47+
color="secondary"
48+
title="Use browser-preferred theme"
49+
active={userPreference === 'auto'}
50+
onClick={() => setTheme('auto')}
51+
>
52+
<FontAwesomeIcon icon={faAdjust} className={userPreference === 'auto' ? 'text-white' : 'text-dark'} />
53+
</Button>
54+
</ButtonGroup>
55+
</Form>
56+
);
57+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
*
3+
* THIS FILE WAS COPIED INTO THANOS FROM PROMETHEUS
4+
* (LIVING AT https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/src/contexts/ThemeContext.tsx),
5+
* THE ORIGINAL CODE WAS LICENSED UNDER AN APACHE 2.0 LICENSE, SEE
6+
* https://github.com/prometheus/prometheus/blob/main/LICENSE.
7+
*
8+
*/
9+
10+
import React from 'react';
11+
12+
export type themeName = 'light' | 'dark';
13+
export type themeSetting = themeName | 'auto';
14+
15+
export interface ThemeCtx {
16+
theme: themeName;
17+
userPreference: themeSetting;
18+
setTheme: (t: themeSetting) => void;
19+
}
20+
21+
// defaults, will be overriden in App.tsx
22+
export const ThemeContext = React.createContext<ThemeCtx>({
23+
theme: 'light',
24+
userPreference: 'auto',
25+
// eslint-disable-next-line @typescript-eslint/no-empty-function
26+
setTheme: (s: themeSetting) => {},
27+
});
28+
29+
export const useTheme = () => {
30+
return React.useContext(ThemeContext);
31+
};

pkg/ui/react-app/src/index.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import './globals';
22
import React from 'react';
33
import ReactDOM from 'react-dom';
44
import App from './App';
5-
import 'bootstrap/dist/css/bootstrap.min.css';
5+
import './themes/app.scss';
6+
import './themes/light.scss';
7+
import './themes/dark.scss';
68
import './fonts/codicon.ttf';
79
import { isPresent } from './utils';
810

pkg/ui/react-app/src/pages/alerts/CollapsibleAlertPanel.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
2727
<strong>{rule.name}</strong> ({`${rule.alerts.length} active`})
2828
</Alert>
2929
<Collapse isOpen={open} className="mb-2">
30-
<pre style={{ background: '#f5f5f5', padding: 15 }}>
30+
<pre className="alert-cell">
3131
<code>
3232
<div>
3333
name: <a href={createExternalExpressionLink(`ALERTS{alertname="${rule.name}"}`)}>{rule.name}</a>

pkg/ui/react-app/src/pages/config/Config.css

-10
This file was deleted.

pkg/ui/react-app/src/pages/config/Config.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Button } from 'reactstrap';
44
import CopyToClipboard from 'react-copy-to-clipboard';
55
import PathPrefixProps from '../../types/PathPrefixProps';
66

7-
import './Config.css';
87
import { withStatusIndicator } from '../../components/withStatusIndicator';
98
import { useFetch } from '../../hooks/useFetch';
109

pkg/ui/react-app/src/pages/graph/CMExpressionInput.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { commentKeymap } from '@codemirror/comment';
1212
import { lintKeymap } from '@codemirror/lint';
1313
import { PromQLExtension, CompleteStrategy } from 'codemirror-promql';
1414
import { autocompletion, completionKeymap, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
15-
import { theme, promqlHighlighter } from './CMTheme';
15+
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
1616
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1717
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
1818
import { newCompleteStrategy } from 'codemirror-promql/cjs/complete';
1919
import PathPrefixProps from '../../types/PathPrefixProps';
20+
import { useTheme } from '../../contexts/ThemeContext';
2021

2122
const promqlExtension = new PromQLExtension();
2223

@@ -88,6 +89,7 @@ const CMExpressionInput: FC<PathPrefixProps & CMExpressionInputProps> = ({
8889
}) => {
8990
const containerRef = useRef<HTMLDivElement>(null);
9091
const viewRef = useRef<EditorView | null>(null);
92+
const { theme } = useTheme();
9193

9294
// (Re)initialize editor based on settings / setting changes.
9395
useEffect(() => {
@@ -103,7 +105,11 @@ const CMExpressionInput: FC<PathPrefixProps & CMExpressionInputProps> = ({
103105
queryHistory
104106
),
105107
});
106-
const dynamicConfig = [enableHighlighting ? promqlHighlighter : [], promqlExtension.asExtension()];
108+
const dynamicConfig = [
109+
enableHighlighting ? promqlHighlighter : [],
110+
promqlExtension.asExtension(),
111+
theme === 'dark' ? darkTheme : lightTheme,
112+
];
107113

108114
// Create or reconfigure the editor.
109115
const view = viewRef.current;
@@ -116,7 +122,7 @@ const CMExpressionInput: FC<PathPrefixProps & CMExpressionInputProps> = ({
116122
const startState = EditorState.create({
117123
doc: value,
118124
extensions: [
119-
theme,
125+
baseTheme,
120126
highlightSpecialChars(),
121127
history(),
122128
EditorState.allowMultipleSelections.of(true),
@@ -189,7 +195,7 @@ const CMExpressionInput: FC<PathPrefixProps & CMExpressionInputProps> = ({
189195
// re-run this effect every time that "value" changes.
190196
//
191197
// eslint-disable-next-line react-hooks/exhaustive-deps
192-
}, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory]);
198+
}, [enableAutocomplete, enableHighlighting, enableLinter, executeQuery, onExpressionChange, queryHistory, theme]);
193199

194200
return (
195201
<>

0 commit comments

Comments
 (0)