Alerting: Add instances with no label value to an ungrouped group (#113170)

add instances with no label value to an ungrouped group in alerting
triage – these will be collapsed by default
This commit is contained in:
Gilles De Mey
2025-10-29 15:21:05 +01:00
committed by GitHub
parent 19826b5b26
commit 87794bec12
6 changed files with 46 additions and 24 deletions
@@ -1,4 +1,5 @@
import { css } from '@emotion/css';
import { isString } from 'lodash';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -27,7 +28,7 @@ export const FolderGroupRow = ({ row, leftColumnWidth, rowKey, depth = 0, childr
title={
<Stack direction="row" gap={0.5} alignItems="center">
<MetaText icon="folder" />
<Text color="primary">{row.metadata.value}</Text>
{isString(row.metadata.value) && <Text color="primary">{row.metadata.value}</Text>}
</Stack>
}
isOpenByDefault={true}
@@ -5,9 +5,10 @@ import { AlertLabel } from '@grafana/alerting/unstable';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { GenericGroupedRow } from '../types';
import { EmptyLabelValue, GenericGroupedRow } from '../types';
import { GenericRow } from './GenericRow';
import { formatLabelValue } from './utils';
interface GroupRowProps {
row: GenericGroupedRow;
@@ -19,13 +20,21 @@ interface GroupRowProps {
export const GroupRow = ({ row, leftColumnWidth, rowKey, depth = 0, children }: GroupRowProps) => {
const styles = useStyles2(getStyles);
const isEmptyValue = row.metadata.value === EmptyLabelValue;
return (
<GenericRow
key={rowKey}
width={leftColumnWidth}
title={<AlertLabel size="sm" labelKey={row.metadata.label} value={row.metadata.value} colorBy="key" />}
isOpenByDefault={true}
title={
<AlertLabel
size="sm"
labelKey={row.metadata.label}
value={formatLabelValue(row.metadata.value)}
colorBy="key"
/>
}
isOpenByDefault={!isEmptyValue}
leftColumnClassName={styles.groupRow}
rightColumnClassName={styles.groupRow}
depth={depth}
@@ -1,4 +1,4 @@
import { WorkbenchRow } from '../types';
import { EmptyLabelValue, LabelValue, WorkbenchRow } from '../types';
// Generate unique keys for WorkbenchRow items
export function generateRowKey(row: WorkbenchRow, fallbackIndex: number): string {
@@ -8,6 +8,8 @@ export function generateRowKey(row: WorkbenchRow, fallbackIndex: number): string
} else {
// For GenericGroupedRow, create key from label and value
const groupedRow = row;
return `group-${groupedRow.metadata.label}-${groupedRow.metadata.value}`;
return `group-${groupedRow.metadata.label}-${formatLabelValue(groupedRow.metadata.value)}`;
}
}
export const formatLabelValue = (value: LabelValue): string => (value === EmptyLabelValue ? '<no value>' : value);
@@ -1,3 +1,4 @@
import { isEmpty } from 'lodash';
import { ArrayValues } from 'type-fest';
import { DataFrame, PanelData } from '@grafana/data';
@@ -6,7 +7,7 @@ import { useQueryRunner, useTimeRange, useVariableValues } from '@grafana/scenes
import { Workbench } from '../Workbench';
import { DEFAULT_FIELDS, METRIC_NAME, VARIABLES } from '../constants';
import { AlertRuleRow, GenericGroupedRow, WorkbenchRow } from '../types';
import { AlertRuleRow, EmptyLabelValue, GenericGroupedRow, WorkbenchRow } from '../types';
import { convertTimeRangeToDomain, getDataQuery, useQueryFilter } from './utils';
@@ -79,29 +80,40 @@ function groupData(dataPoints: DataPoint[], groupBy: string[], depth: number): W
}
const groupByKey = groupBy[depth];
const grouped = new Map<string, DataPoint[]>();
const grouped = new Map<string | typeof EmptyLabelValue, DataPoint[]>();
for (const dp of dataPoints) {
const key = String(dp[groupByKey] ?? 'undefined');
if (!grouped.has(key)) {
grouped.set(key, []);
const mapKey = dp[groupByKey] ?? EmptyLabelValue;
if (!grouped.has(mapKey)) {
grouped.set(mapKey, []);
}
grouped.get(key)?.push(dp);
grouped.get(mapKey)?.push(dp);
}
const result: GenericGroupedRow[] = [];
const emptyGroups: GenericGroupedRow[] = [];
for (const [value, rows] of grouped.entries()) {
result.push({
const labelValue = isEmpty(value) ? EmptyLabelValue : value;
const group: GenericGroupedRow = {
type: 'group',
metadata: {
label: groupByKey,
value: value,
value: labelValue,
},
rows: groupData(rows, groupBy, depth + 1),
});
};
// Separate empty label groups to append at the end
if (group.metadata.value === EmptyLabelValue) {
emptyGroups.push(group);
} else {
result.push(group);
}
}
return result;
return [...result, ...emptyGroups];
}
// @TODO narrower types for PanelData! (if possible)
@@ -1,6 +1,6 @@
import { TimeRange } from '@grafana/data';
import { SceneDataQuery } from '@grafana/scenes';
import { useVariableValue, useVariableValues } from '@grafana/scenes-react';
import { useVariableValue } from '@grafana/scenes-react';
import { DataSourceRef } from '@grafana/schema';
import { DATASOURCE_UID, VARIABLES } from '../constants';
@@ -44,11 +44,6 @@ export function convertTimeRangeToDomain(timeRange: TimeRange): Domain {
* This hook will create a Prometheus label matcher string from the "groupBy" and "filters" variables
*/
export function useQueryFilter(): string {
const [groupBy = []] = useVariableValues<string>(VARIABLES.groupBy);
const [filters = ''] = useVariableValue<string>(VARIABLES.filters);
const groupByFilter = stringifyGroupFilter(groupBy);
const queryFilter = [groupByFilter, filters].filter((s) => Boolean(s)).join(',');
return queryFilter;
return filters;
}
@@ -18,7 +18,10 @@ export interface GenericGroupedRow {
type: 'group';
metadata: {
label: string;
value: string;
value: LabelValue;
};
rows: WorkbenchRow[];
}
export type LabelValue = string | typeof EmptyLabelValue;
export const EmptyLabelValue = Symbol('empty label value');