2022-04-26 12:20:48 -07:00
|
|
|
import React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
2022-01-22 01:39:15 -08:00
|
|
|
import { Badge, Col, Row } from 'reactstrap';
|
2019-12-09 14:42:59 -08:00
|
|
|
import CollapsibleAlertPanel from './CollapsibleAlertPanel';
|
2020-01-14 10:34:48 -08:00
|
|
|
import Checkbox from '../../components/Checkbox';
|
2022-04-26 12:20:48 -07:00
|
|
|
import { getQuerySearchFilter, isPresent, setQuerySearchFilter } from '../../utils';
|
2020-01-27 01:27:43 -08:00
|
|
|
import { Rule } from '../../types/types';
|
2020-06-10 07:44:07 -07:00
|
|
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
2022-01-22 01:39:15 -08:00
|
|
|
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
|
|
|
|
import { KVSearch } from '@nexucis/kvsearch';
|
|
|
|
import SearchBar from '../../components/SearchBar';
|
2019-12-09 14:42:59 -08:00
|
|
|
|
2021-09-03 08:41:20 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2019-12-09 14:42:59 -08:00
|
|
|
export type RuleState = keyof RuleStatus<any>;
|
|
|
|
|
|
|
|
export interface RuleStatus<T> {
|
|
|
|
firing: T;
|
|
|
|
pending: T;
|
|
|
|
inactive: T;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface AlertsProps {
|
|
|
|
groups?: RuleGroup[];
|
|
|
|
statsCount: RuleStatus<number>;
|
|
|
|
}
|
|
|
|
|
2020-01-27 01:27:43 -08:00
|
|
|
export interface Alert {
|
2019-12-09 14:42:59 -08:00
|
|
|
labels: Record<string, string>;
|
|
|
|
state: RuleState;
|
|
|
|
value: string;
|
|
|
|
annotations: Record<string, string>;
|
|
|
|
activeAt: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface RuleGroup {
|
|
|
|
name: string;
|
|
|
|
file: string;
|
|
|
|
rules: Rule[];
|
|
|
|
interval: number;
|
|
|
|
}
|
|
|
|
|
2022-02-21 06:37:28 -08:00
|
|
|
const kvSearchRule = new KVSearch<Rule>({
|
2022-01-22 01:39:15 -08:00
|
|
|
shouldSort: true,
|
|
|
|
indexedKeys: ['name', 'labels', ['labels', /.*/]],
|
|
|
|
});
|
|
|
|
|
2020-01-07 07:12:38 -08:00
|
|
|
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
|
|
|
|
['inactive', 'success'],
|
|
|
|
['pending', 'warning'],
|
|
|
|
['firing', 'danger'],
|
|
|
|
];
|
|
|
|
|
2022-01-22 01:39:15 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-12-09 14:42:59 -08:00
|
|
|
const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
|
2022-01-22 01:39:15 -08:00
|
|
|
const [groupList, setGroupList] = useState(groups);
|
|
|
|
const [filteredList, setFilteredList] = useState(groups);
|
2020-06-10 07:44:07 -07:00
|
|
|
const [filter, setFilter] = useLocalStorage('alerts-status-filter', {
|
2019-12-09 14:42:59 -08:00
|
|
|
firing: true,
|
|
|
|
pending: true,
|
|
|
|
inactive: true,
|
|
|
|
});
|
2020-06-10 07:44:07 -07:00
|
|
|
const [showAnnotations, setShowAnnotations] = useLocalStorage('alerts-annotations-status', { checked: false });
|
2020-01-16 13:22:47 -08:00
|
|
|
const toggleFilter = (ruleState: RuleState) => () => {
|
|
|
|
setFilter({
|
|
|
|
...filter,
|
|
|
|
[ruleState]: !filter[ruleState],
|
2019-12-09 14:42:59 -08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-04-26 12:20:48 -07:00
|
|
|
const handleSearchChange = useCallback(
|
|
|
|
(value: string) => {
|
|
|
|
setQuerySearchFilter(value);
|
|
|
|
if (value !== '') {
|
|
|
|
const pattern = 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),
|
|
|
|
});
|
|
|
|
}
|
2022-01-22 01:39:15 -08:00
|
|
|
}
|
2022-04-26 12:20:48 -07:00
|
|
|
setGroupList(result);
|
|
|
|
} else {
|
|
|
|
setGroupList(groups);
|
2022-01-22 01:39:15 -08:00
|
|
|
}
|
2022-04-26 12:20:48 -07:00
|
|
|
},
|
|
|
|
[groups]
|
|
|
|
);
|
|
|
|
|
|
|
|
const defaultValue = useMemo(getQuerySearchFilter, []);
|
2022-01-22 01:39:15 -08:00
|
|
|
|
|
|
|
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]);
|
|
|
|
|
2019-12-09 14:42:59 -08:00
|
|
|
return (
|
|
|
|
<>
|
2022-01-22 01:39:15 -08:00
|
|
|
<Row className="align-items-center">
|
|
|
|
<Col className="d-flex" lg="4" md="5">
|
|
|
|
{stateColorTuples.map(([state, color]) => {
|
|
|
|
return (
|
|
|
|
<Checkbox key={state} checked={filter[state]} id={`${state}-toggler`} onChange={toggleFilter(state)}>
|
|
|
|
<Badge color={color} className="text-capitalize">
|
|
|
|
{state} ({statsCount[state]})
|
|
|
|
</Badge>
|
|
|
|
</Checkbox>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</Col>
|
|
|
|
<Col lg="5" md="4">
|
2022-04-26 12:20:48 -07:00
|
|
|
<SearchBar defaultValue={defaultValue} handleChange={handleSearchChange} placeholder="Filter by name or labels" />
|
2022-01-22 01:39:15 -08:00
|
|
|
</Col>
|
|
|
|
<Col className="d-flex flex-row-reverse" md="3">
|
|
|
|
<Checkbox
|
|
|
|
checked={showAnnotations.checked}
|
|
|
|
id="show-annotations-toggler"
|
|
|
|
onChange={({ target }) => setShowAnnotations({ checked: target.checked })}
|
|
|
|
>
|
|
|
|
<span style={{ fontSize: '0.9rem', lineHeight: 1.9, display: 'inline-block', whiteSpace: 'nowrap' }}>
|
|
|
|
Show annotations
|
|
|
|
</span>
|
|
|
|
</Checkbox>
|
|
|
|
</Col>
|
|
|
|
</Row>
|
|
|
|
{filteredList.map((group, i) => (
|
|
|
|
<Fragment key={i}>
|
|
|
|
<GroupInfo rules={group.rules}>
|
|
|
|
{group.file} > {group.name}
|
|
|
|
</GroupInfo>
|
|
|
|
<CustomInfiniteScroll allItems={group.rules} child={GroupContent(showAnnotations.checked)} />
|
|
|
|
</Fragment>
|
|
|
|
))}
|
2019-12-09 14:42:59 -08:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-01-16 13:22:47 -08:00
|
|
|
interface GroupInfoProps {
|
2019-12-09 14:42:59 -08:00
|
|
|
rules: Rule[];
|
|
|
|
}
|
|
|
|
|
2020-01-16 13:22:47 -08:00
|
|
|
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
|
2021-09-03 08:41:20 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2019-12-09 14:42:59 -08:00
|
|
|
const statesCounter = rules.reduce<any>(
|
|
|
|
(acc, r) => {
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[r.state]: acc[r.state] + r.alerts.length,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
{
|
|
|
|
firing: 0,
|
|
|
|
pending: 0,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
2020-01-16 13:22:47 -08:00
|
|
|
<div className="group-info border rounded-sm" style={{ lineHeight: 1.1 }}>
|
2019-12-09 14:42:59 -08:00
|
|
|
{children}
|
|
|
|
<div className="badges-wrapper">
|
|
|
|
{isPresent(statesCounter.inactive) && <Badge color="success">inactive</Badge>}
|
|
|
|
{statesCounter.pending > 0 && <Badge color="warning">pending ({statesCounter.pending})</Badge>}
|
|
|
|
{statesCounter.firing > 0 && <Badge color="danger">firing ({statesCounter.firing})</Badge>}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
AlertsContent.displayName = 'Alerts';
|
|
|
|
|
|
|
|
export default AlertsContent;
|