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

@ -897,33 +897,39 @@ type RuleGroup struct {
// In order to preserve rule ordering, while exposing type (alerting or recording)
// specific properties, both alerting and recording rules are exposed in the
// same array.
Rules []rule `json:"rules"`
Interval float64 `json:"interval"`
Rules []rule `json:"rules"`
Interval float64 `json:"interval"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
}
type rule interface{}
type alertingRule struct {
// State can be "pending", "firing", "inactive".
State string `json:"state"`
Name string `json:"name"`
Query string `json:"query"`
Duration float64 `json:"duration"`
Labels labels.Labels `json:"labels"`
Annotations labels.Labels `json:"annotations"`
Alerts []*Alert `json:"alerts"`
Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"`
State string `json:"state"`
Name string `json:"name"`
Query string `json:"query"`
Duration float64 `json:"duration"`
Labels labels.Labels `json:"labels"`
Annotations labels.Labels `json:"annotations"`
Alerts []*Alert `json:"alerts"`
Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
// Type of an alertingRule is always "alerting".
Type string `json:"type"`
}
type recordingRule struct {
Name string `json:"name"`
Query string `json:"query"`
Labels labels.Labels `json:"labels,omitempty"`
Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"`
Name string `json:"name"`
Query string `json:"query"`
Labels labels.Labels `json:"labels,omitempty"`
Health rules.RuleHealth `json:"health"`
LastError string `json:"lastError,omitempty"`
EvaluationTime float64 `json:"evaluationTime"`
LastEvaluation time.Time `json:"lastEvaluation"`
// Type of a recordingRule is always "recording".
Type string `json:"type"`
}
@ -943,10 +949,12 @@ func (api *API) rules(r *http.Request) apiFuncResult {
for i, grp := range ruleGroups {
apiRuleGroup := &RuleGroup{
Name: grp.Name(),
File: grp.File(),
Interval: grp.Interval().Seconds(),
Rules: []rule{},
Name: grp.Name(),
File: grp.File(),
Interval: grp.Interval().Seconds(),
Rules: []rule{},
EvaluationTime: grp.GetEvaluationDuration().Seconds(),
LastEvaluation: grp.GetEvaluationTimestamp(),
}
for _, r := range grp.Rules() {
var enrichedRule rule
@ -961,28 +969,32 @@ func (api *API) rules(r *http.Request) apiFuncResult {
break
}
enrichedRule = alertingRule{
State: rule.State().String(),
Name: rule.Name(),
Query: rule.Query().String(),
Duration: rule.Duration().Seconds(),
Labels: rule.Labels(),
Annotations: rule.Annotations(),
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
Health: rule.Health(),
LastError: lastError,
Type: "alerting",
State: rule.State().String(),
Name: rule.Name(),
Query: rule.Query().String(),
Duration: rule.Duration().Seconds(),
Labels: rule.Labels(),
Annotations: rule.Annotations(),
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
Health: rule.Health(),
LastError: lastError,
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
LastEvaluation: rule.GetEvaluationTimestamp(),
Type: "alerting",
}
case *rules.RecordingRule:
if !returnRecording {
break
}
enrichedRule = recordingRule{
Name: rule.Name(),
Query: rule.Query().String(),
Labels: rule.Labels(),
Health: rule.Health(),
LastError: lastError,
Type: "recording",
Name: rule.Name(),
Query: rule.Query().String(),
Labels: rule.Labels(),
Health: rule.Health(),
LastError: lastError,
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
LastEvaluation: rule.GetEvaluationTimestamp(),
Type: "recording",
}
default:
err := errors.Errorf("failed to assert type of rule '%v'", rule.Name())

View file

@ -237,3 +237,14 @@ button.execute-btn {
margin-right: 5px;
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 Checkbox from '../../components/Checkbox';
import { isPresent } from '../../utils';
import { Rule } from '../../types/types';
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> {
firing: T;
pending: T;
@ -29,7 +18,7 @@ export interface AlertsProps {
statsCount: RuleStatus<number>;
}
interface Alert {
export interface Alert {
labels: Record<string, string>;
state: RuleState;
value: string;

View file

@ -1,9 +1,11 @@
import React, { FC, useState, Fragment } from 'react';
import { Link } from '@reach/router';
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { createExpressionLink } from '../../utils/index';
interface CollapsibleAlertPanelProps {
rule: Rule;
@ -16,10 +18,6 @@ const alertColors: RuleStatus<string> = {
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 [open, toggle] = useState(false);

View file

@ -1,15 +1,16 @@
import React, { FC } from 'react';
import { RouteComponentProps } from '@reach/router';
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 }) => (
<>
<h2>Rules</h2>
<Alert color="warning">
This page is still under construction. Please try it in the <a href={`${pathPrefix}/rules`}>Classic UI</a>.
</Alert>
</>
);
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`);
return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
};
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 {
[key: string]: string;
}
@ -7,3 +9,18 @@ export interface QueryParams {
endTime: 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[]) => {
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`;
};