Adds support for rules screen in react-ui (#6503)

* base

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* base of rules page

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* initial version

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* removed unused function

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* version 1

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* implemented suggestions

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* implemented suggestions

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* implemented suggestions.

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* new fetching pattern

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>

* implemented suggestions

Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>
This commit is contained in:
Harkishen Singh 2020-01-27 14:57:43 +05:30 committed by Julius Volz
parent d996ba20ec
commit c1e49d50c5
8 changed files with 226 additions and 63 deletions

View file

@ -899,6 +899,8 @@ type RuleGroup struct {
// same array. // same array.
Rules []rule `json:"rules"` Rules []rule `json:"rules"`
Interval float64 `json:"interval"` Interval float64 `json:"interval"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
} }
type rule interface{} type rule interface{}
@ -914,6 +916,8 @@ type alertingRule struct {
Alerts []*Alert `json:"alerts"` Alerts []*Alert `json:"alerts"`
Health rules.RuleHealth `json:"health"` Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"` LastError string `json:"lastError,omitempty"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
// Type of an alertingRule is always "alerting". // Type of an alertingRule is always "alerting".
Type string `json:"type"` Type string `json:"type"`
} }
@ -924,6 +928,8 @@ type recordingRule struct {
Labels labels.Labels `json:"labels,omitempty"` Labels labels.Labels `json:"labels,omitempty"`
Health rules.RuleHealth `json:"health"` Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"` LastError string `json:"lastError,omitempty"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
// Type of a recordingRule is always "recording". // Type of a recordingRule is always "recording".
Type string `json:"type"` Type string `json:"type"`
} }
@ -947,6 +953,8 @@ func (api *API) rules(r *http.Request) apiFuncResult {
File: grp.File(), File: grp.File(),
Interval: grp.Interval().Seconds(), Interval: grp.Interval().Seconds(),
Rules: []rule{}, Rules: []rule{},
EvaluationTime: grp.GetEvaluationDuration().Seconds(),
LastEvaluation: grp.GetEvaluationTimestamp(),
} }
for _, r := range grp.Rules() { for _, r := range grp.Rules() {
var enrichedRule rule var enrichedRule rule
@ -970,6 +978,8 @@ func (api *API) rules(r *http.Request) apiFuncResult {
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()), Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
Health: rule.Health(), Health: rule.Health(),
LastError: lastError, LastError: lastError,
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
LastEvaluation: rule.GetEvaluationTimestamp(),
Type: "alerting", Type: "alerting",
} }
case *rules.RecordingRule: case *rules.RecordingRule:
@ -982,6 +992,8 @@ func (api *API) rules(r *http.Request) apiFuncResult {
Labels: rule.Labels(), Labels: rule.Labels(),
Health: rule.Health(), Health: rule.Health(),
LastError: lastError, LastError: lastError,
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
LastEvaluation: rule.GetEvaluationTimestamp(),
Type: "recording", Type: "recording",
} }
default: default:

View file

@ -237,3 +237,14 @@ button.execute-btn {
margin-right: 5px; margin-right: 5px;
max-height: 20px; max-height: 20px;
} }
.rules-head {
font-weight: 600;
}
.rule_cell {
white-space: pre-wrap;
background-color: #F5F5F5;
display: block;
font-family: monospace;
}

View file

@ -3,21 +3,10 @@ import { Badge } 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';
export type RuleState = keyof RuleStatus<any>; export type RuleState = keyof RuleStatus<any>;
export interface Rule {
alerts: Alert[];
annotations: Record<string, string>;
duration: number;
health: string;
labels: Record<string, string>;
name: string;
query: string;
state: RuleState;
type: string;
}
export interface RuleStatus<T> { export interface RuleStatus<T> {
firing: T; firing: T;
pending: T; pending: T;
@ -29,7 +18,7 @@ export interface AlertsProps {
statsCount: RuleStatus<number>; statsCount: RuleStatus<number>;
} }
interface Alert { export interface Alert {
labels: Record<string, string>; labels: Record<string, string>;
state: RuleState; state: RuleState;
value: string; value: string;

View file

@ -1,9 +1,11 @@
import React, { FC, useState, Fragment } from 'react'; import React, { FC, useState, Fragment } from 'react';
import { Link } from '@reach/router'; import { Link } from '@reach/router';
import { Alert, Collapse, Table, Badge } from 'reactstrap'; import { Alert, Collapse, Table, Badge } from 'reactstrap';
import { Rule, RuleStatus } from './AlertContents'; import { RuleStatus } from './AlertContents';
import { Rule } from '../../types/types';
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { createExpressionLink } from '../../utils/index';
interface CollapsibleAlertPanelProps { interface CollapsibleAlertPanelProps {
rule: Rule; rule: Rule;
@ -16,10 +18,6 @@ const alertColors: RuleStatus<string> = {
inactive: 'success', inactive: 'success',
}; };
const createExpressionLink = (expr: string) => {
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`;
};
const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnotations }) => { const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnotations }) => {
const [open, toggle] = useState(false); const [open, toggle] = useState(false);

View file

@ -1,15 +1,16 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router'; import { RouteComponentProps } from '@reach/router';
import PathPrefixProps from '../../types/PathPrefixProps'; import PathPrefixProps from '../../types/PathPrefixProps';
import { Alert } from 'reactstrap'; import { useFetch } from '../../hooks/useFetch';
import { withStatusIndicator } from '../../components/withStatusIndicator';
import { RulesMap, RulesContent } from './RulesContent';
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => ( const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
<>
<h2>Rules</h2> const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
<Alert color="warning"> const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`);
This page is still under construction. Please try it in the <a href={`${pathPrefix}/rules`}>Classic UI</a>.
</Alert> return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
</> };
);
export default Rules; export default Rules;

View file

@ -0,0 +1,131 @@
import React, { FC } 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 { now } from 'moment';
interface RulesContentProps {
response: APIResponse<RulesMap>;
}
interface RuleGroup {
name: string;
file: string;
rules: Rule[];
evaluationTime: string;
lastEvaluation: string;
}
export interface RulesMap {
groups: RuleGroup[];
}
const GraphExpressionLink: FC<{ expr: string; title: string }> = props => {
return (
<>
<strong>{props.title}:</strong>
<Link className="ml-4" to={createExpressionLink(props.expr)}>
{props.expr}
</Link>
<br />
</>
);
};
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;
return (
<>
<h2>Rules</h2>
{groups.map((g, i) => {
return (
<Table bordered key={i}>
<thead>
<tr>
<td colSpan={3}>
<a href={'#' + g.name}>
<h2 id={g.name}>{g.name}</h2>
</a>
</td>
<td>
<h2>{formatRelative(g.lastEvaluation, now())} ago</h2>
</td>
<td>
<h2>{humanizeDuration(parseFloat(g.evaluationTime) * 1000)}</h2>
</td>
</tr>
</thead>
<tbody>
<tr className="font-weight-bold">
<td>Rule</td>
<td>State</td>
<td>Error</td>
<td>Last Evaluation</td>
<td>Evaluation Time</td>
</tr>
{g.rules.map((r, i) => {
return (
<tr key={i}>
{r.alerts ? (
<td style={{ backgroundColor: '#F5F5F5' }} 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>
</td>
) : (
<td style={{ backgroundColor: '#F5F5F5' }}>
<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>
</tr>
);
})}
</tbody>
</Table>
);
})}
</>
);
}
return null;
};

View file

@ -1,3 +1,5 @@
import { Alert, RuleState } from '../pages/alerts/AlertContents';
export interface Metric { export interface Metric {
[key: string]: string; [key: string]: string;
} }
@ -7,3 +9,18 @@ export interface QueryParams {
endTime: number; endTime: number;
resolution: number; 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;
name: string;
query: string;
state: RuleState;
type: string;
}

View file

@ -197,3 +197,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => { export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
return `?${panels.map(toQueryString).join('&')}`; return `?${panels.map(toQueryString).join('&')}`;
}; };
export const createExpressionLink = (expr: string) => {
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`;
};