Compare commits

...

6 Commits

Author SHA1 Message Date
amlannandy
5b17e602e0 chore: additional changes 2025-12-16 17:35:21 +07:00
Srikanth Chekuri
109fcb507f Merge branch 'main' into SIG-3099 2025-11-22 05:07:32 +05:30
amlannandy
0e662586f1 chore: add tests 2025-11-21 11:33:56 +07:00
amlannandy
12d4971aab chore: minor fix 2025-11-17 12:14:35 +07:00
amlannandy
9ed472f525 chore: fix CI 2025-11-17 10:09:25 +07:00
amlannandy
ca4c820fb6 chore: fix undefined labels error in alerts 2025-11-16 23:26:26 +07:00
10 changed files with 391 additions and 16 deletions

View File

@@ -65,11 +65,12 @@ 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">
@@ -50,7 +50,7 @@ function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
<TableCell minWidth="90px" overflowX="scroll">
<div>
{tags.map((e) => (
<Tag key={e}>{`${e}:${labels[e]}`}</Tag>
<Tag key={e}>{`${e}:${labels[e] || '-'}`}</Tag>
))}
</div>
</TableCell>

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],
);
@@ -51,11 +51,14 @@ function FilteredTable({
}
const objects = tagsAlert[0].labels;
if (!objects) return null;
const keysArray = Object.keys(objects);
const valueArray: string[] = [];
keysArray.forEach((e) => {
valueArray.push(objects[e]);
if (objects[e]) {
valueArray.push(objects[e] as string);
}
});
const tags = tagsValue

View File

@@ -25,8 +25,11 @@ function NoFilterTable({
dataIndex: 'status',
width: 80,
key: 'status',
sorter: (a, b): number =>
b.labels.severity.length - a.labels.severity.length,
sorter: (a, b): number => {
const severityLengthOfA = a.labels?.severity?.length || 0;
const severityLengthOfB = b.labels?.severity?.length || 0;
return severityLengthOfA - severityLengthOfB;
},
render: (value): JSX.Element => <AlertStatus severity={value.state} />,
},
{
@@ -48,6 +51,7 @@ function NoFilterTable({
key: 'tags',
width: 100,
render: (labels): JSX.Element => {
if (!labels) return <Typography>-</Typography>;
const objectKeys = Object.keys(labels);
const withOutSeverityKeys = objectKeys.filter((e) => e !== 'severity');
@@ -65,12 +69,14 @@ function NoFilterTable({
dataIndex: 'labels',
key: 'severity',
width: 100,
// eslint-disable-next-line sonarjs/no-identical-functions
sorter: (a, b): number => {
const severityValueA = a.labels.severity;
const severityValueB = b.labels.severity;
return severityValueA.length - severityValueB.length;
const severityLengthOfA = a.labels?.severity?.length || 0;
const severityLengthOfB = b.labels?.severity?.length || 0;
return severityLengthOfA - severityLengthOfB;
},
render: (value): JSX.Element => {
if (!value) return <Typography>-</Typography>;
const objectKeys = Object.keys(value);
const withSeverityKey = objectKeys.find((e) => e === 'severity') || '';
const severityValue = value[withSeverityKey];

View File

@@ -0,0 +1,85 @@
import { render, within } from '@testing-library/react';
import * as timezoneHooks from 'providers/Timezone';
import ExpandableRow from '../FilteredTable/ExapandableRow';
import { createAlert } from './utils';
const mockFormatTimezoneAdjustedTimestamp = jest
.fn()
.mockImplementation((date: Date) => date.toISOString());
const mockTimezone = {
name: 'timezone',
value: 'mock-timezone',
offset: '+1.30',
searchIndex: '1',
};
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
timezone: mockTimezone,
browserTimezone: mockTimezone,
updateTimezone: jest.fn(),
formatTimezoneAdjustedTimestamp: mockFormatTimezoneAdjustedTimestamp,
isAdaptationEnabled: true,
setIsAdaptationEnabled: jest.fn(),
});
const TEST_ALERT_NAME = 'Test Alert';
const allAlerts = [
createAlert({
fingerprint: 'alert-no-labels',
name: TEST_ALERT_NAME,
}),
createAlert({
fingerprint: 'alert-with-labels',
name: 'Test Alert with Labels',
startsAt: '2021-02-03T00:00:00Z',
status: {
inhibitedBy: [],
silencedBy: [],
state: 'active',
},
labels: {
severity: 'warning',
alertname: 'Test Label',
},
}),
];
describe('ExpandableRow', () => {
it('should render the expandable row with both labels and no labels', () => {
const { container } = render(<ExpandableRow allAlerts={allAlerts} />);
const rows = container.querySelectorAll('.ant-card');
expect(rows).toHaveLength(2);
const [rowWithoutLabels, rowWithLabels] = rows;
const rowWithoutLabelsWithin = within(rowWithoutLabels as HTMLElement);
const rowWithLabelsWithin = within(rowWithLabels as HTMLElement);
expect(
rowWithoutLabelsWithin.getByText('Unknown Status'),
).toBeInTheDocument();
expect(
rowWithoutLabelsWithin.getByText('2021-01-03T00:00:00.000Z'),
).toBeInTheDocument();
expect(
rowWithoutLabelsWithin.queryByText(TEST_ALERT_NAME),
).not.toBeInTheDocument();
expect(rowWithoutLabelsWithin.queryByText('warning')).not.toBeInTheDocument();
expect(
rowWithoutLabelsWithin.queryByText(`alertname:${TEST_ALERT_NAME}`),
).not.toBeInTheDocument();
expect(rowWithLabelsWithin.getByText('Firing')).toBeInTheDocument();
expect(rowWithLabelsWithin.getByText('Test Label')).toBeInTheDocument();
expect(rowWithLabelsWithin.getByText('warning')).toBeInTheDocument();
expect(
rowWithLabelsWithin.getByText('2021-02-03T00:00:00.000Z'),
).toBeInTheDocument();
expect(
rowWithLabelsWithin.getByText(`alertname:Test Label`),
).toBeInTheDocument();
expect(mockFormatTimezoneAdjustedTimestamp).toHaveBeenCalledTimes(2);
});
});

View File

@@ -0,0 +1,173 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as timezoneHooks from 'providers/Timezone';
import NoFilterTable from '../NoFilterTable';
import { createAlert } from './utils';
const mockFormatTimezoneAdjustedTimestamp = jest
.fn()
.mockImplementation((date: string) => new Date(date).toISOString());
const mockTimezone = {
name: 'timezone',
value: 'mock-timezone',
offset: '+1.30',
searchIndex: '1',
};
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
timezone: mockTimezone,
browserTimezone: mockTimezone,
updateTimezone: jest.fn(),
formatTimezoneAdjustedTimestamp: mockFormatTimezoneAdjustedTimestamp,
isAdaptationEnabled: true,
setIsAdaptationEnabled: jest.fn(),
});
const TEST_ALERT_1_NAME = 'Test Alert 1';
const TEST_ALERT_2_NAME = 'Test Alert 2';
const COLUMN_ALERT_NAME = 'Alert Name';
const COLUMN_FIRING_SINCE = 'Firing Since';
const allAlerts = [
createAlert({
name: TEST_ALERT_1_NAME,
fingerprint: 'fingerprint-1',
startsAt: '2021-01-01T00:00:00Z',
status: {
state: 'active',
inhibitedBy: [],
silencedBy: [],
},
labels: {
severity: 'warning',
alertname: TEST_ALERT_1_NAME,
},
}),
createAlert({
name: TEST_ALERT_2_NAME,
fingerprint: 'fingerprint-2',
startsAt: '2021-01-02T00:00:00Z',
status: {
state: 'active',
inhibitedBy: [],
silencedBy: [],
},
labels: {
alertname: TEST_ALERT_2_NAME,
},
}),
];
describe('NoFilterTable', () => {
it('should render the no filter table with correct columns', () => {
render(<NoFilterTable allAlerts={allAlerts} selectedFilter={[]} />);
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText(COLUMN_ALERT_NAME)).toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Severity')).toBeInTheDocument();
expect(screen.getByText(COLUMN_FIRING_SINCE)).toBeInTheDocument();
});
it('should render the no filter table with correct rows', () => {
render(<NoFilterTable allAlerts={allAlerts} selectedFilter={[]} />);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(3); // 1 header row + 2 data rows
const [headerRow, dataRow1, dataRow2] = rows;
// Verify header row
expect(headerRow).toHaveTextContent('Status');
expect(headerRow).toHaveTextContent(COLUMN_ALERT_NAME);
expect(headerRow).toHaveTextContent('Tags');
expect(headerRow).toHaveTextContent('Severity');
expect(headerRow).toHaveTextContent(COLUMN_FIRING_SINCE);
// Verify 1st data row
expect(dataRow1).toHaveTextContent(TEST_ALERT_1_NAME);
expect(dataRow1).toHaveTextContent('warning');
// Verify 2nd data row
expect(dataRow2).toHaveTextContent(TEST_ALERT_2_NAME);
expect(dataRow2).toHaveTextContent('-');
});
it('should sort the table by Alert Name when header is clicked', () => {
render(<NoFilterTable allAlerts={allAlerts} selectedFilter={[]} />);
const initialRows = screen.getAllByRole('row');
expect(initialRows[1]).toHaveTextContent(TEST_ALERT_1_NAME);
expect(initialRows[2]).toHaveTextContent(TEST_ALERT_2_NAME);
const headers = screen.getAllByRole('columnheader');
const alertNameHeader = headers.find((header) =>
header.textContent?.includes(COLUMN_ALERT_NAME),
);
expect(alertNameHeader).toBeInTheDocument();
// Click to sort ascending
if (alertNameHeader) {
fireEvent.click(alertNameHeader);
const sortedRowsAsc = screen.getAllByRole('row');
expect(sortedRowsAsc[1]).toHaveTextContent(TEST_ALERT_1_NAME);
expect(sortedRowsAsc[2]).toHaveTextContent(TEST_ALERT_2_NAME);
}
});
it('should sort the table by Severity when header is clicked', () => {
const alertsWithDifferentSeverities = [
createAlert({
name: 'Alert A',
fingerprint: 'fingerprint-a',
startsAt: '2021-01-01T00:00:00Z',
status: {
state: 'active',
inhibitedBy: [],
silencedBy: [],
},
labels: {
severity: 'critical',
alertname: 'Alert A',
},
}),
createAlert({
name: 'Alert B',
fingerprint: 'fingerprint-b',
startsAt: '2021-01-02T00:00:00Z',
status: {
state: 'active',
inhibitedBy: [],
silencedBy: [],
},
labels: {
severity: 'info',
alertname: 'Alert B',
},
}),
];
render(
<NoFilterTable
allAlerts={alertsWithDifferentSeverities}
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[1]).toHaveTextContent('Alert A');
expect(initialRows[2]).toHaveTextContent('Alert B');
fireEvent.click(severityHeader);
const sortedRows = screen.getAllByRole('row');
expect(sortedRows[1]).toHaveTextContent('Alert B');
expect(sortedRows[2]).toHaveTextContent('Alert A');
}
});
});

View File

@@ -0,0 +1,80 @@
import type { Alerts } from 'types/api/alerts/getTriggered';
import type { Value } from '../Filter';
import { FilterAlerts } from '../utils';
const createAlert = (
fingerprint: string,
labels: Alerts['labels'] = {},
): Alerts => ({
annotations: { description: '', summary: '' },
state: 'firing',
name: 'test-alert',
id: 1,
endsAt: '',
fingerprint,
generatorURL: '',
receivers: [],
startsAt: '',
status: { inhibitedBy: [], silencedBy: [], state: 'firing' },
updatedAt: '',
labels,
});
describe('FilterAlerts', () => {
it('returns all alerts when no filters are selected', () => {
const alerts = [createAlert('fp-1'), createAlert('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('warning', { severity: 'warning' });
const criticalAlert = createAlert('critical', { 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('severity', { severity: 'warning' });
const teamAlert = createAlert('team', { team: 'core-observability' });
const otherAlert = createAlert('other', { 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('trim-test', { 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('invalid-filter', { severity: 'warning' });
const alerts = [alert];
const filters: Value[] = [{ value: 'severitywarning' }];
const result = FilterAlerts(alerts, filters);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,26 @@
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,
};
}

View File

@@ -37,6 +37,7 @@ 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;
@@ -26,7 +26,7 @@ interface Receivers {
}
interface AlertsLabel {
[key: string]: string;
[key: string]: string | undefined;
}
export interface Props {