Merge pull request #1098 from prymitive/dark-mode

feat(ui): experimental dark theme
This commit is contained in:
Łukasz Mierzwa
2019-10-28 19:04:16 +00:00
committed by GitHub
20 changed files with 393 additions and 62 deletions

View File

@@ -18,7 +18,6 @@ ARG VERSION
RUN CGO_ENABLED=0 make -C /src VERSION="${VERSION:-dev}" karma
FROM gcr.io/distroless/base
COPY ./docs/dark.css /themes/dark.css
COPY --from=go-builder /src/karma /karma
EXPOSE 8080
ENTRYPOINT ["/karma"]

View File

@@ -770,6 +770,7 @@ ui:
refresh: duration
hideFiltersWhenIdle: bool
colorTitlebar: bool
darkTheme: bool
minimalGroupWidth: integer
alertsPerGroup: integer
collapseGroups: string
@@ -781,6 +782,8 @@ ui:
user inactivity
- `colorTitlebar` - if enabled alert group title bar color will be set to follow
alerts in that group
- `darkTheme` - if enabled dark mode will be enabled.
Note: dark mode is *experimental* and might be buggy.
- `minimalGroupWidth` - minimal width (in pixels) for each alert group rendered
on the grid. This value is used to calculate the number of columns rendered on
the grid.
@@ -799,6 +802,7 @@ ui:
refresh: 30s
hideFiltersWhenIdle: true
colorTitlebar: false
darkTheme: false
minimalGroupWidth: 420
alertsPerGroup: 5
collapseGroups: collapsedOnMobile
@@ -832,14 +836,6 @@ custom:
Use at your own risk and be aware that used CSS class names might change without
warning. This feature is provided as is without any guarantees.
There is an example `dark.css` file providing a dark theme. It's included in the
docker image as `/themes/dark.css` and can be enabled by passing environment
variable via docker:
```shell
-e CUSTOM_CSS=/themes/dark.css
```
## Command line flags
Config file options are mapped to command line flags, so `alertmanager:interval`

View File

@@ -1,43 +0,0 @@
/* Example dark theme */
body,
.bg-primary-transparent {
background-color: #2e2727 !important;
}
.card,
.list-group-item,
.dropdown-menu,
.modal-content {
background-color: #455a64 !important;
}
.input-group-text,
.components-filterinput {
background-color: #455a64 !important;
}
.components-grid-annotation {
background-color: #95a5a6 !important;
}
.badge:not(.components-label-bright),
.form-group > label,
.nav-link {
color: #ccc !important;
}
.nav > .nav-link {
color: #ccc !important;
}
.nav > .nav-link.active,
.nav-item > .nav-link.active {
color: #fff !important;
background-color: #455a64 !important;
}
.badge.badge-light {
color: #666;
background-color: #444;
}

View File

@@ -96,6 +96,7 @@ func init() {
pflag.Duration("ui.refresh", time.Second*30, "UI refresh interval")
pflag.Bool("ui.hideFiltersWhenIdle", true, "Hide the filters bar when idle")
pflag.Bool("ui.colorTitlebar", false, "Color alert group titlebar based on alert state")
pflag.Bool("ui.darkTheme", false, "Enable dark theme")
pflag.Int("ui.minimalGroupWidth", 420, "Minimal width for each alert group on the grid")
pflag.Int("ui.alertsPerGroup", 5, "Default number of alerts to show for each alert group")
pflag.String("ui.collapseGroups", "collapsedOnMobile", "Default state for alert groups")
@@ -186,6 +187,7 @@ func (config *configSchema) Read() {
config.UI.Refresh = v.GetDuration("ui.refresh")
config.UI.HideFiltersWhenIdle = v.GetBool("ui.hideFiltersWhenIdle")
config.UI.ColorTitlebar = v.GetBool("ui.colorTitlebar")
config.UI.DarkTheme = v.GetBool("ui.darkTheme")
config.UI.MinimalGroupWidth = v.GetInt("ui.minimalGroupWidth")
config.UI.AlertsPerGroup = v.GetInt("ui.alertsPerGroup")
config.UI.CollapseGroups = v.GetString("ui.collapseGroups")

View File

@@ -136,6 +136,7 @@ ui:
refresh: 30s
hideFiltersWhenIdle: true
colorTitlebar: false
darkTheme: false
minimalGroupWidth: 420
alertsPerGroup: 5
collapseGroups: collapsedOnMobile

View File

@@ -112,6 +112,7 @@ type configSchema struct {
Refresh time.Duration
HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" mapstructure:"hideFiltersWhenIdle"`
ColorTitlebar bool `yaml:"colorTitlebar" mapstructure:"colorTitlebar"`
DarkTheme bool `yaml:"darkTheme" mapstructure:"darkTheme"`
MinimalGroupWidth int `yaml:"minimalGroupWidth" mapstructure:"minimalGroupWidth"`
AlertsPerGroup int `yaml:"alertsPerGroup" mapstructure:"alertsPerGroup"`
CollapseGroups string `yaml:"collapseGroups" mapstructure:"collapseGroups"`

View File

@@ -154,4 +154,16 @@ describe("<App />", () => {
let event = new PopStateEvent("popstate");
window.onpopstate(event);
});
it("appends 'dark-theme' class to #root if dark mode is enabled", () => {
const tree = shallow(
<App
defaultFilters={["foo=bar"]}
uiDefaults={Object.assign({}, uiDefaults, { DarkMode: true })}
/>
);
tree.instance().componentWillUnmount();
expect(document.body.className.split(" ")).toContain("dark-theme");
});
});

View File

@@ -12,11 +12,13 @@ import { FaviconBadge } from "Components/FaviconBadge";
import { ErrorBoundary } from "./ErrorBoundary";
import "./App.scss";
import "./DarkTheme.scss";
interface UIDefaults {
Refresh: number;
HideFiltersWhenIdle: boolean;
ColorTitlebar: boolean;
DarkTheme: boolean;
MinimalGroupWidth: number;
AlertsPerGroup: number;
CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile";
@@ -70,6 +72,11 @@ class App extends Component<AppProps, {}> {
componentDidMount() {
window.onpopstate = this.onPopState;
document.body.classList.toggle(
"dark-theme",
this.settingsStore.themeConfig.config.darkTheme
);
}
componentWillUnmount() {

View File

@@ -224,7 +224,7 @@ const AlertGroup = observer(
setIsMenuOpen={this.renderConfig.setIsMenuOpen}
/>
{this.collapse.value ? null : (
<div className="card-body bg-white px-2 py-1">
<div className="card-body px-2 py-1 bg-white components-grid-alertgrid-card">
<ul className="list-group">
{group.alerts
.slice(0, this.renderConfig.alertsToRender)

View File

@@ -22,7 +22,7 @@ const AlertGroupTitleBarColor = observer(
const { settingsStore } = this.props;
return (
<div className="form-group mb-0">
<div className="form-group mb-2">
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
<input

View File

@@ -0,0 +1,57 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { action } from "mobx";
import { observer } from "mobx-react";
import { Settings } from "Stores/Settings";
const ThemeConfiguration = observer(
class ThemeConfiguration extends Component {
static propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
onChange = action(event => {
const { settingsStore } = this.props;
settingsStore.themeConfig.config.darkTheme = event.target.checked;
document.body.classList.toggle(
"dark-theme",
settingsStore.themeConfig.config.darkTheme
);
});
render() {
const { settingsStore } = this.props;
return (
<div className="form-group mb-0">
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
<input
id="configuration-theme"
className="custom-control-input"
type="checkbox"
value=""
checked={settingsStore.themeConfig.config.darkTheme || false}
onChange={this.onChange}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-theme"
>
Enable dark mode
</label>
<span className="ml-5 badge badge-danger align-text-bottom">
Experimental
</span>
</span>
</div>
</div>
);
}
}
);
export { ThemeConfiguration };

View File

@@ -0,0 +1,54 @@
import React from "react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
import { ThemeConfiguration } from "./ThemeConfiguration";
let settingsStore;
beforeEach(() => {
settingsStore = new Settings();
});
const FakeConfiguration = () => {
return mount(<ThemeConfiguration settingsStore={settingsStore} />);
};
describe("<ThemeConfiguration />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("darkTheme is 'false' by default", () => {
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
});
it("unchecking the checkbox sets stored darkTheme value to 'false'", done => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-theme");
settingsStore.themeConfig.config.darkTheme = true;
expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
checkbox.simulate("change", { target: { checked: false } });
setTimeout(() => {
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
done();
}, 200);
});
it("checking the checkbox sets stored darkTheme value to 'true'", done => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-theme");
settingsStore.themeConfig.config.darkTheme = false;
expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
checkbox.simulate("change", { target: { checked: true } });
setTimeout(() => {
expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
done();
}, 200);
});
});

View File

@@ -2,7 +2,7 @@
exports[`<AlertGroupTitleBarColor /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group mb-0\\">
<div class=\\"form-group mb-2\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-colortitlebar\\"

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ThemeConfiguration /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-theme\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-theme\\"
>
Enable dark mode
</label>
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
Experimental
</span>
</span>
</div>
</div>
"
`;

View File

@@ -126,7 +126,7 @@ exports[`<Configuration /> matches snapshot 1`] = `
<div class=\\"Collapsible__trigger is-closed card-header cursor-pointer border-bottom-0\\">
<div class=\\"d-flex flex-row justify-content-between\\">
<div>
Alert group titlebar configuration
Theme
</div>
<div>
<svg aria-hidden=\\"true\\"
@@ -150,7 +150,7 @@ exports[`<Configuration /> matches snapshot 1`] = `
style=\\"height:0;-webkit-transition:height 50ms linear;-ms-transition:height 50ms linear;transition:height 50ms linear;overflow:hidden\\"
>
<div class=\\"Collapsible__contentInner card-body my-2\\">
<div class=\\"form-group mb-0\\">
<div class=\\"form-group mb-2\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input type=\\"checkbox\\"
@@ -166,6 +166,25 @@ exports[`<Configuration /> matches snapshot 1`] = `
</span>
</div>
</div>
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input type=\\"checkbox\\"
id=\\"configuration-theme\\"
class=\\"custom-control-input\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-theme\\"
>
Enable dark mode
</label>
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
Experimental
</span>
</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -10,6 +10,7 @@ import { AlertGroupWidthConfiguration } from "./AlertGroupWidthConfiguration";
import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration";
import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
import { ThemeConfiguration } from "./ThemeConfiguration";
const Configuration = ({ settingsStore }) => (
<form className="px-3 accordion">
@@ -23,8 +24,13 @@ const Configuration = ({ settingsStore }) => (
content={<FilterBarConfiguration settingsStore={settingsStore} />}
/>
<Accordion
text="Alert group titlebar configuration"
content={<AlertGroupTitleBarColor settingsStore={settingsStore} />}
text="Theme"
content={
<React.Fragment>
<AlertGroupTitleBarColor settingsStore={settingsStore} />
<ThemeConfiguration settingsStore={settingsStore} />
</React.Fragment>
}
/>
<Accordion
text="Minimal alert group width"

View File

@@ -145,7 +145,7 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
<div class=\\"Collapsible__trigger is-closed card-header cursor-pointer border-bottom-0\\">
<div class=\\"d-flex flex-row justify-content-between\\">
<div>
Alert group titlebar configuration
Theme
</div>
<div>
<svg aria-hidden=\\"true\\"
@@ -169,7 +169,7 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
style=\\"height: 0px; transition: height 50ms linear; overflow: hidden;\\"
>
<div class=\\"Collapsible__contentInner card-body my-2\\">
<div class=\\"form-group mb-0\\">
<div class=\\"form-group mb-2\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-colortitlebar\\"
@@ -185,6 +185,25 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</span>
</div>
</div>
<div class=\\"form-group mb-0\\">
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-theme\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-theme\\"
>
Enable dark mode
</label>
<span class=\\"ml-5 badge badge-danger align-text-bottom\\">
Experimental
</span>
</span>
</div>
</div>
</div>
</div>
</div>

159
ui/src/DarkTheme.scss Normal file
View File

@@ -0,0 +1,159 @@
@import "src/App.scss";
$alertgroup-body-bg: #5a6e7b;
$alertgroup-header-bg: #515658;
$alertgroup-footer-bg: #728998;
$alertgroup-border-inside: #485862;
$silence-bg: #c3cdd5;
$silence-progress-bg: #eaeef0;
$badge-light-bg: #d2dae0;
.dark-theme .card.bg-light {
border-color: #3c3e3e !important;
}
.dark-theme .components-grid-alertgrid-card {
&.card-body,
& > .list-group,
& > .list-group > .list-group-item {
background-color: $alertgroup-body-bg !important;
}
}
.dark-theme .card.bg-light > .card-header {
background-color: $alertgroup-header-bg !important;
}
.dark-theme .card > .card-header {
border-bottom-color: $alertgroup-border-inside !important;
}
.dark-theme .bg-card-footer-default {
background-color: $alertgroup-footer-bg !important;
border-top-color: $alertgroup-border-inside !important;
}
.dark-theme .btn.bg-white {
background-color: $alertgroup-body-bg !important;
}
.dark-theme {
& .components-grid-annotation .text-muted,
& .components-managed-silence-cite .text-muted,
& .components-managed-silence svg.text-muted,
& .text-muted.fa-bell-slash {
color: $dark !important;
}
& .text-muted {
color: $white !important;
}
}
.dark-theme .components-managed-silence {
border-left-color: $dark;
& > .card-header,
& > .card-body {
background-color: $silence-bg !important;
}
}
.dark-theme {
& .modal-content {
background-color: $alertgroup-body-bg;
& .list-group-item {
background-color: $alertgroup-body-bg;
}
}
& .modal-header {
border-bottom-color: $alertgroup-border-inside;
}
& .modal-footer {
border-top-color: $alertgroup-border-inside;
}
& .components-grid-annotation.bg-light {
background-color: $silence-bg !important;
}
& .badge.badge-light {
background-color: $badge-light-bg !important;
}
& .components-filteredinputlabel > .badge-pill.badge-light {
background-color: $badge-light-bg !important;
filter: none;
}
& .dropdown-menu {
background-color: $silence-bg !important;
}
& .dropdown-header {
color: $dark !important;
}
& .progress.bg-white {
background-color: $silence-progress-bg !important;
}
}
.dark-theme {
& .react-select__control,
& .react-select__indicators,
& .react-select__value-container {
background-color: $alertgroup-body-bg !important;
}
& .react-select__menu {
background-color: $silence-bg !important;
}
& .tab-content,
& .tab-content .text-muted,
& .nav-item:not(.components-tab-inactive),
& .custom-control-label,
& .react-select__placeholder {
color: $white !important;
}
& .nav-link.active {
color: $black !important;
background-color: $silence-bg !important;
}
& .form-control {
color: $gray-200 !important;
background-color: $alertgroup-body-bg !important;
}
& .input-group-text {
background-color: $silence-bg !important;
}
& .react-datepicker,
& .react-datepicker__header,
& .react-datepicker__today-button {
background-color: $silence-bg !important;
}
}
.dark-theme {
& .jumbotron.bg-white {
background-color: $alertgroup-body-bg !important;
}
}
.dark-theme {
& .collapse,
& .collapse > .card-body {
color: $white;
background-color: $alertgroup-body-bg !important;
}
& .Collapsible > .card-header {
background-color: $alertgroup-footer-bg !important;
border-bottom-color: $alertgroup-border-inside !important;
}
& .input-range__label,
& .Collapsible__trigger.bg-light {
color: $white;
}
& .Collapsible__trigger.is-closed {
color: $black;
}
}

View File

@@ -110,6 +110,20 @@ class FilterBarConfig {
}
}
class ThemeConfig {
constructor(darkTheme) {
this.config = localStored(
"themeConfig",
{
darkTheme: darkTheme
},
{
delay: 100
}
);
}
}
class Settings {
constructor(defaults) {
let defaultSettings;
@@ -118,6 +132,7 @@ class Settings {
Refresh: 30 * 1000 * 1000 * 1000,
HideFiltersWhenIdle: true,
ColorTitlebar: false,
DarkTheme: false,
MinimalGroupWidth: 420,
AlertsPerGroup: 5,
CollapseGroups: "collapsedOnMobile"
@@ -140,6 +155,7 @@ class Settings {
this.filterBarConfig = new FilterBarConfig(
defaultSettings.HideFiltersWhenIdle
);
this.themeConfig = new ThemeConfig(defaultSettings.DarkTheme);
}
}

View File

@@ -1,9 +1,10 @@
const DefaultsBase64 =
"eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJNaW5pbWFsR3JvdXBXaWR0aCI6NTU1LCJBbGVydHNQZXJHcm91cCI6MTUsIkNvbGxhcHNlR3JvdXBzIjoiZXhwYW5kZWQifQo=";
"eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJEYXJrTW9kZSI6ZmFsc2UsIk1pbmltYWxHcm91cFdpZHRoIjo1NTUsIkFsZXJ0c1Blckdyb3VwIjoxNSwiQ29sbGFwc2VHcm91cHMiOiJleHBhbmRlZCJ9Cg==";
const DefaultsObject = {
Refresh: 45000000000,
HideFiltersWhenIdle: false,
ColorTitlebar: false,
DarkMode: false,
MinimalGroupWidth: 555,
AlertsPerGroup: 15,
CollapseGroups: "expanded"