chore: fix undefined labels error in alerts (#9589)

* chore: fix undefined labels error in alerts

* chore: fix CI

* chore: minor fix

* chore: add tests

* chore: additional changes

* chore: additonal cleanup

* chore: update tests

* chore: update mock

* chore: update checks

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Amlan Kumar Nandy
2026-01-13 12:27:16 +07:00
committed by GitHub
parent 99b9e27bca
commit 30b98582b4
9 changed files with 247 additions and 18 deletions

View File

@@ -65,11 +65,14 @@ function Filter({
const uniqueLabels: Array<string> = useMemo(() => {
const allLabelsSet = new Set<string>();
allAlerts.forEach((e) =>
allAlerts.forEach((e) => {
if (!e.labels) {
return;
}
Object.keys(e.labels).forEach((e) => {
allLabelsSet.add(e);
}),
);
});
});
return [...allLabelsSet];
}, [allAlerts]);

View File

@@ -11,7 +11,7 @@ function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
return (
<>
{allAlerts.map((alert) => {
const { labels } = alert;
const { labels = {} } = alert;
const labelsObject = Object.keys(labels);
const tags = labelsObject.filter((e) => e !== 'severity');
@@ -33,11 +33,11 @@ function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
</TableCell>
<TableCell minWidth="90px" overflowX="scroll">
<Typography>{labels.alertname}</Typography>
<Typography>{labels.alertname || '-'}</Typography>
</TableCell>
<TableCell minWidth="90px">
<Typography>{labels.severity}</Typography>
<Typography>{labels.severity || '-'}</Typography>
</TableCell>
<TableCell minWidth="90px">

View File

@@ -15,7 +15,7 @@ function FilteredTable({
const allGroupsAlerts = useMemo(
() =>
groupBy(FilterAlerts(allAlerts, selectedFilter), (obj) =>
selectedGroup.map((e) => obj.labels[`${e.value}`]).join('+'),
selectedGroup.map((e) => obj.labels?.[`${e.value}`]).join('+'),
),
[selectedGroup, allAlerts, selectedFilter],
);
@@ -50,12 +50,12 @@ function FilteredTable({
return null;
}
const objects = tagsAlert[0].labels;
const keysArray = Object.keys(objects);
const { labels = {} } = tagsAlert[0];
const keysArray = Object.keys(labels);
const valueArray: string[] = [];
keysArray.forEach((e) => {
valueArray.push(objects[e]);
valueArray.push(labels[e]);
});
const tags = tagsValue

View File

@@ -11,6 +11,12 @@ import { Alerts } from 'types/api/alerts/getTriggered';
import { Value } from './Filter';
import { FilterAlerts } from './utils';
const severitySorter = (a: Alerts, b: Alerts): number => {
const severityLengthOfA = a.labels?.severity?.length || 0;
const severityLengthOfB = b.labels?.severity?.length || 0;
return severityLengthOfB - severityLengthOfA;
};
function NoFilterTable({
allAlerts,
selectedFilter,
@@ -25,8 +31,7 @@ function NoFilterTable({
dataIndex: 'status',
width: 80,
key: 'status',
sorter: (a, b): number =>
b.labels.severity.length - a.labels.severity.length,
sorter: (a, b): number => severitySorter(a, b),
render: (value): JSX.Element => <AlertStatus severity={value.state} />,
},
{
@@ -65,11 +70,7 @@ function NoFilterTable({
dataIndex: 'labels',
key: 'severity',
width: 100,
sorter: (a, b): number => {
const severityValueA = a.labels.severity;
const severityValueB = b.labels.severity;
return severityValueA.length - severityValueB.length;
},
sorter: (a, b): number => severitySorter(a, b),
render: (value): JSX.Element => {
const objectKeys = Object.keys(value);
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';

View File

@@ -0,0 +1,84 @@
import { fireEvent, render, screen } from '@testing-library/react';
import NoFilterTable from '../NoFilterTable';
import { createAlert } from './mockUtils';
jest.mock('providers/Timezone', () => ({
useTimezone: jest.requireActual('./mockUtils').useMockTimezone,
}));
const allAlerts = [
createAlert({
name: 'Alert B',
labels: {
severity: 'warning',
alertname: 'Alert B',
},
}),
createAlert({
name: 'Alert C',
labels: {
severity: 'info',
alertname: 'Alert C',
},
}),
createAlert({
name: 'Alert A',
labels: {
severity: 'critical',
alertname: 'Alert A',
},
}),
];
describe('NoFilterTable', () => {
it('should render the no filter table with correct rows', () => {
render(<NoFilterTable allAlerts={allAlerts} selectedFilter={[]} />);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(4); // 1 header row + 2 data rows
const [headerRow, dataRow1, dataRow2, dataRow3] = rows;
// Verify header row
expect(headerRow).toHaveTextContent('Status');
expect(headerRow).toHaveTextContent('Alert Name');
expect(headerRow).toHaveTextContent('Tags');
expect(headerRow).toHaveTextContent('Severity');
expect(headerRow).toHaveTextContent('Firing Since');
// Verify 1st data row
expect(dataRow1).toHaveTextContent('Alert B');
// Verify 2nd data row
expect(dataRow2).toHaveTextContent('Alert C');
// Verify 3rd data row
expect(dataRow3).toHaveTextContent('Alert A');
});
it('should sort the table by severity when header is clicked', () => {
render(<NoFilterTable allAlerts={allAlerts} selectedFilter={[]} />);
const headers = screen.getAllByRole('columnheader');
const severityHeader = headers.find((header) =>
header.textContent?.includes('Severity'),
);
expect(severityHeader).toBeInTheDocument();
if (severityHeader) {
const initialRows = screen.getAllByRole('row');
expect(initialRows.length).toBe(4);
expect(initialRows[1]).toHaveTextContent('Alert B');
expect(initialRows[2]).toHaveTextContent('Alert C');
expect(initialRows[3]).toHaveTextContent('Alert A');
fireEvent.click(severityHeader);
const sortedRows = screen.getAllByRole('row');
expect(sortedRows.length).toBe(4);
expect(sortedRows[1]).toHaveTextContent('Alert A');
expect(sortedRows[2]).toHaveTextContent('Alert B');
expect(sortedRows[3]).toHaveTextContent('Alert C');
}
});
});

View File

@@ -0,0 +1,53 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { Alerts } from 'types/api/alerts/getTriggered';
export function createAlert(overrides: Partial<Alerts> = {}): Alerts {
return {
labels: undefined,
annotations: {
description: 'Test Description',
summary: 'Test Summary',
},
state: 'firing',
name: 'Test Alert',
id: 1,
endsAt: '2021-01-02T00:00:00Z',
fingerprint: '1234567890',
generatorURL: 'https://test.com',
receivers: [{ name: 'Test Receiver' }],
startsAt: '2021-01-03T00:00:00Z',
status: {
inhibitedBy: [],
silencedBy: [],
state: 'firing',
},
updatedAt: '2021-01-01T00:00:00Z',
...overrides,
};
}
export function useMockTimezone(): {
timezone: Timezone;
browserTimezone: Timezone;
updateTimezone: (timezone: Timezone) => void;
formatTimezoneAdjustedTimestamp: (input: string, format?: string) => string;
isAdaptationEnabled: boolean;
setIsAdaptationEnabled: (enabled: boolean) => void;
} {
const mockTimezone: Timezone = {
name: 'timezone',
value: 'mock-timezone',
offset: '+1.30',
searchIndex: '1',
};
return {
timezone: mockTimezone,
browserTimezone: mockTimezone,
updateTimezone: jest.fn(),
formatTimezoneAdjustedTimestamp: jest
.fn()
.mockImplementation((date: string) => new Date(date).toISOString()),
isAdaptationEnabled: true,
setIsAdaptationEnabled: jest.fn(),
};
}

View File

@@ -0,0 +1,85 @@
import type { Value } from '../Filter';
import { FilterAlerts } from '../utils';
import { createAlert } from './mockUtils';
describe('FilterAlerts', () => {
it('returns all alerts when no filters are selected', () => {
const alerts = [
createAlert({ fingerprint: 'fp-1' }),
createAlert({ fingerprint: 'fp-2' }),
];
const filters: Value[] = [];
const result = FilterAlerts(alerts, filters);
expect(result).toBe(alerts);
});
it('filters alerts that have matching label key and value', () => {
const warningAlert = createAlert({
fingerprint: 'warning',
labels: { severity: 'warning' },
});
const criticalAlert = createAlert({
fingerprint: 'critical',
labels: { severity: 'critical' },
});
const alerts = [warningAlert, criticalAlert];
const filters: Value[] = [{ value: 'severity:critical' }];
const result = FilterAlerts(alerts, filters);
expect(result).toEqual([criticalAlert]);
});
it('includes alerts when any filter matches', () => {
const severityAlert = createAlert({
fingerprint: 'severity',
labels: { severity: 'warning' },
});
const teamAlert = createAlert({
fingerprint: 'team',
labels: { team: 'core-observability' },
});
const otherAlert = createAlert({
fingerprint: 'other',
labels: { service: 'ingestor' },
});
const alerts = [severityAlert, teamAlert, otherAlert];
const filters: Value[] = [
{ value: 'severity:warning' },
{ value: 'team:core-observability' },
];
const result = FilterAlerts(alerts, filters);
expect(result).toHaveLength(2);
expect(result).toEqual([severityAlert, teamAlert]);
});
it('matches labels even when filters contain surrounding whitespace', () => {
const alert = createAlert({
fingerprint: 'trim-test',
labels: { severity: 'critical' },
});
const alerts = [alert];
const filters: Value[] = [{ value: ' severity : critical ' }];
const result = FilterAlerts(alerts, filters);
expect(result).toEqual([alert]);
});
it('ignores filters that do not contain a key/value delimiter', () => {
const alert = createAlert({
fingerprint: 'invalid-filter',
labels: { severity: 'warning' },
});
const alerts = [alert];
const filters: Value[] = [{ value: 'severitywarning' }];
const result = FilterAlerts(alerts, filters);
expect(result).toEqual([]);
});
});

View File

@@ -37,6 +37,9 @@ export const FilterAlerts = (
allAlerts.forEach((alert) => {
const { labels } = alert;
if (!labels) {
return;
}
Object.keys(labels).forEach((e) => {
const selectedKey = objectMap.get(e);

View File

@@ -1,5 +1,5 @@
export interface Alerts {
labels: AlertsLabel;
labels?: AlertsLabel;
annotations: {
description: string;
summary: string;