Alerting: List V2 - Add a group link to the rule list item (#108960)

* Add group link to the aler rule breadcrumbs in the new list view

* Remove redundant comments
This commit is contained in:
Konrad Lalik
2025-07-31 15:32:33 +02:00
committed by GitHub
parent 37ccbd77a9
commit 9907191a7b
5 changed files with 108 additions and 38 deletions
@@ -5,6 +5,7 @@ import { PromRuleType, RulerRuleDTO, RulesSourceApplication } from 'app/types/un
import { createReturnTo } from '../hooks/useReturnTo';
import { Annotation } from '../utils/constants';
import { groups } from '../utils/navigation';
import { fromRule, fromRulerRule, stringifyIdentifier } from '../utils/rule-id';
import { getRuleName, getRulePluginOrigin, rulerRuleType } from '../utils/rules';
import { createRelativeUrl } from '../utils/url';
@@ -46,11 +47,14 @@ export function DataSourceRuleListItem({
const ruleName = rulerRule ? getRuleName(rulerRule) : rule.name;
const labels = rulerRule ? rulerRule.labels : rule.labels;
const groupUrl = groups.detailsPageLink(rulesSource.uid, namespace.name, groupName);
const commonProps: RuleListItemCommonProps = {
name: ruleName,
rulesSource: rulesSource,
application: application,
group: groupName,
groupUrl,
namespace: namespace.name,
href,
health: rule.health,
@@ -107,6 +107,30 @@ describe('RuleList - FilterView', () => {
expect(await screen.findByText(/No matching rules found/)).toBeInTheDocument();
});
it('should render group names as clickable links', async () => {
render(
<FilterView
filterState={getFilter({
dataSourceNames: ['Mimir'],
groupName: 'test-group-4501',
ruleName: 'mimir-test-rule-1',
})}
/>
);
await loadMoreResults();
const groupLink = await screen.findByRole('link', {
name: 'test-group-4501',
});
expect(groupLink).toBeInTheDocument();
expect(groupLink).toHaveAttribute(
'href',
'/alerting/mimir/namespaces/test-mimir-namespace/groups/test-group-4501/view'
);
});
});
async function loadMoreResults() {
@@ -1,7 +1,8 @@
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto';
import { GrafanaRulesSource } from '../utils/datasource';
import { GRAFANA_RULES_SOURCE_NAME, GrafanaRulesSource } from '../utils/datasource';
import { groups } from '../utils/navigation';
import { totalFromStats } from '../utils/ruleStats';
import { prometheusRuleType } from '../utils/rules';
import { createRelativeUrl } from '../utils/url';
@@ -32,10 +33,17 @@ export function GrafanaRuleListItem({
}: GrafanaRuleListItemProps) {
const { name, uid, labels, provenance } = rule;
const groupUrl = groups.detailsPageLink(
GRAFANA_RULES_SOURCE_NAME,
groupIdentifier.namespace.uid,
groupIdentifier.groupName
);
const commonProps: RuleListItemCommonProps = {
name,
rulesSource: GrafanaRulesSource,
group: groupIdentifier.groupName,
groupUrl,
namespace: namespaceName,
href: createRelativeUrl(`/alerting/grafana/${uid}/view`),
health: rule?.health,
@@ -4,7 +4,7 @@ import { ReactNode, useEffect, useId } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Alert, Icon, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
import { Alert, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
import { Rule, RuleGroupIdentifierV2, RuleHealth, RulesSourceIdentifier } from 'app/types/unified-alerting';
import { Labels, PromAlertingRuleState, RulerRuleDTO, RulesSourceApplication } from 'app/types/unified-alerting-dto';
@@ -20,8 +20,8 @@ import { createContactPointSearchLink } from '../../utils/misc';
import { RulePluginOrigin } from '../../utils/rules';
import { ListItem } from './ListItem';
import { DataSourceIcon } from './Namespace';
import { RuleListIcon, RuleOperation } from './RuleListIcon';
import { RuleLocation } from './RuleLocation';
import { calculateNextEvaluationEstimate } from './util';
export interface AlertRuleListItemProps {
@@ -39,6 +39,7 @@ export interface AlertRuleListItemProps {
instancesCount?: number;
namespace?: string;
group?: string;
groupUrl?: string;
rulesSource?: RulesSourceIdentifier;
application?: RulesSourceApplication;
// used for alert rules that use simplified routing
@@ -65,6 +66,7 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
instancesCount = 0,
namespace,
group,
groupUrl,
rulesSource,
application,
contactPoint,
@@ -81,7 +83,13 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
if (namespace && group && showLocation) {
metadata.push(
<Text color="secondary" variant="bodySmall">
<RuleLocation namespace={namespace} group={group} rulesSource={rulesSource} application={application} />
<RuleLocation
namespace={namespace}
group={group}
groupUrl={groupUrl}
rulesSource={rulesSource}
application={application}
/>
</Text>
);
}
@@ -160,6 +168,7 @@ export function RecordingRuleListItem({
name,
namespace,
group,
groupUrl,
rulesSource,
application,
href,
@@ -175,7 +184,13 @@ export function RecordingRuleListItem({
if (namespace && group && showLocation) {
metadata.push(
<Text color="secondary" variant="bodySmall">
<RuleLocation namespace={namespace} group={group} rulesSource={rulesSource} application={application} />
<RuleLocation
namespace={namespace}
group={group}
groupUrl={groupUrl}
rulesSource={rulesSource}
application={application}
/>
</Text>
);
}
@@ -206,6 +221,7 @@ interface RuleOperationListItemProps {
name: string;
namespace: string;
group: string;
groupUrl?: string;
rulesSource?: RulesSourceIdentifier;
application?: RulesSourceApplication;
operation: RuleOperation;
@@ -216,6 +232,7 @@ export function RuleOperationListItem({
name,
namespace,
group,
groupUrl,
rulesSource,
application,
operation,
@@ -227,7 +244,13 @@ export function RuleOperationListItem({
if (namespace && group && showLocation) {
metadata.push(
<Text color="secondary" variant="bodySmall">
<RuleLocation namespace={namespace} group={group} rulesSource={rulesSource} application={application} />
<RuleLocation
namespace={namespace}
group={group}
groupUrl={groupUrl}
rulesSource={rulesSource}
application={application}
/>
</Text>
);
}
@@ -368,38 +391,6 @@ export const UnknownRuleListItem = ({ ruleName, groupIdentifier, ruleDefinition
);
};
interface RuleLocationProps {
namespace: string;
group: string;
rulesSource?: RulesSourceIdentifier;
application?: RulesSourceApplication;
}
// @TODO make the datasource / namespace / group click-able to allow further filtering of the list
export const RuleLocation = ({ namespace, group, rulesSource, application }: RuleLocationProps) => {
const isGrafanaApp = application === 'grafana';
const isDataSourceApp = !!rulesSource && !!application && !isGrafanaApp;
return (
<Stack direction="row" alignItems="center" gap={0.5}>
{isGrafanaApp && <Icon size="xs" name="folder" />}
{isDataSourceApp && (
<Tooltip content={rulesSource.name}>
<span>
<DataSourceIcon application={application} size={14} />
</span>
</Tooltip>
)}
<Stack direction="row" alignItems="center" gap={0}>
{namespace}
<Icon size="sm" name="angle-right" />
{group}
</Stack>
</Stack>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
alertListItemContainer: css({
position: 'relative',
@@ -0,0 +1,43 @@
import { Icon, Stack, TextLink, Tooltip } from '@grafana/ui';
import { RulesSourceIdentifier } from 'app/types/unified-alerting';
import { RulesSourceApplication } from 'app/types/unified-alerting-dto';
import { DataSourceIcon } from './Namespace';
interface RuleLocationProps {
namespace: string;
group: string;
groupUrl?: string;
rulesSource?: RulesSourceIdentifier;
application?: RulesSourceApplication;
}
export function RuleLocation({ namespace, group, groupUrl, rulesSource, application }: RuleLocationProps) {
const isGrafanaApp = application === 'grafana';
const isDataSourceApp = !!rulesSource && !!application && !isGrafanaApp;
return (
<Stack direction="row" alignItems="center" gap={0.5}>
{isGrafanaApp && <Icon size="xs" name="folder" />}
{isDataSourceApp && (
<Tooltip content={rulesSource.name}>
<span>
<DataSourceIcon application={application} size={14} />
</span>
</Tooltip>
)}
<Stack direction="row" alignItems="center" gap={0}>
{namespace}
<Icon size="sm" name="angle-right" />
{groupUrl ? (
<TextLink href={groupUrl} color="secondary" variant="bodySmall" inline={false}>
{group}
</TextLink>
) : (
group
)}
</Stack>
</Stack>
);
}