mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
fix(ui): rewrite label components with hooks
This commit is contained in:
committed by
Łukasz Mierzwa
parent
55463e9ef5
commit
06d8eb6089
@@ -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 };
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
63
ui/src/Components/Labels/Utils.js
Normal file
63
ui/src/Components/Labels/Utils.js
Normal 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 };
|
||||
76
ui/src/Components/Labels/Utils.test.js
Normal file
76
ui/src/Components/Labels/Utils.test.js
Normal 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({});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user