feat(ui): experimental dark theme

Very ugly, might be buggy, but need to start somewhere
This commit is contained in:
Łukasz Mierzwa
2019-10-28 17:52:24 +00:00
parent 4beec89ebc
commit 794607919a
14 changed files with 385 additions and 10 deletions

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"