Rework Alert page (#10142)

* rework the target page

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* put back the URL of the endpoint

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* replace old code by the new one and change function style

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* align filter and search bar on the same row

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* remove unnecessary return

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* upgrade kvsearch to v0.3.0

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix unit test

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* add missing style on column

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* add placeholder and autofocus

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* put back the previous table design

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix issue relative to the position of the tooltip

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix health filter

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix test on label tooltip

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* simplify filter condition

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* rework service discovery page

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* introduced generic custom infinite scroll component

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* adjust the placeholder in discovery page

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* ignore returning type missing

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* rework alert page

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* update snapshot to match the new rendering

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* fix infinite scroll component usage in alert

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* align checkbox like it was before

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* propose a more responsive line to display the buttons and the search bar

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* add a script to update the snapshot and update it

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>

* text in span won't be wrapped

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
Augustin Husson 2022-01-22 10:39:15 +01:00 committed by GitHub
parent a4d3206618
commit 027b082793
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 228 additions and 132 deletions

View file

@ -51,7 +51,8 @@
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache", "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"", "lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
"lint": "eslint --fix \"src/**/*.{ts,tsx}\"" "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
"snapshot": "react-scripts test --updateSnapshot"
}, },
"prettier": { "prettier": {
"singleQuote": true, "singleQuote": true,

View file

@ -1,10 +1,13 @@
import React, { FC, Fragment } from 'react'; import React, { ChangeEvent, FC, Fragment, useEffect, useState } from 'react';
import { Badge } from 'reactstrap'; import { Badge, Col, Row } from 'reactstrap';
import CollapsibleAlertPanel from './CollapsibleAlertPanel'; import CollapsibleAlertPanel from './CollapsibleAlertPanel';
import Checkbox from '../../components/Checkbox'; import Checkbox from '../../components/Checkbox';
import { isPresent } from '../../utils'; import { isPresent } from '../../utils';
import { Rule } from '../../types/types'; import { Rule } from '../../types/types';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../hooks/useLocalStorage';
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
import { KVSearch } from '@nexucis/kvsearch';
import SearchBar from '../../components/SearchBar';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RuleState = keyof RuleStatus<any>; export type RuleState = keyof RuleStatus<any>;
@ -35,13 +38,33 @@ interface RuleGroup {
interval: number; interval: number;
} }
const kvSearchRule = new KVSearch({
shouldSort: true,
indexedKeys: ['name', 'labels', ['labels', /.*/]],
});
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [ const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
['inactive', 'success'], ['inactive', 'success'],
['pending', 'warning'], ['pending', 'warning'],
['firing', 'danger'], ['firing', 'danger'],
]; ];
function GroupContent(showAnnotations: boolean) {
const Content: FC<InfiniteScrollItemsProps<Rule>> = ({ items }) => {
return (
<>
{items.map((rule, j) => (
<CollapsibleAlertPanel key={rule.name + j} showAnnotations={showAnnotations} rule={rule} />
))}
</>
);
};
return Content;
}
const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => { const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
const [groupList, setGroupList] = useState(groups);
const [filteredList, setFilteredList] = useState(groups);
const [filter, setFilter] = useLocalStorage('alerts-status-filter', { const [filter, setFilter] = useLocalStorage('alerts-status-filter', {
firing: true, firing: true,
pending: true, pending: true,
@ -56,50 +79,80 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
}); });
}; };
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
if (e.target.value !== '') {
const pattern = e.target.value.trim();
const result: RuleGroup[] = [];
for (const group of groups) {
const ruleFilterList = kvSearchRule.filter(pattern, group.rules);
if (ruleFilterList.length > 0) {
result.push({
file: group.file,
name: group.name,
interval: group.interval,
rules: ruleFilterList.map((value) => value.original as unknown as Rule),
});
}
}
setGroupList(result);
} else {
setGroupList(groups);
}
};
useEffect(() => {
const result: RuleGroup[] = [];
for (const group of groupList) {
const newGroup = {
file: group.file,
name: group.name,
interval: group.interval,
rules: group.rules.filter((value) => filter[value.state]),
};
if (newGroup.rules.length > 0) {
result.push(newGroup);
}
}
setFilteredList(result);
}, [groupList, filter]);
return ( return (
<> <>
<div className="d-flex togglers-wrapper"> <Row className="align-items-center">
{stateColorTuples.map(([state, color]) => { <Col className="d-flex" lg="4" md="5">
return ( {stateColorTuples.map(([state, color]) => {
<Checkbox return (
key={state} <Checkbox key={state} checked={filter[state]} id={`${state}-toggler`} onChange={toggleFilter(state)}>
wrapperStyles={{ marginRight: 20 }} <Badge color={color} className="text-capitalize">
checked={filter[state]} {state} ({statsCount[state]})
id={`${state}-toggler`} </Badge>
onChange={toggleFilter(state)} </Checkbox>
> );
<Badge color={color} className="text-capitalize"> })}
{state} ({statsCount[state]}) </Col>
</Badge> <Col lg="5" md="4">
</Checkbox> <SearchBar handleChange={handleSearchChange} placeholder="Filter by name or labels" />
); </Col>
})} <Col className="d-flex flex-row-reverse" md="3">
<Checkbox <Checkbox
wrapperStyles={{ marginLeft: 'auto' }} checked={showAnnotations.checked}
checked={showAnnotations.checked} id="show-annotations-toggler"
id="show-annotations-toggler" onChange={({ target }) => setShowAnnotations({ checked: target.checked })}
onChange={({ target }) => setShowAnnotations({ checked: target.checked })} >
> <span style={{ fontSize: '0.9rem', lineHeight: 1.9, display: 'inline-block', whiteSpace: 'nowrap' }}>
<span style={{ fontSize: '0.9rem', lineHeight: 1.9 }}>Show annotations</span> Show annotations
</Checkbox> </span>
</div> </Checkbox>
{groups.map((group, i) => { </Col>
const hasFilterOn = group.rules.some((rule) => filter[rule.state]); </Row>
return hasFilterOn ? ( {filteredList.map((group, i) => (
<Fragment key={i}> <Fragment key={i}>
<GroupInfo rules={group.rules}> <GroupInfo rules={group.rules}>
{group.file} &gt; {group.name} {group.file} &gt; {group.name}
</GroupInfo> </GroupInfo>
{group.rules.map((rule, j) => { <CustomInfiniteScroll allItems={group.rules} child={GroupContent(showAnnotations.checked)} />
return ( </Fragment>
filter[rule.state] && ( ))}
<CollapsibleAlertPanel key={rule.name + j} showAnnotations={showAnnotations.checked} rule={rule} />
)
);
})}
</Fragment>
) : null;
})}
</> </>
); );
}; };

View file

@ -2,99 +2,141 @@
exports[`AlertsContent matches a snapshot 1`] = ` exports[`AlertsContent matches a snapshot 1`] = `
<Fragment> <Fragment>
<div <Row
className="d-flex togglers-wrapper" className="align-items-center"
tag="div"
widths={
Array [
"xs",
"sm",
"md",
"lg",
"xl",
]
}
> >
<Memo(Checkbox) <Col
checked={true} className="d-flex"
id="inactive-toggler" lg="4"
key="inactive" md="5"
onChange={[Function]} tag="div"
wrapperStyles={ widths={
Object { Array [
"marginRight": 20, "xs",
} "sm",
"md",
"lg",
"xl",
]
} }
> >
<Badge <Memo(Checkbox)
className="text-capitalize" checked={true}
color="success" id="inactive-toggler"
pill={false} key="inactive"
tag="span" onChange={[Function]}
> >
inactive <Badge
( className="text-capitalize"
0 color="success"
) pill={false}
</Badge> tag="span"
</Memo(Checkbox)> >
<Memo(Checkbox) inactive
checked={true} (
id="pending-toggler" 0
key="pending" )
onChange={[Function]} </Badge>
wrapperStyles={ </Memo(Checkbox)>
Object { <Memo(Checkbox)
"marginRight": 20, checked={true}
} id="pending-toggler"
} key="pending"
> onChange={[Function]}
<Badge
className="text-capitalize"
color="warning"
pill={false}
tag="span"
> >
pending <Badge
( className="text-capitalize"
0 color="warning"
) pill={false}
</Badge> tag="span"
</Memo(Checkbox)> >
<Memo(Checkbox) pending
checked={true} (
id="firing-toggler" 0
key="firing" )
onChange={[Function]} </Badge>
wrapperStyles={ </Memo(Checkbox)>
Object { <Memo(Checkbox)
"marginRight": 20, checked={true}
} id="firing-toggler"
} key="firing"
> onChange={[Function]}
<Badge
className="text-capitalize"
color="danger"
pill={false}
tag="span"
> >
firing <Badge
( className="text-capitalize"
0 color="danger"
) pill={false}
</Badge> tag="span"
</Memo(Checkbox)> >
<Memo(Checkbox) firing
checked={false} (
id="show-annotations-toggler" 0
onChange={[Function]} )
wrapperStyles={ </Badge>
Object { </Memo(Checkbox)>
"marginLeft": "auto", </Col>
} <Col
lg="5"
md="4"
tag="div"
widths={
Array [
"xs",
"sm",
"md",
"lg",
"xl",
]
} }
> >
<span <SearchBar
style={ handleChange={[Function]}
Object { placeholder="Filter by name or labels"
"fontSize": "0.9rem", />
"lineHeight": 1.9, </Col>
<Col
className="d-flex flex-row-reverse"
md="3"
tag="div"
widths={
Array [
"xs",
"sm",
"md",
"lg",
"xl",
]
}
>
<Memo(Checkbox)
checked={false}
id="show-annotations-toggler"
onChange={[Function]}
>
<span
style={
Object {
"display": "inline-block",
"fontSize": "0.9rem",
"lineHeight": 1.9,
"whiteSpace": "nowrap",
}
} }
} >
> Show annotations
Show annotations </span>
</span> </Memo(Checkbox)>
</Memo(Checkbox)> </Col>
</div> </Row>
</Fragment> </Fragment>
`; `;

View file

@ -16,7 +16,7 @@ export interface QueryParams {
resolution: number; resolution: number;
} }
export interface Rule { export type Rule = {
alerts: Alert[]; alerts: Alert[];
annotations: Record<string, string>; annotations: Record<string, string>;
duration: number; duration: number;
@ -29,7 +29,7 @@ export interface Rule {
query: string; query: string;
state: RuleState; state: RuleState;
type: string; type: string;
} };
export interface WALReplayData { export interface WALReplayData {
min: number; min: number;