mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-28 13:34:18 +00:00
Compare commits
6 Commits
optimizati
...
SIG-3099
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b17e602e0 | ||
|
|
109fcb507f | ||
|
|
0e662586f1 | ||
|
|
12d4971aab | ||
|
|
9ed472f525 | ||
|
|
ca4c820fb6 |
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
26
frontend/src/container/TriggeredAlerts/__tests__/utils.ts
Normal file
26
frontend/src/container/TriggeredAlerts/__tests__/utils.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user