Merge pull request #636 from prymitive/color-titlebar

feat(ui): add colored second display style for titlebar
This commit is contained in:
Łukasz Mierzwa
2019-04-19 21:43:59 +01:00
committed by GitHub
15 changed files with 358 additions and 36 deletions

View File

@@ -13,6 +13,7 @@ const StateLabelClassMap = Object.freeze({
suppressed: "badge-success components-label-dark",
unprocessed: "badge-secondary components-label-bright"
});
// same but for borders
const BorderClassMap = Object.freeze({
active: "border-danger",
@@ -20,10 +21,17 @@ const BorderClassMap = Object.freeze({
unprocessed: "border-secondary"
});
const BackgroundClassMap = Object.freeze({
active: "bg-danger",
suppressed: "bg-success",
unprocessed: "bg-secondary"
});
export {
DefaultLabelClass,
StaticColorLabelClass,
AlertNameLabelClass,
StateLabelClassMap,
BorderClassMap
BorderClassMap,
BackgroundClassMap
};

View File

@@ -86,7 +86,8 @@ const GroupMenu = observer(
class GroupMenu extends Component {
static propTypes = {
group: APIGroup.isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
themed: PropTypes.bool.isRequired
};
collapse = observable(
@@ -108,7 +109,7 @@ const GroupMenu = observer(
});
render() {
const { group, silenceFormStore } = this.props;
const { group, silenceFormStore, themed } = this.props;
return (
<Manager>
@@ -117,7 +118,9 @@ const GroupMenu = observer(
<span
ref={ref}
onClick={this.collapse.toggle}
className={`text-muted cursor-pointer badge text-nowrap text-truncate pl-0 components-grid-alertgroup-${
className={`${
themed ? "text-white" : "text-muted"
} cursor-pointer badge text-nowrap text-truncate pl-0 components-grid-alertgroup-${
group.id
}`}
data-toggle="dropdown"

View File

@@ -21,10 +21,14 @@ beforeEach(() => {
const MockAfterClick = jest.fn();
const MountedGroupMenu = group => {
const MountedGroupMenu = (group, themed) => {
return mount(
<Provider alertStore={alertStore}>
<GroupMenu group={group} silenceFormStore={silenceFormStore} />
<GroupMenu
group={group}
silenceFormStore={silenceFormStore}
themed={themed}
/>
</Provider>
).find("GroupMenu");
};
@@ -32,13 +36,13 @@ const MountedGroupMenu = group => {
describe("<GroupMenu />", () => {
it("is collapsed by default", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
const tree = MountedGroupMenu(group, true);
expect(tree.instance().collapse.value).toBe(true);
});
it("clicking toggle sets collapse value to 'false'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
const tree = MountedGroupMenu(group, true);
const toggle = tree.find(".cursor-pointer");
toggle.simulate("click");
expect(tree.instance().collapse.value).toBe(false);
@@ -46,7 +50,7 @@ describe("<GroupMenu />", () => {
it("handleClickOutside() call sets collapse value to 'true'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {}, {});
const tree = MountedGroupMenu(group);
const tree = MountedGroupMenu(group, true);
const toggle = tree.find(".cursor-pointer");
toggle.simulate("click");

View File

@@ -22,20 +22,32 @@ const GroupHeader = observer(
toggle: PropTypes.func.isRequired
}).isRequired,
group: APIGroup.isRequired,
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired
silenceFormStore: PropTypes.instanceOf(SilenceFormStore).isRequired,
headerBackgroundClass: PropTypes.string.isRequired,
themedCounters: PropTypes.bool.isRequired
};
render() {
const { collapseStore, group, silenceFormStore } = this.props;
const {
collapseStore,
group,
silenceFormStore,
headerBackgroundClass,
themedCounters
} = this.props;
return (
<h5
className={`card-header bg-light mb-0 d-flex flex-row px-2 py-1 ${
className={`card-header ${headerBackgroundClass} mb-0 d-flex flex-row px-2 py-1 ${
collapseStore.value ? "border-bottom-0" : ""
}`}
>
<span className="flex-shrink-0 flex-grow-0">
<GroupMenu group={group} silenceFormStore={silenceFormStore} />
<GroupMenu
group={group}
silenceFormStore={silenceFormStore}
themed={!themedCounters}
/>
</span>
<span className="flex-shrink-1 flex-grow-1" style={{ minWidth: 0 }}>
{Object.keys(group.labels).map(name => (
@@ -51,19 +63,24 @@ const GroupHeader = observer(
name="@state"
value="unprocessed"
counter={group.stateCount.unprocessed}
themed={themedCounters}
/>
<FilteringCounterBadge
name="@state"
value="suppressed"
counter={group.stateCount.suppressed}
themed={themedCounters}
/>
<FilteringCounterBadge
name="@state"
value="active"
counter={group.stateCount.active}
themed={themedCounters}
/>
<span
className="text-muted cursor-pointer badge text-nowrap text-truncate px-0"
className={`${
themedCounters ? "text-muted" : "text-white"
} cursor-pointer badge text-nowrap text-truncate px-0`}
onClick={collapseStore.toggle}
>
<TooltipWrapper title="Toggle group details">

View File

@@ -14,6 +14,7 @@ import { APIGroup } from "Models/API";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { IsMobile } from "Common/Device";
import { BackgroundClassMap, BorderClassMap } from "Common/Colors";
import { MountFade } from "Components/Animations/MountFade";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { GroupHeader } from "./GroupHeader";
@@ -156,6 +157,7 @@ const AlertGroup = observer(
showAlertmanagers,
afterUpdate,
silenceFormStore,
settingsStore,
style
} = this.props;
@@ -176,14 +178,34 @@ const AlertGroup = observer(
}
}
let themedCounters = true;
const groupClassesMap = {
background: "bg-light",
border: "border-light"
};
if (settingsStore.alertGroupConfig.config.colorTitleBar) {
const stateList = Object.entries(group.stateCount)
.filter(([k, v]) => v !== 0)
.map(([k, _]) => k);
if (stateList.length === 1) {
const state = stateList.pop();
groupClassesMap.background = BackgroundClassMap[state];
groupClassesMap.border = BorderClassMap[state];
themedCounters = false;
}
}
return (
<div className="components-grid-alertgrid-alertgroup p-1" style={style}>
<MountFade in={true}>
<div className="card">
<div className={`card ${groupClassesMap.border}`}>
<GroupHeader
collapseStore={this.collapse}
group={group}
silenceFormStore={silenceFormStore}
headerBackgroundClass={groupClassesMap.background}
themedCounters={themedCounters}
/>
{this.collapse.value ? null : (
<div className="card-body px-2 py-1">

View File

@@ -274,3 +274,68 @@ describe("<AlertGroup /> renderConfig", () => {
ValidateLoadButtonAction(25, 1, /fa-plus/, 22, 17);
});
});
describe("<AlertGroup /> theme", () => {
it("renders bg-light border when colorTitleBar=false", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = false;
group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find(".card").hasClass("border-light")).toBe(true);
expect(tree.find(".card").hasClass("border-danger")).toBe(false);
expect(tree.find(".card").hasClass("border-success")).toBe(false);
expect(tree.find(".card").hasClass("border-secondary")).toBe(false);
});
it("renders themed titlebar when colorTitleBar=false", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = false;
group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find("GroupHeader").props().themedCounters).toBe(true);
expect(tree.find(".card-header").hasClass("bg-light")).toBe(true);
expect(tree.find(".card-header").hasClass("bg-danger")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-success")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false);
});
it("renders bg-light border when colorTitleBar=true and there are multiple alert states", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = false;
group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find(".card").hasClass("border-light")).toBe(true);
expect(tree.find(".card").hasClass("border-danger")).toBe(false);
expect(tree.find(".card").hasClass("border-success")).toBe(false);
expect(tree.find(".card").hasClass("border-secondary")).toBe(false);
});
it("renders themed titlebar when colorTitleBar=true and there are multiple alert states", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = true;
group.stateCount = { active: 5, suppressed: 6, unprocessed: 7 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find("GroupHeader").props().themedCounters).toBe(true);
expect(tree.find(".card-header").hasClass("bg-light")).toBe(true);
expect(tree.find(".card-header").hasClass("bg-danger")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-success")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false);
});
it("renders state based border when colorTitleBar=true and there's only one alert state", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = true;
group.stateCount = { active: 0, suppressed: 5, unprocessed: 0 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find(".card").hasClass("border-light")).toBe(false);
expect(tree.find(".card").hasClass("border-danger")).toBe(false);
expect(tree.find(".card").hasClass("border-success")).toBe(true);
expect(tree.find(".card").hasClass("border-secondary")).toBe(false);
});
it("renders unthemed titlebar when colorTitleBar=true and there's only one alert state", () => {
settingsStore.alertGroupConfig.config.colorTitleBar = true;
group.stateCount = { active: 5, suppressed: 0, unprocessed: 0 };
const tree = MountedAlertGroup(jest.fn(), false);
expect(tree.find("GroupHeader").props().themedCounters).toBe(false);
expect(tree.find(".card-header").hasClass("bg-light")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-danger")).toBe(true);
expect(tree.find(".card-header").hasClass("bg-success")).toBe(false);
expect(tree.find(".card-header").hasClass("bg-secondary")).toBe(false);
});
});

View File

@@ -17,15 +17,16 @@ const FilteringCounterBadge = inject("alertStore")(
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
counter: PropTypes.number.isRequired
counter: PropTypes.number.isRequired,
themed: PropTypes.bool.isRequired
};
render() {
const { name, value, counter } = this.props;
const { name, value, counter, themed } = this.props;
if (counter === 0) return null;
let cs = this.getClassAndStyle(
const cs = this.getClassAndStyle(
name,
value,
"badge-pill components-label-with-hover"
@@ -36,8 +37,15 @@ const FilteringCounterBadge = inject("alertStore")(
title={`Click to only show ${value} alerts or Alt+Click to hide them`}
>
<span
className={cs.className}
style={cs.style}
className={
themed
? cs.className
: [
"badge-light badge-pill components-label-with-hover",
...cs.baseClassNames
].join(" ")
}
style={themed ? {} : cs.style}
onClick={e => this.handleClick(e)}
>
{counter}

View File

@@ -12,37 +12,40 @@ beforeEach(() => {
alertStore = new AlertStore([]);
});
const validateClassName = (value, className) => {
const validateClassName = (value, className, themed) => {
const tree = mount(
<FilteringCounterBadge
alertStore={alertStore}
name="@state"
value={value}
counter={1}
themed={themed}
/>
);
expect(tree.find("span").hasClass(className)).toBe(true);
};
const validateStyle = value => {
const validateStyle = (value, themed) => {
const tree = mount(
<FilteringCounterBadge
alertStore={alertStore}
name="@state"
value={value}
counter={1}
themed={themed}
/>
);
expect(tree.find("span").prop("style")).toEqual({});
};
const validateOnClick = value => {
const validateOnClick = (value, themed) => {
const tree = mount(
<FilteringCounterBadge
alertStore={alertStore}
name="@state"
value={value}
counter={1}
themed={themed}
/>
);
tree.find(".components-label").simulate("click");
@@ -53,24 +56,27 @@ const validateOnClick = value => {
};
describe("<FilteringCounterBadge />", () => {
it("@state=unprocessed counter badge should have className 'badge-secondary'", () => {
validateClassName("unprocessed", "badge-secondary");
it("themed @state=unprocessed counter badge should have className 'badge-secondary'", () => {
validateClassName("unprocessed", "badge-secondary", true);
});
it("@state=active counter badge should have className 'badge-secondary'", () => {
validateClassName("active", "badge-danger");
it("themed @state=active counter badge should have className 'badge-secondary'", () => {
validateClassName("active", "badge-danger", true);
});
it("@state=suppressed counter badge should have className 'badge-secondary'", () => {
validateClassName("suppressed", "badge-success");
it("themed @state=suppressed counter badge should have className 'badge-secondary'", () => {
validateClassName("suppressed", "badge-success", true);
});
it("unthemed @state=suppressed counter badge should have className 'badge-light'", () => {
validateClassName("suppressed", "badge-light", false);
});
it("@state=unprocessed counter badge should have empty style", () => {
validateStyle("unprocessed");
validateStyle("unprocessed", true);
});
it("@state=active counter badge should have empty style", () => {
validateStyle("active");
validateStyle("active", true);
});
it("@state=suppressed counter badge should have empty style", () => {
validateStyle("suppressed");
validateStyle("suppressed", true);
});
it("counter badge should have correct children based on the counter prop value", () => {
@@ -80,18 +86,19 @@ describe("<FilteringCounterBadge />", () => {
name="@state"
value="active"
counter={123}
themed={true}
/>
);
expect(tree.text()).toBe("123");
});
it("onClick method on @state=unprocessed counter badge should add a new filter", () => {
validateOnClick("unprocessed");
validateOnClick("unprocessed", true);
});
it("onClick method on @state=active counter badge should add a new filter", () => {
validateOnClick("active");
validateOnClick("active", true);
});
it("onClick method on @state=suppressed counter badge should add a new filter", () => {
validateOnClick("suppressed");
validateOnClick("suppressed", true);
});
});

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 AlertGroupTitleBarColor = observer(
class AlertGroupTitleBarColor extends Component {
static propTypes = {
settingsStore: PropTypes.instanceOf(Settings).isRequired
};
onChange = action(event => {
const { settingsStore } = this.props;
settingsStore.alertGroupConfig.config.colorTitleBar =
event.target.checked;
});
render() {
const { settingsStore } = this.props;
return (
<div className="form-group">
<div className="text-center">
<label className="mb-2 font-weight-bold">
Alert group titlebar configuration
</label>
</div>
<div className="form-check form-check-inline">
<span className="custom-control custom-switch">
<input
id="configuration-colortitlebar"
className="custom-control-input"
type="checkbox"
value=""
checked={
settingsStore.alertGroupConfig.config.colorTitleBar || false
}
onChange={this.onChange}
/>
<label
className="custom-control-label cursor-pointer mr-3"
htmlFor="configuration-colortitlebar"
>
Color group titlebar
</label>
</span>
</div>
</div>
);
}
}
);
export { AlertGroupTitleBarColor };

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 { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
let settingsStore;
beforeEach(() => {
settingsStore = new Settings();
});
const FakeConfiguration = () => {
return mount(<AlertGroupTitleBarColor settingsStore={settingsStore} />);
};
describe("<AlertGroupTitleBarColor />", () => {
it("matches snapshot with default values", () => {
const tree = FakeConfiguration();
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("colorTitleBar is 'false' by default", () => {
expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false);
});
it("unchecking the checkbox sets stored colorTitleBar value to 'false'", done => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-colortitlebar");
settingsStore.alertGroupConfig.config.colorTitleBar = true;
expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(true);
checkbox.simulate("change", { target: { checked: false } });
setTimeout(() => {
expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false);
done();
}, 200);
});
it("checking the checkbox sets stored colorTitleBar value to 'true'", done => {
const tree = FakeConfiguration();
const checkbox = tree.find("#configuration-colortitlebar");
settingsStore.alertGroupConfig.config.colorTitleBar = false;
expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(false);
checkbox.simulate("change", { target: { checked: true } });
setTimeout(() => {
expect(settingsStore.alertGroupConfig.config.colorTitleBar).toBe(true);
done();
}, 200);
});
});

View File

@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AlertGroupTitleBarColor /> matches snapshot with default values 1`] = `
"
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">
Alert group titlebar configuration
</label>
</div>
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-colortitlebar\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-colortitlebar\\"
>
Color group titlebar
</label>
</span>
</div>
</div>
"
`;

View File

@@ -72,6 +72,29 @@ exports[`<Configuration /> matches snapshot 1`] = `
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">
Alert group titlebar configuration
</label>
</div>
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input type=\\"checkbox\\"
id=\\"configuration-colortitlebar\\"
class=\\"custom-control-input\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-colortitlebar\\"
>
Color group titlebar
</label>
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Minimal alert group width

View File

@@ -8,6 +8,7 @@ import { AlertGroupConfiguration } from "./AlertGroupConfiguration";
import { AlertGroupWidthConfiguration } from "./AlertGroupWidthConfiguration";
import { AlertGroupSortConfiguration } from "./AlertGroupSortConfiguration";
import { AlertGroupCollapseConfiguration } from "./AlertGroupCollapseConfiguration";
import { AlertGroupTitleBarColor } from "./AlertGroupTitleBarColor";
const Configuration = ({ settingsStore }) => (
<form className="px-3">
@@ -15,6 +16,8 @@ const Configuration = ({ settingsStore }) => (
<div className="mt-5" />
<FilterBarConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupTitleBarColor settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupWidthConfiguration settingsStore={settingsStore} />
<div className="mt-5" />
<AlertGroupConfiguration settingsStore={settingsStore} />

View File

@@ -91,6 +91,29 @@ exports[`<MainModalContent /> matches snapshot 1`] = `
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group\\">
<div class=\\"text-center\\">
<label class=\\"mb-2 font-weight-bold\\">
Alert group titlebar configuration
</label>
</div>
<div class=\\"form-check form-check-inline\\">
<span class=\\"custom-control custom-switch\\">
<input id=\\"configuration-colortitlebar\\"
class=\\"custom-control-input\\"
type=\\"checkbox\\"
value
>
<label class=\\"custom-control-label cursor-pointer mr-3\\"
for=\\"configuration-colortitlebar\\"
>
Color group titlebar
</label>
</span>
</div>
</div>
<div class=\\"mt-5\\">
</div>
<div class=\\"form-group text-center\\">
<label class=\\"mb-4 font-weight-bold\\">
Minimal alert group width

View File

@@ -45,7 +45,8 @@ class AlertGroupConfig {
"alertGroupConfig",
{
defaultRenderCount: 5,
defaultCollapseState: this.options.collapsedOnMobile.value
defaultCollapseState: this.options.collapsedOnMobile.value,
colorTitleBar: false
},
{ delay: 100 }
);