fix(ui): rewrite label components with hooks

This commit is contained in:
Łukasz Mierzwa
2020-04-28 21:18:26 +01:00
committed by Łukasz Mierzwa
parent 55463e9ef5
commit 06d8eb6089
12 changed files with 447 additions and 501 deletions

View File

@@ -1,90 +0,0 @@
import { Component } from "react";
import PropTypes from "prop-types";
import { AlertStore } from "Stores/AlertStore";
import {
StaticColorLabelClassMap,
DefaultLabelClassMap,
AlertNameLabelClassMap,
StateLabelClassMap,
} from "Common/Colors";
import { QueryOperators, FormatQuery, StaticLabels } from "Common/Query";
const isBackgroundDark = (brightness) => brightness <= 125;
// base class for shared code, not used directly
class BaseLabel extends Component {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};
getClassAndStyle(name, value, extraClass, baseClass) {
const { alertStore } = this.props;
const elementType = baseClass || "badge";
const data = {
style: {},
className: "",
baseClassNames: ["components-label", elementType],
colorClassNames: [],
};
if (name === StaticLabels.AlertName) {
data.colorClassNames.push(AlertNameLabelClassMap[elementType]);
} else if (name === StaticLabels.State) {
data.colorClassNames.push(
StateLabelClassMap[value]
? `${elementType}-${StateLabelClassMap[value]}`
: DefaultLabelClassMap[elementType]
);
} else if (alertStore.settings.values.staticColorLabels.includes(name)) {
data.colorClassNames.push(StaticColorLabelClassMap[elementType]);
} else {
const c = alertStore.data.getColorData(name, value);
if (c) {
// if there's color information use it
data.style["backgroundColor"] = `rgba(${[
c.background.red,
c.background.green,
c.background.blue,
c.background.alpha,
].join(", ")})`;
data.colorClassNames.push(
isBackgroundDark(c.brightness)
? "components-label-dark"
: "components-label-bright"
);
data.colorClassNames.push(
`components-label-brightness-${Math.round(c.brightness / 25)}`
);
} else {
// if not fall back to class
data.colorClassNames.push(DefaultLabelClassMap[elementType]);
}
}
data.className = `${[...data.baseClassNames, ...data.colorClassNames].join(
" "
)} ${extraClass || ""}`;
return data;
}
handleClick = (event) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true ? QueryOperators.NotEqual : QueryOperators.Equal;
event.preventDefault();
const { name, value, alertStore } = this.props;
alertStore.filters.addFilter(FormatQuery(name, operator, value));
};
}
export { BaseLabel };

View File

@@ -1,114 +0,0 @@
import React from "react";
import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import {
StaticColorLabelClassMap,
DefaultLabelClassMap,
AlertNameLabelClassMap,
StateLabelClassMap,
} from "Common/Colors";
import { BaseLabel } from ".";
let alertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
});
const FakeBaseLabel = (name = "foo", value = "bar") => {
class RenderableBaseLabel extends BaseLabel {
render() {
const { name, value } = this.props;
let cs = this.getClassAndStyle(name, value);
return (
<span className={cs.className} style={cs.style}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
);
}
}
return shallow(
<RenderableBaseLabel alertStore={alertStore} name={name} value={value} />
);
};
describe("<BaseLabel />", () => {
it("static label uses StaticColorLabelClassMap.badge", () => {
alertStore.settings.values.staticColorLabels = ["foo", "job", "bar"];
const tree = FakeBaseLabel();
expect(
tree.find(".components-label").hasClass(StaticColorLabelClassMap.badge)
).toBe(true);
});
Object.entries(StaticColorLabelClassMap).map(([key, val]) =>
it(`non-static label doesn't use StaticColorLabelClassMap.${key}`, () => {
alertStore.settings.values.staticColorLabels = [];
const tree = FakeBaseLabel();
expect(tree.find(".components-label").hasClass(val)).toBe(false);
})
);
it("label with no special color information should use DefaultLabelClassMap.badge", () => {
const tree = FakeBaseLabel();
expect(
tree.find(".components-label").hasClass(DefaultLabelClassMap.badge)
).toBe(true);
});
it("alertname label should use AlertNameLabelClassMap.badge", () => {
const tree = FakeBaseLabel("alertname", "foo");
expect(
tree.find(".components-label").hasClass(AlertNameLabelClassMap.badge)
).toBe(true);
});
it("@state=active label should use StateLabelClassMap.active class", () => {
const tree = FakeBaseLabel("@state", "active");
expect(
tree
.find(".components-label")
.hasClass(`badge-${StateLabelClassMap.active}`)
).toBe(true);
});
it("@state=suppressed label should use StateLabelClassMap.suppressed class", () => {
const tree = FakeBaseLabel("@state", "suppressed");
expect(
tree
.find(".components-label")
.hasClass(`badge-${StateLabelClassMap.suppressed}`)
).toBe(true);
});
it("@state=unprocessed label should use StateLabelClassMap.unprocessed class", () => {
const tree = FakeBaseLabel("@state", "unprocessed");
expect(
tree
.find(".components-label")
.hasClass(`badge-${StateLabelClassMap.unprocessed}`)
).toBe(true);
});
it("@state with unknown label should use DefaultLabelClassMap.badge", () => {
const tree = FakeBaseLabel("@state", "foobar");
expect(
tree.find(".components-label").hasClass(DefaultLabelClassMap.badge)
).toBe(true);
});
it("style prop on a label included in staticColorLabels should be empty", () => {
alertStore.settings.values.staticColorLabels = ["foo", "job", "bar"];
const tree = FakeBaseLabel();
expect(tree.find(".components-label").props().style).toEqual({});
});
it("style prop on a label without any color information should be empty", () => {
alertStore.settings.values.staticColorLabels = [];
const tree = FakeBaseLabel();
expect(tree.find(".components-label").props().style).toEqual({});
});
});

View File

@@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
import { useObserver } from "mobx-react";
import { RIEInput } from "@attently/riek";
@@ -13,103 +13,89 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
import { AlertStore } from "Stores/AlertStore";
import { QueryOperators } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
const FilterInputLabel = observer(
class FilterInputLabel extends BaseLabel {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
filter: PropTypes.shape({
raw: PropTypes.string,
applied: PropTypes.bool,
isValid: PropTypes.bool,
hits: PropTypes.number,
name: PropTypes.string,
matcher: PropTypes.string,
value: PropTypes.string,
}),
};
onChange = (update) => {
const { alertStore, filter } = this.props;
// if filter is empty string then remove it
if (update.raw === "") {
alertStore.filters.removeFilter(filter.raw);
}
// if not empty replace it
alertStore.filters.replaceFilter(filter.raw, update.raw);
};
render() {
const { filter, alertStore } = this.props;
let cs = this.getClassAndStyle(
filter.matcher === QueryOperators.Equal ? filter.name : "",
filter.matcher === QueryOperators.Equal ? filter.value : "",
"components-filteredinputlabel btn-sm",
"btn"
);
const showCounter =
alertStore.filters.values.filter(
(f) => f.hits !== alertStore.info.totalAlerts
).length > 0;
const rootClasses = filter.applied
? cs.className
: [
"btn-secondary btn-sm components-filteredinputlabel",
...cs.baseClassNames,
].join(" ");
return (
<button
type="button"
className={`${rootClasses} d-inline-flex flex-row align-items-center`}
style={filter.applied ? cs.style : {}}
>
{filter.isValid ? (
filter.applied ? (
showCounter ? (
<span className="badge badge-light badge-pill">
{filter.hits}
</span>
) : null
) : (
<FontAwesomeIcon icon={faSpinner} spin />
)
) : (
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-danger"
/>
)}
<TooltipWrapper
title="Click to edit this filter"
className="components-filteredinputlabel-text flex-grow-1 flex-shrink-1 ml-1"
>
<RIEInput
className="cursor-text px-1"
defaultValue=""
value={filter.raw}
propName="raw"
change={this.onChange}
classEditing="py-0 border-0 editing rounded"
afterStart={alertStore.status.pause}
afterFinish={alertStore.status.resume}
/>
</TooltipWrapper>
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
icon={faTimes}
onClick={() => alertStore.filters.removeFilter(filter.raw)}
/>
</button>
);
const FilterInputLabel = ({ alertStore, filter }) => {
const onChange = ({ raw }) => {
// if filter is empty string then remove it
if (raw === "") {
alertStore.filters.removeFilter(filter.raw);
}
}
);
// if not empty replace it
alertStore.filters.replaceFilter(filter.raw, raw);
};
const cs = GetClassAndStyle(
alertStore,
filter.matcher === QueryOperators.Equal ? filter.name : "",
filter.matcher === QueryOperators.Equal ? filter.value : "",
"components-filteredinputlabel btn-sm",
"btn"
);
const showCounter =
alertStore.filters.values.filter(
(f) => f.hits !== alertStore.info.totalAlerts
).length > 0;
const rootClasses = filter.applied
? cs.className
: [
"btn-secondary btn-sm components-filteredinputlabel",
...cs.baseClassNames,
].join(" ");
return useObserver(() => (
<button
type="button"
className={`${rootClasses} d-inline-flex flex-row align-items-center`}
style={filter.applied ? cs.style : {}}
>
{filter.isValid ? (
filter.applied ? (
showCounter ? (
<span className="badge badge-light badge-pill">{filter.hits}</span>
) : null
) : (
<FontAwesomeIcon icon={faSpinner} spin />
)
) : (
<FontAwesomeIcon icon={faExclamationCircle} className="text-danger" />
)}
<TooltipWrapper
title="Click to edit this filter"
className="components-filteredinputlabel-text flex-grow-1 flex-shrink-1 ml-1"
>
<RIEInput
className="cursor-text px-1"
defaultValue=""
value={filter.raw}
propName="raw"
change={onChange}
classEditing="py-0 border-0 editing rounded"
afterStart={alertStore.status.pause}
afterFinish={alertStore.status.resume}
/>
</TooltipWrapper>
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
icon={faTimes}
onClick={() => alertStore.filters.removeFilter(filter.raw)}
/>
</button>
));
};
FilterInputLabel.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
filter: PropTypes.shape({
raw: PropTypes.string,
applied: PropTypes.bool,
isValid: PropTypes.bool,
hits: PropTypes.number,
name: PropTypes.string,
matcher: PropTypes.string,
value: PropTypes.string,
}),
};
export { FilterInputLabel };

View File

@@ -48,7 +48,9 @@ const ValidateOnChange = (newRaw) => {
filter={alertStore.filters.values[0]}
/>
);
tree.instance().onChange({ raw: newRaw });
const input = tree.find("RIEInput");
input.props().change({ raw: newRaw });
return tree;
};

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
@@ -6,70 +6,84 @@ import { observer } from "mobx-react";
import Flash from "react-reveal/Flash";
import { AlertStore } from "Stores/AlertStore";
import { QueryOperators, FormatQuery } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
// Same as FilteringLabel but for labels that are counters (usually @state)
// and only renders a pill badge with the counter, it doesn't render anything
// if the counter is 0
const FilteringCounterBadge = observer(
class FilteringCounterBadge extends BaseLabel {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
counter: PropTypes.number.isRequired,
themed: PropTypes.bool.isRequired,
alwaysVisible: PropTypes.bool,
defaultColor: PropTypes.oneOf(["light", "primary"]),
};
static defaultProps = {
defaultColor: "light",
};
({
alertStore,
name,
value,
counter,
themed,
alwaysVisible,
defaultColor,
}) => {
const handleClick = useCallback(
(event) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true
? QueryOperators.NotEqual
: QueryOperators.Equal;
render() {
const {
name,
value,
counter,
themed,
alwaysVisible,
defaultColor,
} = this.props;
event.preventDefault();
if (!alwaysVisible && counter === 0) return null;
alertStore.filters.addFilter(FormatQuery(name, operator, value));
},
[alertStore.filters, name, value]
);
const cs = this.getClassAndStyle(
name,
value,
"badge-pill components-label-with-hover"
);
if (!alwaysVisible && counter === 0) return null;
return (
<TooltipWrapper
title={`Click to only show ${name}=${value} alerts or Alt+Click to hide them`}
>
<Flash spy={counter}>
<span
className={
themed
? cs.className
: [
`badge-${defaultColor}`,
"badge-pill components-label-with-hover",
...cs.baseClassNames,
].join(" ")
}
style={themed ? {} : cs.style}
onClick={(e) => this.handleClick(e)}
>
{counter}
</span>
</Flash>
</TooltipWrapper>
);
}
const cs = GetClassAndStyle(
alertStore,
name,
value,
"badge-pill components-label-with-hover"
);
return (
<TooltipWrapper
title={`Click to only show ${name}=${value} alerts or Alt+Click to hide them`}
>
<Flash spy={counter}>
<span
className={
themed
? cs.className
: [
`badge-${defaultColor}`,
"badge-pill components-label-with-hover",
...cs.baseClassNames,
].join(" ")
}
style={themed ? {} : cs.style}
onClick={handleClick}
>
{counter}
</span>
</Flash>
</TooltipWrapper>
);
}
);
FilteringCounterBadge.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
counter: PropTypes.number.isRequired,
themed: PropTypes.bool.isRequired,
alwaysVisible: PropTypes.bool,
defaultColor: PropTypes.oneOf(["light", "primary"]),
};
FilteringCounterBadge.defaultProps = {
defaultColor: "light",
};
export { FilteringCounterBadge };

View File

@@ -3,7 +3,7 @@ import React from "react";
import { mount, render } from "enzyme";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { QueryOperators } from "Common/Query";
import { FilteringCounterBadge } from ".";
let alertStore;
@@ -39,7 +39,7 @@ const validateStyle = (value, themed) => {
expect(tree.find("span").prop("style")).toEqual({ opacity: 1 });
};
const validateOnClick = (value, themed) => {
const validateOnClick = (value, themed, isNegative) => {
const tree = mount(
<FilteringCounterBadge
alertStore={alertStore}
@@ -49,10 +49,16 @@ const validateOnClick = (value, themed) => {
themed={themed}
/>
);
tree.find(".components-label").simulate("click");
tree
.find(".components-label")
.simulate("click", { altKey: isNegative ? true : false });
expect(alertStore.filters.values).toHaveLength(1);
expect(alertStore.filters.values).toContainEqual(
NewUnappliedFilter(`@state=${value}`)
NewUnappliedFilter(
`@state${
isNegative ? QueryOperators.NotEqual : QueryOperators.Equal
}${value}`
)
);
};
@@ -93,13 +99,12 @@ describe("<FilteringCounterBadge />", () => {
expect(tree.text()).toBe("123");
});
it("onClick method on @state=unprocessed counter badge should add a new filter", () => {
validateOnClick("unprocessed", true);
});
it("onClick method on @state=active counter badge should add a new filter", () => {
validateOnClick("active", true);
});
it("onClick method on @state=suppressed counter badge should add a new filter", () => {
validateOnClick("suppressed", true);
});
for (let state of ["unprocessed", "active", "suppressed"]) {
it(`click on @state=${state} counter badge should add a new filter`, () => {
validateOnClick(state, true, false);
});
it(`alt+click method on @state=${state} counter badge should add a new negative filter`, () => {
validateOnClick(state, true, true);
});
}
});

View File

@@ -1,36 +1,41 @@
import React from "react";
import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useObserver } from "mobx-react";
import { QueryOperators, FormatQuery } from "Common/Query";
import { TooltipWrapper } from "Components/TooltipWrapper";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
// Renders a label element that after clicking adds current label as a filter
const FilteringLabel = observer(
class FilteringLabel extends BaseLabel {
render() {
const { name, value } = this.props;
const FilteringLabel = ({ alertStore, name, value }) => {
const handleClick = useCallback(
(event) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true ? QueryOperators.NotEqual : QueryOperators.Equal;
let cs = this.getClassAndStyle(
name,
value,
"components-label-with-hover"
);
event.preventDefault();
return (
<TooltipWrapper title="Click to only show alerts with this label or Alt+Click to hide them">
<span
className={cs.className}
style={cs.style}
onClick={(e) => this.handleClick(e)}
>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
</TooltipWrapper>
);
}
}
);
alertStore.filters.addFilter(FormatQuery(name, operator, value));
},
[alertStore.filters, name, value]
);
const cs = GetClassAndStyle(
alertStore,
name,
value,
"components-label-with-hover"
);
return useObserver(() => (
<TooltipWrapper title="Click to only show alerts with this label or Alt+Click to hide them">
<span className={cs.className} style={cs.style} onClick={handleClick}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
</TooltipWrapper>
));
};
export { FilteringLabel };

View File

@@ -1,38 +1,32 @@
import React from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
import { useObserver } from "mobx-react";
import { QueryOperators } from "Common/Query";
import { AlertStore } from "Stores/AlertStore";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
const HistoryLabel = observer(
class HistoryLabel extends BaseLabel {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
matcher: PropTypes.string.isRequired,
};
const HistoryLabel = ({ alertStore, name, matcher, value }) => {
const cs = GetClassAndStyle(
alertStore,
matcher === QueryOperators.Equal ? name : "",
matcher === QueryOperators.Equal ? value : "",
"components-label-history components-label-value"
);
render() {
const { name, matcher, value } = this.props;
let cs = this.getClassAndStyle(
matcher === QueryOperators.Equal ? name : "",
matcher === QueryOperators.Equal ? value : "",
"components-label-history components-label-value"
);
return (
<span className={cs.className} style={cs.style}>
{name ? `${name}${matcher}` : null}
{value}
</span>
);
}
}
);
return useObserver(() => (
<span className={cs.className} style={cs.style}>
{name ? `${name}${matcher}` : null}
{value}
</span>
));
};
HistoryLabel.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
matcher: PropTypes.string.isRequired,
};
export { HistoryLabel };

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import { observer } from "mobx-react";
@@ -8,82 +8,93 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
import { AlertStore } from "Stores/AlertStore";
import { QueryOperators, FormatQuery } from "Common/Query";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
const LabelWithPercent = observer(
class LabelWithPercent extends BaseLabel {
static propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hits: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
offset: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
};
({ alertStore, name, value, hits, percent, offset, isActive }) => {
const handleClick = useCallback(
(event) => {
// left click => apply foo=bar filter
// left click + alt => apply foo!=bar filter
const operator =
event.altKey === true
? QueryOperators.NotEqual
: QueryOperators.Equal;
removeFromFilters = () => {
const { alertStore, name, value } = this.props;
event.preventDefault();
alertStore.filters.addFilter(FormatQuery(name, operator, value));
},
[alertStore.filters, name, value]
);
const removeFromFilters = () => {
alertStore.filters.removeFilter(
FormatQuery(name, QueryOperators.Equal, value)
);
};
render() {
const { name, value, hits, percent, offset, isActive } = this.props;
const cs = GetClassAndStyle(
alertStore,
name,
value,
"components-label-with-hover mb-0 pl-0 text-left"
);
let cs = this.getClassAndStyle(
name,
value,
"components-label-with-hover mb-0 pl-0 text-left"
);
const progressBarBg =
percent > 66 ? "bg-danger" : percent > 33 ? "bg-warning" : "bg-success";
const progressBarBg =
percent > 66 ? "bg-danger" : percent > 33 ? "bg-warning" : "bg-success";
return (
<div className="d-inline-block mw-100">
<span className={cs.className} style={cs.style}>
<span className="mr-1 px-1 bg-primary text-white components-labelWithPercent-percent">
{hits}
</span>
<span onClick={(e) => this.handleClick(e)}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
{isActive ? (
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
style={{ fontSize: "100%" }}
icon={faTimes}
onClick={this.removeFromFilters}
/>
) : null}
return (
<div className="d-inline-block mw-100">
<span className={cs.className} style={cs.style}>
<span className="mr-1 px-1 bg-primary text-white components-labelWithPercent-percent">
{hits}
</span>
<div className="progress components-labelWithPercent-progress mr-1">
{offset === 0 ? null : (
<div
className="progress-bar bg-transparent"
role="progressbar"
style={{ width: offset + "%" }}
aria-valuenow={offset}
aria-valuemin="0"
aria-valuemax="100"
/>
)}
<span onClick={handleClick}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
{isActive ? (
<FontAwesomeIcon
className="cursor-pointer text-reset ml-1 close"
style={{ fontSize: "100%" }}
icon={faTimes}
onClick={removeFromFilters}
/>
) : null}
</span>
<div className="progress components-labelWithPercent-progress mr-1">
{offset === 0 ? null : (
<div
className={`progress-bar ${progressBarBg}`}
className="progress-bar bg-transparent"
role="progressbar"
style={{ width: percent + "%" }}
aria-valuenow={percent}
style={{ width: offset + "%" }}
aria-valuenow={offset}
aria-valuemin="0"
aria-valuemax="100"
/>
</div>
)}
<div
className={`progress-bar ${progressBarBg}`}
role="progressbar"
style={{ width: percent + "%" }}
aria-valuenow={percent}
aria-valuemin="0"
aria-valuemax="100"
/>
</div>
);
}
</div>
);
}
);
LabelWithPercent.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hits: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
offset: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
};
export { LabelWithPercent };

View File

@@ -1,25 +1,19 @@
import React from "react";
import { observer } from "mobx-react";
import { useObserver } from "mobx-react";
import { BaseLabel } from "Components/Labels/BaseLabel";
import { GetClassAndStyle } from "Components/Labels/Utils";
// Renders a static label element, no click actions, no hover
const StaticLabel = observer(
class StaticLabel extends BaseLabel {
render() {
const { name, value } = this.props;
const StaticLabel = ({ alertStore, name, value }) => {
const cs = GetClassAndStyle(alertStore, name, value);
let cs = this.getClassAndStyle(name, value);
return (
<span className={cs.className} style={cs.style}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
);
}
}
);
return useObserver(() => (
<span className={cs.className} style={cs.style}>
<span className="components-label-name">{name}:</span>{" "}
<span className="components-label-value">{value}</span>
</span>
));
};
export { StaticLabel };

View File

@@ -0,0 +1,63 @@
import {
StaticColorLabelClassMap,
DefaultLabelClassMap,
AlertNameLabelClassMap,
StateLabelClassMap,
} from "Common/Colors";
import { StaticLabels } from "Common/Query";
const isBackgroundDark = (brightness) => brightness <= 125;
const GetClassAndStyle = (alertStore, name, value, extraClass, baseClass) => {
const elementType = baseClass || "badge";
const data = {
style: {},
className: "",
baseClassNames: ["components-label", elementType],
colorClassNames: [],
};
if (name === StaticLabels.AlertName) {
data.colorClassNames.push(AlertNameLabelClassMap[elementType]);
} else if (name === StaticLabels.State) {
data.colorClassNames.push(
StateLabelClassMap[value]
? `${elementType}-${StateLabelClassMap[value]}`
: DefaultLabelClassMap[elementType]
);
} else if (alertStore.settings.values.staticColorLabels.includes(name)) {
data.colorClassNames.push(StaticColorLabelClassMap[elementType]);
} else {
const c = alertStore.data.getColorData(name, value);
if (c) {
// if there's color information use it
data.style["backgroundColor"] = `rgba(${[
c.background.red,
c.background.green,
c.background.blue,
c.background.alpha,
].join(", ")})`;
data.colorClassNames.push(
isBackgroundDark(c.brightness)
? "components-label-dark"
: "components-label-bright"
);
data.colorClassNames.push(
`components-label-brightness-${Math.round(c.brightness / 25)}`
);
} else {
// if not fall back to class
data.colorClassNames.push(DefaultLabelClassMap[elementType]);
}
}
data.className = `${[...data.baseClassNames, ...data.colorClassNames].join(
" "
)} ${extraClass || ""}`;
return data;
};
export { GetClassAndStyle };

View File

@@ -0,0 +1,76 @@
import { AlertStore } from "Stores/AlertStore";
import {
StaticColorLabelClassMap,
DefaultLabelClassMap,
AlertNameLabelClassMap,
StateLabelClassMap,
} from "Common/Colors";
import { GetClassAndStyle } from "./Utils";
let alertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
});
describe("<GetClassAndStyle />", () => {
it("static label uses StaticColorLabelClassMap.badge", () => {
alertStore.settings.values.staticColorLabels = ["foo", "job", "bar"];
const cs = GetClassAndStyle(alertStore, "foo", "bar");
expect(cs.colorClassNames).toContain(StaticColorLabelClassMap.badge);
});
Object.entries(StaticColorLabelClassMap).map(([key, val]) =>
it(`non-static label doesn't use StaticColorLabelClassMap.${key}`, () => {
alertStore.settings.values.staticColorLabels = [];
const cs = GetClassAndStyle(alertStore, "foo", "bar");
expect(cs.colorClassNames).not.toContain(StaticColorLabelClassMap.badge);
})
);
it("label with no special color information should use DefaultLabelClassMap.badge", () => {
const cs = GetClassAndStyle(alertStore, "foo", "bar");
expect(cs.colorClassNames).toContain(DefaultLabelClassMap.badge);
});
it("alertname label should use AlertNameLabelClassMap.badge", () => {
const cs = GetClassAndStyle(alertStore, "alertname", "foo");
expect(cs.colorClassNames).toContain(AlertNameLabelClassMap.badge);
});
it("@state=active label should use StateLabelClassMap.active class", () => {
const cs = GetClassAndStyle(alertStore, "@state", "active");
expect(cs.colorClassNames).toContain(`badge-${StateLabelClassMap.active}`);
});
it("@state=suppressed label should use StateLabelClassMap.suppressed class", () => {
const cs = GetClassAndStyle(alertStore, "@state", "suppressed");
expect(cs.colorClassNames).toContain(
`badge-${StateLabelClassMap.suppressed}`
);
});
it("@state=unprocessed label should use StateLabelClassMap.unprocessed class", () => {
const cs = GetClassAndStyle(alertStore, "@state", "unprocessed");
expect(cs.colorClassNames).toContain(
`badge-${StateLabelClassMap.unprocessed}`
);
});
it("@state with unknown label should use DefaultLabelClassMap.badge", () => {
const cs = GetClassAndStyle(alertStore, "@state", "foobar");
expect(cs.colorClassNames).toContain(DefaultLabelClassMap.badge);
});
it("style prop on a label included in staticColorLabels should be empty", () => {
alertStore.settings.values.staticColorLabels = ["foo", "job", "bar"];
const cs = GetClassAndStyle(alertStore, "foo", "bar");
expect(cs.style).toEqual({});
});
it("style prop on a label without any color information should be empty", () => {
alertStore.settings.values.staticColorLabels = [];
const cs = GetClassAndStyle(alertStore, "foo", "bar");
expect(cs.style).toEqual({});
});
});