mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-03 08:33:26 +00:00
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:
committed by
GitHub
parent
99b9e27bca
commit
30b98582b4
@@ -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]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') || '';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface Alerts {
|
||||
labels: AlertsLabel;
|
||||
labels?: AlertsLabel;
|
||||
annotations: {
|
||||
description: string;
|
||||
summary: string;
|
||||
|
||||
Reference in New Issue
Block a user