mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 20:59:35 +08:00
Alerting: Make rules clickable in rule group detail view (#105622)
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
import { set } from 'lodash';
|
||||
|
||||
import { RelativeTimeRange } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n/internal';
|
||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
@ -49,11 +47,18 @@ export interface Datasource {
|
||||
export const PREVIEW_URL = '/api/v1/rule/test/grafana';
|
||||
export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules';
|
||||
|
||||
// for some reason vanilla Prometheus uses param notation with [] appended
|
||||
export enum PrometheusAPIFilters {
|
||||
RuleName = 'rule_name',
|
||||
RuleNameVanilla = 'rule_name[]',
|
||||
RuleGroup = 'rule_group',
|
||||
RuleGroupVanilla = 'rule_group[]',
|
||||
Namespace = 'file',
|
||||
NamespaceVanilla = 'file[]',
|
||||
FolderUID = 'folder_uid',
|
||||
LimitAlerts = 'limit_alerts',
|
||||
MaxGroups = 'max_groups',
|
||||
ExcludeAlerts = 'exclude_alerts',
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
@ -146,7 +151,9 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
|
||||
if (identifier && (isPrometheusRuleIdentifier(identifier) || isCloudRuleIdentifier(identifier))) {
|
||||
searchParams.set(PrometheusAPIFilters.Namespace, identifier.namespace);
|
||||
searchParams.set(PrometheusAPIFilters.NamespaceVanilla, identifier.namespace);
|
||||
searchParams.set(PrometheusAPIFilters.RuleGroup, identifier.groupName);
|
||||
searchParams.set(PrometheusAPIFilters.RuleGroupVanilla, identifier.groupName);
|
||||
}
|
||||
|
||||
const filterParams = getRulesFilterSearchParams(filter);
|
||||
@ -193,22 +200,23 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
|
||||
if (namespace) {
|
||||
if (isGrafanaRulesSource(ruleSourceName)) {
|
||||
set(queryParams, PrometheusAPIFilters.FolderUID, namespace);
|
||||
queryParams[PrometheusAPIFilters.FolderUID] = namespace;
|
||||
} else {
|
||||
set(queryParams, PrometheusAPIFilters.Namespace, namespace);
|
||||
queryParams[PrometheusAPIFilters.Namespace] = namespace;
|
||||
queryParams[PrometheusAPIFilters.NamespaceVanilla] = namespace;
|
||||
}
|
||||
}
|
||||
|
||||
if (limitAlerts !== undefined) {
|
||||
set(queryParams, PrometheusAPIFilters.LimitAlerts, String(limitAlerts));
|
||||
queryParams[PrometheusAPIFilters.LimitAlerts] = String(PrometheusAPIFilters.LimitAlerts);
|
||||
}
|
||||
|
||||
if (maxGroups) {
|
||||
set(queryParams, 'max_groups', maxGroups);
|
||||
queryParams[PrometheusAPIFilters.MaxGroups] = String(maxGroups);
|
||||
}
|
||||
|
||||
if (excludeAlerts) {
|
||||
set(queryParams, 'exclude_alerts', 'true');
|
||||
queryParams[PrometheusAPIFilters.ExcludeAlerts] = 'true';
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import { Route, Routes } from 'react-router-dom-v5-compat';
|
||||
import { Props } from 'react-virtualized-auto-sizer';
|
||||
import { render, screen, waitFor } from 'test/test-utils';
|
||||
import { render, screen, waitFor, within } from 'test/test-utils';
|
||||
import { byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -100,7 +100,7 @@ describe('GroupDetailsPage', () => {
|
||||
// Assert
|
||||
expect(header).toHaveTextContent('test-group-cpu');
|
||||
expect(await screen.findByRole('link', { name: /test-folder-title/ })).toBeInTheDocument();
|
||||
expect(await screen.findByText(/5m/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(group.interval!)).toBeInTheDocument();
|
||||
expect(editLink).toHaveAttribute(
|
||||
'href',
|
||||
'/alerting/grafana/namespaces/test-folder-uid/groups/test-group-cpu/edit?returnTo=%2Falerting%2Fgrafana%2Fnamespaces%2Ftest-folder-uid%2Fgroups%2Ftest-group-cpu%2Fview'
|
||||
@ -109,12 +109,18 @@ describe('GroupDetailsPage', () => {
|
||||
const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find());
|
||||
expect(tableRows).toHaveLength(2);
|
||||
|
||||
expect(tableRows[0]).toHaveTextContent('High CPU Usage');
|
||||
expect(tableRows[0]).toHaveTextContent('10m');
|
||||
expect(within(tableRows[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute(
|
||||
'href',
|
||||
`/alerting/grafana/${rule1.grafana_alert.uid}/view`
|
||||
);
|
||||
expect(tableRows[0]).toHaveTextContent(String(rule1.for));
|
||||
expect(tableRows[0]).toHaveTextContent('5');
|
||||
|
||||
expect(tableRows[1]).toHaveTextContent('Memory Pressure');
|
||||
expect(tableRows[1]).toHaveTextContent('5m');
|
||||
expect(within(tableRows[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute(
|
||||
'href',
|
||||
`/alerting/grafana/${rule2.grafana_alert.uid}/view`
|
||||
);
|
||||
expect(tableRows[1]).toHaveTextContent(String(rule2.for));
|
||||
expect(tableRows[1]).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
|
@ -16,10 +16,12 @@ import { DynamicTable, DynamicTableColumnProps } from '../components/DynamicTabl
|
||||
import { GrafanaRuleGroupExporter } from '../components/export/GrafanaRuleGroupExporter';
|
||||
import { useFolder } from '../hooks/useFolder';
|
||||
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../rule-editor/formDefaults';
|
||||
import { createViewLinkFromIdentifier } from '../rule-list/DataSourceRuleListItem';
|
||||
import { useRulesAccess } from '../utils/accessControlHooks';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getDataSourceByUid } from '../utils/datasource';
|
||||
import { makeFolderLink, stringifyErrorLike } from '../utils/misc';
|
||||
import { createListFilterLink, groups } from '../utils/navigation';
|
||||
import { fromRule, fromRulerRule } from '../utils/rule-id';
|
||||
import {
|
||||
calcRuleEvalsToStartAlerting,
|
||||
getRuleName,
|
||||
@ -78,6 +80,7 @@ function GroupDetailsPage() {
|
||||
);
|
||||
const { t } = useTranslate();
|
||||
|
||||
const ruleSourceName = isGrafanaRuleGroup ? GRAFANA_RULES_SOURCE_NAME : getDataSourceByUid(dataSourceUid)?.name;
|
||||
const isLoading = isFolderLoading || isDsFeaturesLoading || isRuleNamespacesLoading || isRuleGroupLoading;
|
||||
|
||||
const groupInterval = promGroup?.interval
|
||||
@ -148,8 +151,12 @@ function GroupDetailsPage() {
|
||||
<div>{stringifyErrorLike(ruleNamespacesError || ruleGroupError)}</div>
|
||||
</Alert>
|
||||
)}
|
||||
{promGroup && <GroupDetails group={promRuleGroupToRuleGroupDetails(promGroup)} />}
|
||||
{rulerGroup && <GroupDetails group={rulerRuleGroupToRuleGroupDetails(rulerGroup)} />}
|
||||
{promGroup && ruleSourceName && (
|
||||
<GroupDetails group={promRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, promGroup)} />
|
||||
)}
|
||||
{rulerGroup && ruleSourceName && (
|
||||
<GroupDetails group={rulerRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, rulerGroup)} />
|
||||
)}
|
||||
{!promGroup && !rulerGroup && <EntityNotFound entity={`${namespaceId}/${groupName}`} />}
|
||||
</>
|
||||
</AlertingPageWrapper>
|
||||
@ -213,12 +220,14 @@ interface RuleGroupDetails {
|
||||
|
||||
interface AlertingRuleDetails {
|
||||
name: string;
|
||||
href?: string;
|
||||
type: 'alerting';
|
||||
pendingPeriod: string;
|
||||
evaluationsToFire: number;
|
||||
}
|
||||
interface RecordingRuleDetails {
|
||||
name: string;
|
||||
href?: string;
|
||||
type: 'recording';
|
||||
}
|
||||
|
||||
@ -248,8 +257,16 @@ function RulesTable({ rules }: { rules: RuleDetails[] }) {
|
||||
{
|
||||
id: 'alertName',
|
||||
label: t('alerting.group-details.rule-name', 'Rule name'),
|
||||
renderCell: ({ data }) => {
|
||||
return <Text truncate>{data.name}</Text>;
|
||||
renderCell: ({ data: { name, href } }) => {
|
||||
if (href) {
|
||||
return (
|
||||
<TextLink href={href} inline={false} color="primary">
|
||||
{name}
|
||||
</TextLink>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text truncate>{name}</Text>;
|
||||
},
|
||||
size: 0.4,
|
||||
},
|
||||
@ -285,29 +302,41 @@ function RulesTable({ rules }: { rules: RuleDetails[] }) {
|
||||
return <DynamicTable items={rows} cols={columns} />;
|
||||
}
|
||||
|
||||
function promRuleGroupToRuleGroupDetails(group: RuleGroup): RuleGroupDetails {
|
||||
function promRuleGroupToRuleGroupDetails(
|
||||
ruleSourceName: string,
|
||||
namespaceName: string,
|
||||
group: RuleGroup
|
||||
): RuleGroupDetails {
|
||||
const groupIntervalMs = group.interval * 1000;
|
||||
|
||||
return {
|
||||
name: group.name,
|
||||
interval: formatPrometheusDuration(group.interval * 1000),
|
||||
rules: group.rules.map<RuleDetails>((rule) => {
|
||||
const ruleIdentifier = fromRule(ruleSourceName, namespaceName, group.name, rule);
|
||||
const href = ruleIdentifier ? createViewLinkFromIdentifier(ruleIdentifier) : undefined;
|
||||
|
||||
switch (rule.type) {
|
||||
case PromRuleType.Alerting:
|
||||
return {
|
||||
name: rule.name,
|
||||
href,
|
||||
type: 'alerting',
|
||||
pendingPeriod: formatPrometheusDuration(rule.duration ? rule.duration * 1000 : 0),
|
||||
evaluationsToFire: calcRuleEvalsToStartAlerting(rule.duration ? rule.duration * 1000 : 0, groupIntervalMs),
|
||||
};
|
||||
case PromRuleType.Recording:
|
||||
return { name: rule.name, type: 'recording' };
|
||||
return { name: rule.name, href, type: 'recording' };
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDetails {
|
||||
function rulerRuleGroupToRuleGroupDetails(
|
||||
ruleSourceName: string,
|
||||
namespaceName: string,
|
||||
group: RulerRuleGroupDTO
|
||||
): RuleGroupDetails {
|
||||
const groupIntervalMs = safeParsePrometheusDuration(group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
|
||||
|
||||
return {
|
||||
@ -316,9 +345,13 @@ function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDe
|
||||
rules: group.rules.map<RuleDetails>((rule) => {
|
||||
const name = getRuleName(rule);
|
||||
|
||||
const ruleIdentifier = fromRulerRule(ruleSourceName, namespaceName, group.name, rule);
|
||||
const href = createViewLinkFromIdentifier(ruleIdentifier);
|
||||
|
||||
if (rulerRuleType.any.alertingRule(rule)) {
|
||||
return {
|
||||
name,
|
||||
href,
|
||||
type: 'alerting',
|
||||
pendingPeriod: rule.for ?? '0s',
|
||||
evaluationsToFire: calcRuleEvalsToStartAlerting(
|
||||
@ -328,7 +361,7 @@ function rulerRuleGroupToRuleGroupDetails(group: RulerRuleGroupDTO): RuleGroupDe
|
||||
};
|
||||
}
|
||||
|
||||
return { name, type: 'recording' };
|
||||
return { name, href, type: 'recording' };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user