Alerting: misc fixes (#33070)

This commit is contained in:
Domas 2021-04-19 12:53:02 +03:00 committed by GitHub
parent 41f6af96c4
commit 382cab6406
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 137 additions and 56 deletions

View File

@ -125,6 +125,11 @@ describe('RuleList', () => {
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),

View File

@ -17,6 +17,7 @@ import RulesFilter from './components/rules/RulesFilter';
import { RuleListGroupView } from './components/rules/RuleListGroupView';
import { RuleListStateView } from './components/rules/RuleListStateView';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { config } from '@grafana/runtime';
const VIEWS = {
groups: RuleListGroupView,
@ -105,13 +106,15 @@ export const RuleList: FC = () => {
)}
{promReqeustErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules state from <a href={`datasources/edit/${dataSource.id}`}>{dataSource.name}</a>:{' '}
Failed to load rules state from{' '}
<a href={`${config.appSubUrl ?? ''}/datasources/edit/${dataSource.id}`}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
{rulerRequestErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules config from <a href={`datasources/edit/${dataSource.id}`}>{dataSource.name}</a>:{' '}
Failed to load rules config from{' '}
<a href={`${config.appSubUrl ?? ''}/datasources/edit/${dataSource.id}`}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
@ -123,19 +126,19 @@ export const RuleList: FC = () => {
<div className={styles.break} />
<div className={styles.buttonsContainer}>
<ButtonGroup>
<a href={urlUtil.renderUrl('/alerting/list', { ...queryParams, view: 'group' })}>
<a href={urlUtil.renderUrl(`${config.appSubUrl ?? ''}/alerting/list`, { ...queryParams, view: 'group' })}>
<ToolbarButton variant={view === 'groups' ? 'active' : 'default'} icon="folder">
Groups
</ToolbarButton>
</a>
<a href={urlUtil.renderUrl('/alerting/list', { ...queryParams, view: 'state' })}>
<a href={urlUtil.renderUrl(`${config.appSubUrl ?? ''}/alerting/list`, { ...queryParams, view: 'state' })}>
<ToolbarButton variant={view === 'state' ? 'active' : 'default'} icon="heart-rate">
State
</ToolbarButton>
</a>
</ButtonGroup>
<div />
<a href="/alerting/new">
<a href={`${config.appSubUrl ?? ''}/alerting/new`}>
<Button icon="plus">New alert rule</Button>
</a>
</div>

View File

@ -18,6 +18,7 @@ import { useDispatch } from 'react-redux';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { rulerRuleToFormValues, defaultFormValues } from '../../utils/rule-form';
import { Link } from 'react-router-dom';
import { config } from '@grafana/runtime';
type Props = {
existing?: RuleWithLocation;
@ -56,6 +57,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
dispatch(
saveRuleFormAction({
values: {
...defaultValues,
...values,
annotations: values.annotations?.filter(({ key }) => !!key) ?? [],
labels: values.labels?.filter(({ key }) => !!key) ?? [],
@ -70,7 +72,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
<FormContext {...formAPI}>
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
<Link to="/alerting/list">
<Link to={`${config.appSubUrl ?? ''}/alerting/list`}>
<ToolbarButton variant="default" disabled={submitState.loading} type="button">
Cancel
</ToolbarButton>

View File

@ -1,11 +1,12 @@
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import React, { FC } from 'react';
import { config } from '@grafana/runtime';
export const NoRulesSplash: FC = () => (
<EmptyListCTA
title="You haven`t created any alert rules yet"
buttonIcon="bell"
buttonLink="/alerting/new"
buttonLink={`${config.appSubUrl ?? ''}/alerting/new`}
buttonTitle="New alert rule"
proTip="you can also create alert rules from existing panels and queries."
proTipLink="https://grafana.com/docs/"

View File

@ -1,5 +1,5 @@
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { useStyles } from '@grafana/ui';
import { css, cx } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
@ -10,6 +10,7 @@ import { AlertLabels } from '../AlertLabels';
import { AlertInstancesTable } from './AlertInstancesTable';
import { DetailsField } from './DetailsField';
import { RuleQuery } from './RuleQuery';
import { getDataSourceSrv } from '@grafana/runtime';
interface Props {
rule: CombinedRule;
@ -23,6 +24,23 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
const annotations = Object.entries(rule.annotations);
const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => {
if (isCloudRulesSource(rulesSource)) {
return [{ name: rulesSource.name, icon: rulesSource.meta.info.logos.small }];
} else if (rule.queries) {
return rule.queries
.map(({ datasource }) => {
const ds = getDataSourceSrv().getInstanceSettings(datasource);
if (ds) {
return { name: ds.name, icon: ds.meta.info.logos.small };
}
return { name: datasource };
})
.filter(({ name }) => name !== '__expr__');
}
return [];
}, [rule, rulesSource]);
return (
<div>
<div className={styles.wrapper}>
@ -42,9 +60,18 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
))}
</div>
<div className={styles.rightSide}>
{isCloudRulesSource(rulesSource) && (
{!!dataSources.length && (
<DetailsField label="Data source">
<img className={styles.dataSourceIcon} src={rulesSource.meta.info.logos.small} /> {rulesSource.name}
{dataSources.map(({ name, icon }) => (
<div key={name}>
{icon && (
<>
<img className={styles.dataSourceIcon} src={icon} />{' '}
</>
)}
{name}
</div>
))}
</DetailsField>
)}
</div>

View File

@ -1,6 +1,8 @@
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { CombinedRule, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import React, { FC, useMemo } from 'react';
import { getFiltersFromUrlParams } from '../../utils/misc';
import { isAlertingRule } from '../../utils/rules';
import { RuleListStateSection } from './RuleListSateSection';
@ -11,6 +13,8 @@ interface Props {
type GroupedRules = Record<PromAlertingRuleState, CombinedRule[]>;
export const RuleListStateView: FC<Props> = ({ namespaces }) => {
const filters = getFiltersFromUrlParams(useQueryParams()[0]);
const groupedRules = useMemo(() => {
const result: GroupedRules = {
[PromAlertingRuleState.Firing]: [],
@ -34,13 +38,22 @@ export const RuleListStateView: FC<Props> = ({ namespaces }) => {
}, [namespaces]);
return (
<>
<RuleListStateSection state={PromAlertingRuleState.Firing} rules={groupedRules[PromAlertingRuleState.Firing]} />
<RuleListStateSection state={PromAlertingRuleState.Pending} rules={groupedRules[PromAlertingRuleState.Pending]} />
<RuleListStateSection
defaultCollapsed={true}
state={PromAlertingRuleState.Inactive}
rules={groupedRules[PromAlertingRuleState.Inactive]}
/>
{(!filters.alertState || filters.alertState === PromAlertingRuleState.Firing) && (
<RuleListStateSection state={PromAlertingRuleState.Firing} rules={groupedRules[PromAlertingRuleState.Firing]} />
)}
{(!filters.alertState || filters.alertState === PromAlertingRuleState.Pending) && (
<RuleListStateSection
state={PromAlertingRuleState.Pending}
rules={groupedRules[PromAlertingRuleState.Pending]}
/>
)}
{(!filters.alertState || filters.alertState === PromAlertingRuleState.Inactive) && (
<RuleListStateSection
defaultCollapsed={filters.alertState !== PromAlertingRuleState.Inactive}
state={PromAlertingRuleState.Inactive}
rules={groupedRules[PromAlertingRuleState.Inactive]}
/>
)}
</>
);
};

View File

@ -3,7 +3,7 @@ import React, { FC, useMemo, useState, Fragment } from 'react';
import { Icon, Tooltip, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from '@emotion/css';
import { isAlertingRule } from '../../utils/rules';
import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { StateColoredText } from '../StateColoredText';
import { CollapseToggle } from '../CollapseToggle';
@ -12,6 +12,9 @@ import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datas
import { ActionIcon } from './ActionIcon';
import pluralize from 'pluralize';
import { useHasRuler } from '../../hooks/useHasRuler';
import kbn from 'app/core/utils/kbn';
import { config } from '@grafana/runtime';
interface Props {
namespace: CombinedRuleNamespace;
group: CombinedRuleGroup;
@ -60,11 +63,28 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace }) => {
}
const actionIcons: React.ReactNode[] = [];
if (hasRuler(rulesSource)) {
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" />);
}
// for grafana, link to folder views
if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
actionIcons.push(<ActionIcon key="manage-perms" icon="lock" tooltip="manage permissions" />);
const rulerRule = group.rules[0]?.rulerRule;
const folderUID = rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid;
if (folderUID) {
const baseUrl = `${config.appSubUrl ?? ''}/dashboards/f/${folderUID}/${kbn.slugifyForUrl(namespace.name)}`;
actionIcons.push(
<ActionIcon key="edit" icon="pen" tooltip="edit" href={baseUrl + '/settings'} target="__blank" />
);
actionIcons.push(
<ActionIcon
key="manage-perms"
icon="lock"
tooltip="manage permissions"
href={baseUrl + '/permissions'}
target="__blank"
/>
);
} else if (hasRuler(rulesSource)) {
actionIcons.push(<ActionIcon key="edit" icon="pen" tooltip="edit" />); // @TODO
}
}
const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name;

View File

@ -14,6 +14,7 @@ import { useDispatch } from 'react-redux';
import { deleteRuleAction } from '../../state/actions';
import { useHasRuler } from '../../hooks/useHasRuler';
import { CombinedRule } from 'app/types/unified-alerting';
import { config } from '@grafana/runtime';
interface Props {
rules: CombinedRule[];
@ -145,7 +146,7 @@ export const RulesTable: FC<Props> = ({
<ActionIcon
icon="pen"
tooltip="edit rule"
href={`/alerting/${encodeURIComponent(
href={`${config.appSubUrl ?? ''}/alerting/${encodeURIComponent(
stringifyRuleIdentifier(
getRuleIdentifier(getRulesSourceName(rulesSource), namespace.name, group.name, rulerRule)
)

View File

@ -120,6 +120,7 @@ function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, gr
return {
name: rule.name,
query: rule.query,
queries: rule.query && isGrafanaRulesSource(namespace.rulesSource) ? JSON.parse(rule.query) : undefined,
labels: rule.labels || {},
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
promRule: rule,
@ -155,9 +156,10 @@ function rulerRuleToCombinedRule(
}
: {
name: rule.grafana_alert.title,
queries: rule.grafana_alert.data.map((d) => d.model),
query: '',
labels: rule.grafana_alert.labels || {},
annotations: rule.grafana_alert.annotations || {},
labels: rule.labels || {},
annotations: rule.annotations || {},
rulerRule: rule,
namespace,
group,

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
import { isCloudRulesSource } from '../utils/datasource';
import { isCloudRulesSource, isGrafanaRulesSource } from '../utils/datasource';
import { isAlertingRule } from '../utils/rules';
import { getFiltersFromUrlParams } from '../utils/misc';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
@ -45,7 +45,13 @@ const reduceNamespaces = (filters: RuleFilterState) => {
const reduceGroups = (filters: RuleFilterState) => {
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
const rules = group.rules.filter((rule) => {
let shouldKeep = true;
if (
filters.dataSource &&
isGrafanaRulesSource(rule.namespace.rulesSource) &&
!rule.queries?.find(({ datasource }) => datasource === filters.dataSource)
) {
return false;
}
// Query strings can match alert name, label keys, and label values
if (filters.queryString) {
const normalizedQueryString = filters.queryString.toLocaleLowerCase();
@ -56,16 +62,17 @@ const reduceGroups = (filters: RuleFilterState) => {
key.toLocaleLowerCase().includes(normalizedQueryString) ||
value.toLocaleLowerCase().includes(normalizedQueryString)
);
shouldKeep = doesNameContainsQueryString || doLabelsContainQueryString;
if (!(doesNameContainsQueryString || doLabelsContainQueryString)) {
return false;
}
}
if (filters.alertState) {
const matchesAlertState = Boolean(
rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filters.alertState
);
shouldKeep = shouldKeep && matchesAlertState;
if (
filters.alertState &&
!(rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filters.alertState)
) {
return false;
}
return shouldKeep;
return true;
});
// Add rules to the group that match the rule list filters
if (rules.length) {

View File

@ -1,5 +1,5 @@
import { AppEvents } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { locationService, config } from '@grafana/runtime';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { appEvents } from 'app/core/core';
import { AlertManagerCortexConfig, Silence } from 'app/plugins/datasource/alertmanager/types';
@ -159,7 +159,8 @@ export function deleteRuleAction(ruleIdentifier: RuleIdentifier): ThunkResult<vo
}
await deleteRule(ruleWithLocation);
// refetch rules for this rules source
return dispatch(fetchRulerRulesAction(ruleWithLocation.ruleSourceName));
dispatch(fetchRulerRulesAction(ruleWithLocation.ruleSourceName));
dispatch(fetchPromRulesAction(ruleWithLocation.ruleSourceName));
};
}
@ -295,10 +296,12 @@ export const saveRuleFormAction = createAsyncThunk(
throw new Error('Unexpected rule form type');
}
if (exitOnSave) {
locationService.push('/alerting/list');
locationService.push(`${config.appSubUrl ?? ''}/alerting/list`);
} else {
// redirect to edit page
const newLocation = `/alerting/${encodeURIComponent(stringifyRuleIdentifier(identifier))}/edit`;
const newLocation = `${config.appSubUrl ?? ''}/alerting/${encodeURIComponent(
stringifyRuleIdentifier(identifier)
)}/edit`;
if (locationService.getLocation().pathname !== newLocation) {
locationService.replace(newLocation);
}

View File

@ -1,4 +1,3 @@
import { describeInterval, secondsToHms } from '@grafana/data/src/datetime/rangeutil';
import { RuleWithLocation } from 'app/types/unified-alerting';
import {
Annotations,
@ -55,11 +54,6 @@ function parseInterval(value: string): [number, string] {
throw new Error(`Invalid interval description: ${value}`);
}
function intervalToSeconds(interval: string): number {
const { sec, count } = describeInterval(interval);
return sec * count;
}
function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Array<{ key: string; value: string }> {
return [...recordToArray(item || {}), { key: '', value: '' }];
}
@ -71,13 +65,13 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
grafana_alert: {
title: name,
condition,
for: intervalToSeconds(evaluateFor), // @TODO provide raw string once backend supports it
no_data_state: noDataState,
exec_err_state: execErrState,
data: queries,
annotations: arrayToRecord(values.annotations || []),
labels: arrayToRecord(values.labels || []),
},
for: evaluateFor,
annotations: arrayToRecord(values.annotations || []),
labels: arrayToRecord(values.labels || []),
};
}
throw new Error('Cannot create rule without specifying alert condition');
@ -93,14 +87,14 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
name: ga.title,
type: RuleFormType.threshold,
dataSourceName: ga.data[0]?.model.datasource,
evaluateFor: secondsToHms(ga.for),
evaluateFor: rule.for,
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
noDataState: ga.no_data_state,
execErrState: ga.exec_err_state,
queries: ga.data,
condition: ga.condition,
annotations: listifyLabelsOrAnnotations(ga.annotations),
labels: listifyLabelsOrAnnotations(ga.labels),
annotations: listifyLabelsOrAnnotations(rule.annotations),
labels: listifyLabelsOrAnnotations(rule.labels),
folder: { title: namespace, id: -1 },
};
} else {

View File

@ -113,12 +113,9 @@ export interface PostableGrafanaRuleDefinition {
uid?: string;
title: string;
condition: string;
for: number; //@TODO Sofia will update to accept string
no_data_state: GrafanaAlertState;
exec_err_state: GrafanaAlertState;
data: GrafanaQuery[];
annotations: Annotations;
labels: Labels;
}
export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
uid: string;
@ -127,12 +124,16 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
export interface RulerGrafanaRuleDTO {
grafana_alert: GrafanaRuleDefinition;
// labels?: Labels; @TODO to be discussed
// annotations?: Annotations;
for: string;
annotations: Annotations;
labels: Labels;
}
export interface PostableRuleGrafanaRuleDTO {
grafana_alert: PostableGrafanaRuleDefinition;
for: string;
annotations: Annotations;
labels: Labels;
}
export type RulerRuleDTO = RulerAlertingRuleDTO | RulerRecordingRuleDTO | RulerGrafanaRuleDTO;

View File

@ -8,6 +8,7 @@ import {
Labels,
Annotations,
RulerRuleGroupDTO,
GrafanaQueryModel,
} from './unified-alerting-dto';
export type Alert = {
@ -81,6 +82,7 @@ export interface CombinedRule {
rulerRule?: RulerRuleDTO;
group: CombinedRuleGroup;
namespace: CombinedRuleNamespace;
queries?: GrafanaQueryModel[];
}
export interface CombinedRuleGroup {