Rule panel (#6708)

* make rules content to consume groups directly

Signed-off-by: blalov <boiskila@gmail.com>

* create  rule panel component

Signed-off-by: blalov <boiskila@gmail.com>

* fix react warning

Signed-off-by: blalov <boiskila@gmail.com>

* pr review changes

Signed-off-by: blalov <boiskila@gmail.com>

* lint fixes

Signed-off-by: blalov <boiskila@gmail.com>

* addreses PR comments

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* remove unnecessary double quote escaping

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* fix regex

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* apply suggested change

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* display duration when is > 0

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* split rule type to more specific types

Signed-off-by: blalov <boiskila@gmail.com>

* fix typo and remove unused utils function

Signed-off-by: blalov <boiskila@gmail.com>

* type guard alerting rule

Signed-off-by: blalov <boiskila@gmail.com>

* fix ts error cause by BaseRule type

Signed-off-by: blalov <boiskila@gmail.com>

* handle record expression properly

Signed-off-by: blalov <boiskila@gmail.com>

* remove quotes escaping logic for recoreding rule

Signed-off-by: blalov <boiskila@gmail.com>
This commit is contained in:
Boyko 2020-03-15 23:10:45 +02:00 committed by GitHub
parent eed32ef3e1
commit d2318dc822
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 155 additions and 171 deletions

View file

@ -3,7 +3,8 @@ import { Badge } from 'reactstrap';
import CollapsibleAlertPanel from './CollapsibleAlertPanel';
import Checkbox from '../../components/Checkbox';
import { isPresent } from '../../utils';
import { Rule } from '../../types/types';
import { AlertingRule } from '../../types/types';
import { RuleGroup } from '../rules/RulesContent';
export type RuleState = keyof RuleStatus<any>;
@ -14,7 +15,7 @@ export interface RuleStatus<T> {
}
export interface AlertsProps {
groups?: RuleGroup[];
groups?: RuleGroup<AlertingRule>[];
statsCount: RuleStatus<number>;
}
@ -26,13 +27,6 @@ export interface Alert {
activeAt: string;
}
interface RuleGroup {
name: string;
file: string;
rules: Rule[];
interval: number;
}
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
['inactive', 'success'],
['pending', 'warning'],
@ -101,11 +95,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
);
};
interface GroupInfoProps {
rules: Rule[];
}
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
export const GroupInfo: FC<{ rules: AlertingRule[] }> = ({ rules, children }) => {
const statesCounter = rules.reduce<any>(
(acc, r) => {
return {

View file

@ -1,14 +1,13 @@
import React, { FC, useState, Fragment } from 'react';
import { Link } from '@reach/router';
import { Alert, Collapse, Table, Badge } from 'reactstrap';
import { RuleStatus } from './AlertContents';
import { Rule } from '../../types/types';
import { AlertingRule } from '../../types/types';
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { createExpressionLink } from '../../utils/index';
import { RulePanel } from '../rules/RulePanel';
interface CollapsibleAlertPanelProps {
rule: Rule;
rule: AlertingRule;
showAnnotations: boolean;
}
@ -25,27 +24,10 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
<>
<Alert fade={false} onClick={() => toggle(!open)} color={alertColors[rule.state]} style={{ cursor: 'pointer' }}>
<FontAwesomeIcon icon={open ? faChevronDown : faChevronRight} fixedWidth />
<strong>{rule.name}</strong> ({`${rule.alerts.length} active`})
<strong>{rule.name}</strong> ({rule.alerts.length} active)
</Alert>
<Collapse isOpen={open} className="mb-2">
<pre style={{ background: '#f5f5f5', padding: 15 }}>
<code>
<div>
name: <Link to={createExpressionLink(`ALERTS{alertname="${rule.name}"}`)}>{rule.name}</Link>
</div>
<div>
expr: <Link to={createExpressionLink(rule.query)}>{rule.query}</Link>
</div>
<div>
<div>labels:</div>
<div className="ml-5">severity: {rule.labels.severity}</div>
</div>
<div>
<div>annotations:</div>
<div className="ml-5">summary: {rule.annotations.summary}</div>
</div>
</code>
</pre>
<RulePanel rule={rule} styles={{ padding: 15, border: '1px solid #ccc', borderRadius: 4 }} />
{rule.alerts.length > 0 && (
<Table bordered size="sm">
<thead>
@ -71,11 +53,9 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
})}
</td>
<td>
<h5 className="m-0">
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3">
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3 py-2">
{alert.state}
</Badge>
</h5>
</td>
<td>{alert.activeAt}</td>
<td>{alert.value}</td>

View file

@ -0,0 +1,54 @@
import React, { FC, CSSProperties, Fragment } from 'react';
import { Rule } from '../../types/types';
import { Link } from '@reach/router';
import { createExpressionLink, mapObjEntries, formatRange } from '../../utils';
interface RulePanelProps {
rule: Rule;
styles?: CSSProperties;
tag?: keyof JSX.IntrinsicElements;
}
export const RulePanel: FC<RulePanelProps> = ({ rule, styles = {}, tag }) => {
const Tag = tag || Fragment;
const { name, query, labels } = rule;
const style = { background: '#f5f5f5', ...styles };
return (
<Tag {...(tag ? { style } : {})}>
<pre className="m-0" style={tag ? undefined : style}>
<code>
{rule.type === 'alerting' ? 'alert: ' : 'record: '}
<Link
to={createExpressionLink(rule.type === 'alerting' ? `ALERTS{alertname="${name.replace(/"/g, '\\"')}"}` : name)}
>
{name}
</Link>
<div>
expr: <Link to={createExpressionLink(query)}>{query}</Link>
</div>
{rule.type === 'alerting' && rule.duration > 0 && <div>for: {formatRange(rule.duration)}</div>}
{labels && (
<>
labels:
{mapObjEntries(labels, ([key, value]) => (
<div className="ml-4" key={key}>
{key}: {value}
</div>
))}
</>
)}
{rule.type === 'alerting' && rule.annotations && (
<>
annotations:
{mapObjEntries(rule.annotations, ([key, value]) => (
<div className="ml-4" key={key}>
{key}: {value}
</div>
))}
</>
)}
</code>
</pre>
</Tag>
);
};

View file

@ -3,14 +3,14 @@ import { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps';
import { useFetch } from '../../hooks/useFetch';
import { withStatusIndicator } from '../../components/withStatusIndicator';
import { RulesMap, RulesContent } from './RulesContent';
import { RulesGroups, RulesContent } from './RulesContent';
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`);
const { response, error, isLoading } = useFetch<RulesGroups>(`${pathPrefix}/api/v1/rules`);
return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
return <RulesWithStatusIndicator {...response.data} error={error} isLoading={isLoading} />;
};
export default Rules;

View file

@ -1,72 +1,51 @@
import React, { FC } from 'react';
import React, { FC, Fragment } from 'react';
import { RouteComponentProps } from '@reach/router';
import { APIResponse } from '../../hooks/useFetch';
import { Alert, Table, Badge } from 'reactstrap';
import { Link } from '@reach/router';
import { formatRelative, createExpressionLink, humanizeDuration } from '../../utils';
import { Rule } from '../../types/types';
import { formatRelative, humanizeDuration, isPresent } from '../../utils';
import { now } from 'moment';
import { RulePanel } from './RulePanel';
import { Rule } from '../../types/types';
interface RulesContentProps {
response: APIResponse<RulesMap>;
}
interface RuleGroup {
export interface RuleGroup<T> {
name: string;
file: string;
rules: Rule[];
rules: T[];
interval: number;
evaluationTime: string;
lastEvaluation: string;
}
export interface RulesMap {
groups: RuleGroup[];
export interface RulesGroups {
groups: RuleGroup<Rule>[];
}
const GraphExpressionLink: FC<{ expr: string; title: string }> = props => {
return (
<>
<strong>{props.title}:</strong> <Link to={createExpressionLink(props.expr)}>{props.expr}</Link>
<br />
</>
);
export const badgeColorMap: Record<'ok' | 'err' | 'unknown', 'success' | 'danger' | 'warning'> = {
ok: 'success',
err: 'danger',
unknown: 'warning',
};
export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ response }) => {
const getBadgeColor = (state: string) => {
switch (state) {
case 'ok':
return 'success';
case 'err':
return 'danger';
case 'unknown':
return 'warning';
}
};
if (response.data) {
const groups: RuleGroup[] = response.data.groups;
export const RulesContent: FC<RouteComponentProps & RulesGroups> = ({ groups }) => {
return (
<>
<h2>Rules</h2>
<Table bordered>
{groups.map((g, i) => {
{groups.map(group => {
const { name: groupName, lastEvaluation, evaluationTime } = group;
return (
<React.Fragment key={i}>
<Fragment key={groupName}>
<thead>
<tr>
<td colSpan={3}>
<a href={'#' + g.name}>
<h2 id={g.name}>{g.name}</h2>
<a href={`#${groupName}`}>
<h2 id={groupName}>{groupName}</h2>
</a>
</td>
<td>
<h2>{formatRelative(g.lastEvaluation, now())} ago</h2>
<h2>{formatRelative(lastEvaluation, now())} ago</h2>
</td>
<td>
<h2>{humanizeDuration(parseFloat(g.evaluationTime) * 1000)}</h2>
<h2>{humanizeDuration(parseFloat(evaluationTime) * 1000)}</h2>
</td>
</tr>
</thead>
@ -78,53 +57,26 @@ export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ resp
<td>Last Evaluation</td>
<td>Evaluation Time</td>
</tr>
{g.rules.map((r, i) => {
{group.rules.map((rule, i) => {
return (
<tr key={i}>
{r.alerts ? (
<td className="rule-cell">
<GraphExpressionLink title="alert" expr={r.name} />
<GraphExpressionLink title="expr" expr={r.query} />
<div>
<strong>labels:</strong>
{Object.entries(r.labels).map(([key, value]) => (
<div className="ml-4" key={key}>
{key}: {value}
</div>
))}
</div>
<div>
<strong>annotations:</strong>
{Object.entries(r.annotations).map(([key, value]) => (
<div className="ml-4" key={key}>
{key}: {value}
</div>
))}
</div>
<RulePanel tag="td" rule={rule} />
<td style={{ textAlign: 'center', verticalAlign: 'middle' }}>
<Badge className="p-2 px-4 text-uppercase" color={badgeColorMap[rule.health]}>
{rule.health}
</Badge>
</td>
) : (
<td className="rule-cell">
<GraphExpressionLink title="record" expr={r.name} />
<GraphExpressionLink title="expr" expr={r.query} />
</td>
)}
<td>
<Badge color={getBadgeColor(r.health)}>{r.health.toUpperCase()}</Badge>
</td>
<td>{r.lastError ? <Alert color="danger">{r.lastError}</Alert> : null}</td>
<td>{formatRelative(r.lastEvaluation, now())} ago</td>
<td>{humanizeDuration(parseFloat(r.evaluationTime) * 1000)}</td>
<td>{isPresent(rule.lastError) && <Alert color="danger">{rule.lastError}</Alert>}</td>
<td>{formatRelative(rule.lastEvaluation, now())} ago</td>
<td>{humanizeDuration(parseFloat(rule.evaluationTime) * 1000)}</td>
</tr>
);
})}
</tbody>
</React.Fragment>
</Fragment>
);
})}
</Table>
</>
);
}
return null;
};

View file

@ -10,17 +10,26 @@ export interface QueryParams {
resolution: number;
}
export interface Rule {
alerts: Alert[];
annotations: Record<string, string>;
duration: number;
evaluationTime: string;
health: string;
labels: Record<string, string>;
lastError?: string;
lastEvaluation: string;
export interface BaseRule {
name: string;
query: string;
state: RuleState;
type: string;
labels: Record<string, string>;
health: 'ok' | 'err' | 'unknown';
lastError?: string;
evaluationTime: string;
lastEvaluation: string;
}
export interface AlertingRule extends BaseRule {
alerts: Alert[];
duration: number;
state: RuleState;
annotations: Record<string, string>;
type: 'alerting';
}
export interface RecordingRule extends BaseRule {
type: 'recording';
}
export type Rule = RecordingRule | AlertingRule;

View file

@ -207,6 +207,5 @@ export const mapObjEntries = <T, key extends keyof T, Z>(
) => Object.entries(o).map(cb);
export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
// eslint-disable-next-line prefer-spread
fns.filter(Boolean).forEach(fn => fn.apply(null, args));
fns.filter(Boolean).forEach(fn => fn(...args));
};