Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
906cb15010 fix(query-context): ensure operator is correctly suggested 2026-06-03 15:40:37 -03:00
61 changed files with 424 additions and 3829 deletions

View File

@@ -1261,7 +1261,6 @@ components:
- sqs
- storageaccountsblob
- cdnprofile
- containerapp
type: string
CloudintegrationtypesServiceMetadata:
properties:

View File

@@ -0,0 +1,28 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@@ -1,8 +1,6 @@
import type { Config } from '@jest/types';
const USE_SAFE_NAVIGATE_MOCK_PATH =
'<rootDir>/src/__tests__/safeNavigateMock.ts';
const LOG_EVENT_MOCK_PATH = '<rootDir>/src/__tests__/logEventMock.ts';
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
const config: Config.InitialOptions = {
silent: true,
@@ -24,8 +22,6 @@ const config: Config.InitialOptions = {
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^src/api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',

View File

@@ -1,11 +0,0 @@
// Shared mock for `api/common/logEvent`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `api/common/logEvent` in test code resolves to this file.
// Tests can import `logEventMock` to assert analytics calls — Jest's
// `clearMocks: true` resets call history between tests.
export const logEventMock: jest.MockedFunction<
(eventName: string, attributes?: Record<string, unknown>) => void
> = jest.fn();
export default logEventMock;

View File

@@ -1,29 +0,0 @@
// Shared mock for `hooks/useSafeNavigate`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `hooks/useSafeNavigate` in test code resolves to this file.
// Tests can import `safeNavigateMock` to assert navigation calls — Jest's
// `clearMocks: true` resets call history between tests.
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
export const safeNavigateMock: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
> = jest.fn();
export const useSafeNavigate = (): {
safeNavigate: typeof safeNavigateMock;
} => ({
safeNavigate: safeNavigateMock,
});

View File

@@ -2574,7 +2574,6 @@ export enum CloudintegrationtypesServiceIDDTO {
sqs = 'sqs',
storageaccountsblob = 'storageaccountsblob',
cdnprofile = 'cdnprofile',
containerapp = 'containerapp',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**

View File

@@ -3,12 +3,17 @@ import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -30,6 +35,7 @@ jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
const mockLogEvent = logEvent as jest.MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
@@ -44,7 +50,6 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
jest.clearAllMocks();
logEventMock.mockReturnValue(Promise.resolve() as never);
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,
@@ -111,7 +116,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'feedback',
page: mockLocation.pathname,
@@ -144,7 +149,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'reportBug',
page: mockLocation.pathname,
@@ -177,7 +182,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'featureRequest',
page: mockLocation.pathname,

View File

@@ -2,11 +2,16 @@
import { useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -45,6 +50,7 @@ jest.mock('hooks/useIsAIAssistantEnabled', () => ({
useIsAIAssistantEnabled: (): boolean => false,
}));
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
@@ -114,7 +120,7 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(logEventMock).toHaveBeenCalledWith('Feedback: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
@@ -127,7 +133,7 @@ describe('HeaderRightSection', () => {
const shareButton = screen.getByRole('button', { name: /share/i });
await user.click(shareButton);
expect(logEventMock).toHaveBeenCalledWith('Share: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Share: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('share-modal')).toBeInTheDocument();
@@ -144,7 +150,7 @@ describe('HeaderRightSection', () => {
await user.click(announcementsButton!);
expect(logEventMock).toHaveBeenCalledWith('Announcements: Clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Announcements: Clicked', {
page: mockLocation.pathname,
});
});

View File

@@ -5,13 +5,18 @@ import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -48,6 +53,7 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseUrlQuery = useUrlQuery as jest.Mock;
const mockUseSelector = useSelector as jest.Mock;
@@ -119,7 +125,7 @@ describe('ShareURLModal', () => {
await user.click(copyButton);
expect(mockHandleCopyToClipboard).toHaveBeenCalled();
expect(logEventMock).toHaveBeenCalledWith('Share: Copy link clicked', {
expect(mockLogEvent).toHaveBeenCalledWith('Share: Copy link clicked', {
page: TEST_PATH,
URL: expect.any(String),
});

View File

@@ -139,6 +139,7 @@ jest.mock('react-query', (): unknown => {
});
// mock other side-effecty modules
jest.mock('api/common/logEvent', () => jest.fn());
jest.mock('api/browser/localstorage/set', () => jest.fn());
jest.mock('utils/error', () => ({ showErrorNotification: jest.fn() }));

View File

@@ -1,7 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
@@ -24,6 +24,8 @@ jest.mock('providers/cmdKProvider', () => ({
}),
}));
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
@@ -137,7 +139,7 @@ describe('Sidebar Toggle Shortcut', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEventMock('Global Shortcut: Sidebar Toggle', {
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
@@ -153,13 +155,10 @@ describe('Sidebar Toggle Shortcut', () => {
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEventMock).toHaveBeenCalledWith(
'Global Shortcut: Sidebar Toggle',
{
previousState: false,
newState: true,
},
);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
it('should update user preference in context', async () => {

View File

@@ -1,10 +1,16 @@
import { logEventMock } from '__tests__/logEventMock';
import { Events } from 'constants/events';
import { DEFAULT_PIN_TOOLTIP_KEY } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { render, screen, userEvent } from 'tests/test-utils';
import TooltipFooter from '../TooltipFooter';
const mockLogEvent = jest.fn();
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: (...args: unknown[]): unknown => mockLogEvent(...args),
}));
describe('TooltipFooter', () => {
const defaultProps = {
id: 'panel-123',
@@ -78,7 +84,7 @@ describe('TooltipFooter', () => {
await user.click(screen.getByTestId('uplot-tooltip-unpin'));
expect(logEventMock).toHaveBeenCalledWith(Events.TOOLTIP_UNPINNED, {
expect(mockLogEvent).toHaveBeenCalledWith(Events.TOOLTIP_UNPINNED, {
id: 'panel-123',
});
expect(dismiss).toHaveBeenCalledTimes(1);

View File

@@ -1,6 +1,7 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
@@ -66,6 +67,20 @@ function GridCardGraph({
const [errorMessage, setErrorMessage] = useState<string>();
const [isInternalServerError, setIsInternalServerError] =
useState<boolean>(false);
const queryRangeCalledRef = useRef(false);
useEffect(() => {
const timeoutId = setTimeout(() => {
if (!queryRangeCalledRef.current) {
Sentry.captureEvent({
message: `Dashboard query range not called within expected timeframe for widget ${widget?.id}`,
level: 'warning',
});
}
}, 120000);
return (): void => clearTimeout(timeoutId);
}, [widget?.id]);
const {
minTime,
maxTime,
@@ -256,12 +271,14 @@ function GridCardGraph({
});
}
}
queryRangeCalledRef.current = true;
},
onSettled: (data) => {
dataAvailable?.(
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
);
getGraphData?.(data?.payload?.data);
queryRangeCalledRef.current = true;
},
},
);

View File

@@ -89,7 +89,7 @@ export function AlertsEmptyState({
onClick={onClickNewAlertHandler}
disabled={!addNewAlert}
loading={loading}
testId="add-alert"
data-testid="add-alert"
>
<span className={styles.buttonContent}>
<Plus size="md" />
@@ -97,12 +97,7 @@ export function AlertsEmptyState({
</span>
</Button>
{onRefresh && (
<Button
onClick={onRefresh}
prefix={<RefreshCw />}
color="secondary"
testId="list-alerts-empty-refresh-button"
>
<Button onClick={onRefresh} prefix={<RefreshCw />} color="secondary">
Refresh
</Button>
)}

View File

@@ -1,215 +0,0 @@
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen, waitFor } from 'tests/test-utils';
import { findAlertRow, renderListAlertRules } from './_helpers';
async function openActionsMenu(row: HTMLElement): Promise<void> {
const trigger = row.querySelector(
'[data-testid="alert-actions"]',
) as HTMLElement | null;
expect(trigger).not.toBeNull();
const user = userEvent.setup({ delay: null });
await user.click(trigger as HTMLElement);
// Radix renders the menu items in a portal once the trigger is activated.
await screen.findByRole('menu');
}
async function clickMenuItem(label: string): Promise<void> {
const user = userEvent.setup({ delay: null });
const item = await screen.findByRole('menuitem', { name: label });
await user.click(item);
}
describe('ListAlertRules — actions menu', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('renders Enable/Disable/Edit/Edit in New Tab/Clone/Delete items after opening the menu', async () => {
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
await openActionsMenu(row);
const items = screen.getAllByRole('menuitem');
const labels = items.map((it) => it.textContent);
expect(labels).toStrictEqual(
expect.arrayContaining([
'Edit',
'Edit in New Tab',
'Clone',
'Delete',
'Disable',
]),
);
});
it('disabled rule (rule-4) shows "Enable" instead of "Disable"', async () => {
renderListAlertRules();
const row = await findAlertRow('Disabled Alert');
await openActionsMenu(row);
const items = screen.getAllByRole('menuitem');
const labels = items.map((it) => it.textContent);
expect(labels).toContain('Enable');
expect(labels).not.toContain('Disable');
});
it('toggle action: clicking Disable sends PATCH with disabled:true', async () => {
let capturedBody: unknown = null;
let capturedPath: string | null = null;
server.use(
rest.patch('http://localhost/api/v2/rules/:id', async (req, res, ctx) => {
capturedBody = await req.json();
capturedPath = req.params.id as string;
return res(ctx.status(200), ctx.json({ status: 'success' }));
}),
);
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Disable');
await waitFor(() => {
expect(capturedBody).toStrictEqual(
expect.objectContaining({ disabled: true }),
);
});
expect(capturedPath).toBe('rule-1');
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Enable/Disable', ruleId: 'rule-1' }),
);
});
it('edit action: clicking Edit navigates via safeNavigate and logs event', async () => {
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Edit');
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
expect(safeNavigateMock.mock.calls[0][0]).toContain('ruleId=rule-1');
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Edit', ruleId: 'rule-1' }),
);
});
it('edit in new tab action: clicking opens with newTab:true', async () => {
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Edit in New Tab');
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
const [url, options] = safeNavigateMock.mock.calls[0];
expect(url).toContain('ruleId=rule-1');
expect(options).toStrictEqual(expect.objectContaining({ newTab: true }));
});
it('clone action: sends POST with " - Copy" suffix and opens the cloned rule returned by the API', async () => {
let capturedPostBody: unknown = null;
server.use(
rest.post('http://localhost/api/v2/rules', async (req, res, ctx) => {
capturedPostBody = await req.json();
return res(
ctx.status(201),
ctx.json({
data: {
...(capturedPostBody as Record<string, unknown>),
id: 'cloned-from-server',
},
status: 'success',
}),
);
}),
);
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Clone');
await waitFor(() => {
expect(capturedPostBody).toStrictEqual(
expect.objectContaining({ alert: 'High CPU Alert - Copy' }),
);
});
// The id from the server response round-trips into the navigate URL — this
// protects against a regression where the code hardcodes the id.
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
expect(safeNavigateMock.mock.calls[0][0]).toContain(
'ruleId=cloned-from-server',
);
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Clone', ruleId: 'rule-1' }),
);
});
it('delete action: sends DELETE for the rule id', async () => {
let deletedId: string | null = null;
server.use(
rest.delete('http://localhost/api/v2/rules/:id', (req, res, ctx) => {
deletedId = req.params.id as string;
return res(ctx.status(200), ctx.json({ status: 'success' }));
}),
);
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Delete');
await waitFor(() => {
expect(deletedId).toBe('rule-1');
});
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Delete', ruleId: 'rule-1' }),
);
});
it('error path: PATCH is still attempted when server returns 500', async () => {
let patchAttempted = false;
server.use(
rest.patch('http://localhost/api/v2/rules/:id', (_, res, ctx) => {
patchAttempted = true;
return res(ctx.status(500), ctx.json({ status: 'error' }));
}),
);
renderListAlertRules();
const row = await findAlertRow('High CPU Alert');
await openActionsMenu(row);
await clickMenuItem('Disable');
await waitFor(() => {
expect(patchAttempted).toBe(true);
});
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Enable/Disable', ruleId: 'rule-1' }),
);
});
});

View File

@@ -1,79 +0,0 @@
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { renderListAlertRules } from './_helpers';
const COLUMN_STORAGE_KEY = '@signoz/table-columns/alert-rules-columns';
describe('ListAlertRules — columns selector', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
});
it('opens columns popover and lists toggleable columns', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.click(screen.getByTestId('alert-columns-button'));
// Popover should reveal "Toggle Columns" heading + per-column labels.
await screen.findByText('Toggle Columns');
expect(screen.getByText('Created At')).toBeInTheDocument();
expect(screen.getByText('Created By')).toBeInTheDocument();
expect(screen.getByText('Updated At')).toBeInTheDocument();
expect(screen.getByText('Updated By')).toBeInTheDocument();
});
it('default-hidden columns (Created At/By, Updated At/By) are not in the table header', async () => {
renderListAlertRules();
await screen.findByText('High CPU Alert');
const headers = document.querySelectorAll('th');
const headerTexts = Array.from(headers).map((h) => h.textContent || '');
expect(headerTexts.some((t) => t.includes('Created At'))).toBe(false);
expect(headerTexts.some((t) => t.includes('Created By'))).toBe(false);
expect(headerTexts.some((t) => t.includes('Updated At'))).toBe(false);
expect(headerTexts.some((t) => t.includes('Updated By'))).toBe(false);
});
it('toggling Created At on writes to localStorage and adds the header', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
const headersBefore = Array.from(document.querySelectorAll('th')).map(
(h) => h.textContent ?? '',
);
expect(headersBefore.some((t) => t.includes('Created At'))).toBe(false);
await user.click(screen.getByTestId('alert-columns-button'));
await screen.findByText('Toggle Columns');
const checkbox = document.getElementById('col-createdAt');
expect(checkbox).not.toBeNull();
await user.click(checkbox as HTMLElement);
await waitFor(() => {
const stored = window.localStorage.getItem(COLUMN_STORAGE_KEY);
expect(stored).not.toBeNull();
const parsed = JSON.parse(stored as string);
expect(parsed.hiddenColumnIds).not.toContain('createdAt');
});
await waitFor(() => {
const headersAfter = Array.from(document.querySelectorAll('th')).map(
(h) => h.textContent ?? '',
);
expect(headersAfter.some((t) => t.includes('Created At'))).toBe(true);
});
});
});

View File

@@ -1,91 +0,0 @@
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { alertRulesFixture } from 'mocks-server/__mockdata__/alert_rules';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen } from 'tests/test-utils';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — empty states', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('renders AlertsEmptyState when API returns no rules', async () => {
const user = userEvent.setup({ delay: null });
server.use(
rest.get('http://localhost/api/v2/rules', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [], status: 'success' })),
),
);
renderListAlertRules();
await screen.findByText('No Alert rules yet.');
expect(
screen.getByText('Create an Alert Rule to get started'),
).toBeInTheDocument();
// New Alert Rule button is visible and triggers safeNavigate to ALERTS_NEW.
await user.click(screen.getByTestId('add-alert'));
expect(safeNavigateMock).toHaveBeenCalledWith(
ROUTES.ALERTS_NEW,
expect.objectContaining({ newTab: false }),
);
});
it('renders ErrorEmptyState when API returns 500; refresh triggers a refetch', async () => {
const user = userEvent.setup({ delay: null });
let callCount = 0;
server.use(
rest.get('http://localhost/api/v2/rules', (_, res, ctx) => {
callCount += 1;
if (callCount === 1) {
return res(ctx.status(500), ctx.json({ status: 'error' }));
}
return res(
ctx.status(200),
ctx.json({ data: alertRulesFixture, status: 'success' }),
);
}),
);
renderListAlertRules();
await screen.findByTestId('error-empty-state');
await user.click(screen.getByTestId('error-refresh-button'));
const rule = await screen.findByText('High CPU Alert');
expect(rule).toBeInTheDocument();
});
it('renders NoResultsEmptyState when search yields no match; Clear Search resets', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
const searchInput = screen.getByTestId('list-alerts-search-input');
await user.clear(searchInput);
await user.type(searchInput, 'totally-not-found');
await screen.findByTestId('no-results-empty-state');
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'No matching alert rules',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'No alert rules match your search. Try adjusting your search criteria.',
);
await user.click(screen.getByTestId('no-results-clear-button'));
const rule = await screen.findByText('High CPU Alert');
expect(rule).toBeInTheDocument();
});
});

View File

@@ -1,123 +0,0 @@
import { screen, waitFor } from 'tests/test-utils';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — list rendering', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('renders alert rules from API', async () => {
renderListAlertRules();
await expect(
screen.findByTestId('alert-row-rule-1-name'),
).resolves.toHaveTextContent('High CPU Alert');
expect(screen.getByTestId('alert-row-rule-2-name')).toHaveTextContent(
'Memory Pending Alert',
);
expect(screen.getByTestId('alert-row-rule-3-name')).toHaveTextContent(
'Healthy Alert',
);
expect(screen.getByTestId('alert-row-rule-4-name')).toHaveTextContent(
'Disabled Alert',
);
});
it('renders state badges via STATE_CONFIG mapping', async () => {
renderListAlertRules();
await waitFor(() =>
expect(screen.getByTestId('alert-row-rule-1-state')).toBeInTheDocument(),
);
expect(screen.getByTestId('alert-row-rule-1-state')).toHaveTextContent(
'Firing',
);
expect(screen.getByTestId('alert-row-rule-2-state')).toHaveTextContent(
'Pending',
);
expect(screen.getByTestId('alert-row-rule-3-state')).toHaveTextContent('OK');
expect(screen.getByTestId('alert-row-rule-4-state')).toHaveTextContent(
'Disabled',
);
expect(screen.getByTestId('alert-row-rule-5-state')).toHaveTextContent('OK');
});
it('renders state badges with semantic colors', async () => {
renderListAlertRules();
await waitFor(() =>
expect(screen.getByTestId('alert-row-rule-1-state')).toBeInTheDocument(),
);
expect(screen.getByTestId('alert-row-rule-1-state')).toHaveAttribute(
'data-color',
'cherry',
);
expect(screen.getByTestId('alert-row-rule-2-state')).toHaveAttribute(
'data-color',
'amber',
);
expect(screen.getByTestId('alert-row-rule-3-state')).toHaveAttribute(
'data-color',
'forest',
);
expect(screen.getByTestId('alert-row-rule-4-state')).toHaveAttribute(
'data-color',
'vanilla',
);
});
it('renders severity badges for rules with severity', async () => {
renderListAlertRules();
await waitFor(() =>
expect(screen.getByTestId('alert-row-rule-1-severity')).toBeInTheDocument(),
);
expect(screen.getByTestId('alert-row-rule-1-severity')).toHaveTextContent(
'critical',
);
expect(screen.getByTestId('alert-row-rule-2-severity')).toHaveTextContent(
'warning',
);
expect(screen.getByTestId('alert-row-rule-3-severity')).toHaveTextContent(
'info',
);
expect(screen.getByTestId('alert-row-rule-4-severity')).toHaveTextContent(
'critical',
);
expect(screen.getByTestId('alert-row-rule-5-severity')).toHaveTextContent(
'-',
);
expect(screen.getByTestId('alert-row-rule-1-severity')).toHaveAttribute(
'data-color',
'cherry',
);
expect(screen.getByTestId('alert-row-rule-2-severity')).toHaveAttribute(
'data-color',
'amber',
);
});
it('renders header controls (search, columns, new alert)', async () => {
renderListAlertRules();
await waitFor(() =>
expect(screen.getByTestId('alert-row-rule-1-name')).toBeInTheDocument(),
);
expect(screen.getByTestId('list-alerts-search-input')).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Search by Alert Name, Severity and Labels'),
).toBeInTheDocument();
expect(screen.getByTestId('alert-columns-button')).toBeInTheDocument();
expect(
screen.getByTestId('list-alerts-new-alert-button'),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /new alert/i }),
).toBeInTheDocument();
});
});

View File

@@ -1,65 +0,0 @@
import { logEventMock } from '__tests__/logEventMock';
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { screen, waitFor } from 'tests/test-utils';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — new alert button', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('plain click navigates to ALERTS_NEW with newTab:false', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.click(screen.getByRole('button', { name: /new alert/i }));
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
expect(safeNavigateMock).toHaveBeenCalledWith(
ROUTES.ALERTS_NEW,
expect.objectContaining({ newTab: false }),
);
});
it('logs Alert: New alert button clicked', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.click(screen.getByRole('button', { name: /new alert/i }));
await waitFor(() => {
expect(logEventMock).toHaveBeenCalledWith(
'Alert: New alert button clicked',
expect.objectContaining({ layout: 'new' }),
);
});
});
it('ctrl+click on New Alert opens in a new tab (newTab:true)', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.keyboard('{Control>}');
await user.click(screen.getByRole('button', { name: /new alert/i }));
await user.keyboard('{/Control}');
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
expect(safeNavigateMock).toHaveBeenCalledWith(
ROUTES.ALERTS_NEW,
expect.objectContaining({ newTab: true }),
);
});
});

View File

@@ -1,64 +0,0 @@
import userEvent from '@testing-library/user-event';
import { alertRulesPaginationFixture } from 'mocks-server/__mockdata__/alert_rules';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen, waitFor } from 'tests/test-utils';
import { getCurrentNuqsQueryString } from 'tests/nuqs-helpers';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — pagination', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
server.use(
rest.get('http://localhost/api/v2/rules', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ data: alertRulesPaginationFixture, status: 'success' }),
),
),
);
});
it('shows first 10 rows on page 1 (default limit)', async () => {
renderListAlertRules();
await screen.findByText('Pag Rule 0');
for (let i = 0; i < 10; i += 1) {
expect(screen.getByText(`Pag Rule ${i}`)).toBeInTheDocument();
}
expect(screen.queryByText('Pag Rule 10')).not.toBeInTheDocument();
expect(screen.queryByText('Pag Rule 14')).not.toBeInTheDocument();
});
it('shows total count when showTotalCount is enabled', async () => {
renderListAlertRules();
await screen.findByText('Pag Rule 0');
const totalCount = await screen.findByTestId('pagination-total-count');
expect(totalCount.textContent).toContain('Showing');
expect(totalCount.textContent).toContain('of 15');
});
it('navigates to page 2 and shows remaining rows', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('Pag Rule 0');
const nextBtn = screen.getByLabelText('Go to next page');
await user.click(nextBtn);
await waitFor(() => {
expect(screen.getByText('Pag Rule 10')).toBeInTheDocument();
expect(screen.getByText('Pag Rule 14')).toBeInTheDocument();
expect(screen.queryByText('Pag Rule 0')).not.toBeInTheDocument();
});
await waitFor(() => {
expect(getCurrentNuqsQueryString()).toContain('page=2');
});
});
});

View File

@@ -1,71 +0,0 @@
import { screen, waitFor } from 'tests/test-utils';
import { USER_ROLES } from 'types/roles';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — permissions', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('VIEWER role hides "New Alert" button and "Actions" column', async () => {
renderListAlertRules({ role: USER_ROLES.VIEWER });
await screen.findByText('High CPU Alert');
expect(
screen.queryByTestId('list-alerts-new-alert-button'),
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /new alert/i }),
).not.toBeInTheDocument();
const headers = Array.from(document.querySelectorAll('th')).map(
(h) => h.textContent ?? '',
);
expect(headers.some((t) => t.includes('Actions'))).toBe(false);
expect(screen.queryByTestId('alert-actions')).not.toBeInTheDocument();
});
it('ADMIN role shows "New Alert" button and "Actions" column', async () => {
renderListAlertRules({ role: USER_ROLES.ADMIN });
await screen.findByText('High CPU Alert');
expect(
screen.getByTestId('list-alerts-new-alert-button'),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /new alert/i }),
).toBeInTheDocument();
await waitFor(() => {
const headers = Array.from(document.querySelectorAll('th')).map(
(h) => h.textContent ?? '',
);
expect(headers.some((t) => t.includes('Actions'))).toBe(true);
});
expect(screen.getAllByTestId('alert-actions').length).toBeGreaterThan(0);
});
it('EDITOR role behaves like ADMIN (New Alert + Actions visible)', async () => {
renderListAlertRules({ role: USER_ROLES.EDITOR });
await screen.findByText('High CPU Alert');
expect(
screen.getByTestId('list-alerts-new-alert-button'),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /new alert/i }),
).toBeInTheDocument();
await waitFor(() => {
const headers = Array.from(document.querySelectorAll('th')).map(
(h) => h.textContent ?? '',
);
expect(headers.some((t) => t.includes('Actions'))).toBe(true);
});
expect(screen.getAllByTestId('alert-actions').length).toBeGreaterThan(0);
});
});

View File

@@ -1,52 +0,0 @@
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { renderListAlertRules } from './_helpers';
describe('ListAlertRules — row click navigation', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('clicking a row calls safeNavigate to alerts/overview with composite query + ruleId', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
const ruleCell = await screen.findByText('High CPU Alert');
const td = ruleCell.closest('td');
expect(td).not.toBeNull();
await user.click(td as HTMLElement);
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
const [url] = safeNavigateMock.mock.calls[0];
expect(url).toContain('/alerts/overview?');
expect(url).toContain('ruleId=rule-1');
expect(url).toContain('panelTypes=graph');
expect(url).toContain('compositeQuery=');
});
it('ctrl+click on a row navigates with newTab option', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
const ruleCell = await screen.findByText('High CPU Alert');
const td = ruleCell.closest('td');
await user.keyboard('{Control>}');
await user.click(td as HTMLElement);
await user.keyboard('{/Control}');
await waitFor(() => {
expect(safeNavigateMock).toHaveBeenCalled();
});
const [url, options] = safeNavigateMock.mock.calls[0];
expect(url).toContain('ruleId=rule-1');
expect(options).toStrictEqual(expect.objectContaining({ newTab: true }));
});
});

View File

@@ -1,99 +0,0 @@
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { getCurrentNuqsQueryString } from 'tests/nuqs-helpers';
import { renderListAlertRules } from './_helpers';
function getSearchInput(): HTMLInputElement {
return screen.getByTestId('list-alerts-search-input') as HTMLInputElement;
}
describe('ListAlertRules — search', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('filters rows by alert name with debounce', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.clear(getSearchInput());
await user.type(getSearchInput(), 'CPU');
await waitFor(() => {
expect(screen.getByText('High CPU Alert')).toBeInTheDocument();
expect(screen.queryByText('Memory Pending Alert')).not.toBeInTheDocument();
});
});
it('filters rows by label values (severity)', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.clear(getSearchInput());
await user.type(getSearchInput(), 'warning');
await waitFor(() => {
expect(screen.getByText('Memory Pending Alert')).toBeInTheDocument();
expect(screen.queryByText('High CPU Alert')).not.toBeInTheDocument();
});
});
it('restores all rows when search is cleared', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.clear(getSearchInput());
await user.type(getSearchInput(), 'CPU');
await waitFor(() => {
expect(screen.queryByText('Memory Pending Alert')).not.toBeInTheDocument();
});
await user.clear(getSearchInput());
await waitFor(() => {
expect(screen.getByText('High CPU Alert')).toBeInTheDocument();
expect(screen.getByText('Memory Pending Alert')).toBeInTheDocument();
expect(screen.getByText('Healthy Alert')).toBeInTheDocument();
});
});
it('shows no-results state when no match', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules();
await screen.findByText('High CPU Alert');
await user.clear(getSearchInput());
await user.type(getSearchInput(), 'zzzzzz-no-match');
await waitFor(() => {
expect(screen.getByTestId('no-results-empty-state')).toBeInTheDocument();
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'No matching alert rules',
);
});
});
it('resets page to 1 when search debounce fires', async () => {
const user = userEvent.setup({ delay: null });
renderListAlertRules({ initialRoute: '/?page=2' });
// Page 2 of the 4-rule fixture has no rows; we only need the search input
// to be mounted, which happens before data is fetched.
const input = await screen.findByTestId('list-alerts-search-input');
await user.clear(input);
await user.type(input, 'CPU');
await waitFor(() => {
expect(getCurrentNuqsQueryString()).not.toContain('page=2');
});
});
});

View File

@@ -1,232 +0,0 @@
import { logEventMock } from '__tests__/logEventMock';
import { RuletypesAlertStateDTO } from 'api/generated/services/sigNoz.schemas';
import type { SortState } from 'components/TanStackTableView/types';
import type { AlertRule } from '../types';
import {
ALERT_ACTIONS,
alertActionLogEvent,
filterRulesByFilters,
getAlertSortValue,
sortRules,
} from '../utils';
const baseRule = {
id: 'r1',
alert: 'Rule 1',
alertType: 'METRIC_BASED_ALERT',
state: 'inactive',
labels: { severity: 'info' },
condition: {},
createdAt: '2023-10-15T10:00:00Z',
updatedAt: '2023-10-19T10:00:00Z',
} as unknown as AlertRule;
const makeRule = (overrides: Partial<AlertRule>): AlertRule => ({
...baseRule,
...overrides,
});
describe('getAlertSortValue', () => {
it('returns state for "state"', () => {
expect(
getAlertSortValue(
makeRule({ state: RuletypesAlertStateDTO.firing }),
'state',
),
).toBe('firing');
});
it('returns alert name for "name"', () => {
expect(getAlertSortValue(makeRule({ alert: 'My Rule' }), 'name')).toBe(
'My Rule',
);
});
it('returns severity label for "severity"', () => {
expect(
getAlertSortValue(
makeRule({ labels: { severity: 'critical' } }),
'severity',
),
).toBe('critical');
});
it('returns createdAt as ms', () => {
const rule = makeRule({ createdAt: '2023-10-15T10:00:00Z' });
const result = getAlertSortValue(rule, 'createdAt');
expect(result).toBe(new Date('2023-10-15T10:00:00Z').getTime());
});
it('returns updatedAt as ms', () => {
const rule = makeRule({ updatedAt: '2023-10-19T10:00:00Z' });
const result = getAlertSortValue(rule, 'updatedAt');
expect(result).toBe(new Date('2023-10-19T10:00:00Z').getTime());
});
it('returns 0 when createdAt missing', () => {
expect(
getAlertSortValue(makeRule({ createdAt: undefined }), 'createdAt'),
).toBe(0);
});
it('returns empty for unknown column', () => {
expect(getAlertSortValue(baseRule, 'xxx')).toBe('');
});
it('returns empty for missing fields', () => {
expect(
getAlertSortValue(
makeRule({ state: undefined, labels: undefined }),
'state',
),
).toBe('');
expect(
getAlertSortValue(
makeRule({ state: undefined, labels: undefined }),
'severity',
),
).toBe('');
});
});
describe('sortRules', () => {
const r1 = makeRule({ id: '1', alert: 'A' });
const r2 = makeRule({ id: '2', alert: 'B' });
const r3 = makeRule({ id: '3', alert: 'C' });
it('sorts ascending by name', () => {
const order: SortState = { columnName: 'name', order: 'asc' };
const result = sortRules([r3, r1, r2], order);
expect(result.map((r) => r.alert)).toStrictEqual(['A', 'B', 'C']);
});
it('sorts descending by name', () => {
const order: SortState = { columnName: 'name', order: 'desc' };
const result = sortRules([r1, r2, r3], order);
expect(result.map((r) => r.alert)).toStrictEqual(['C', 'B', 'A']);
});
it('returns unsorted when orderBy is null', () => {
const result = sortRules([r3, r1, r2], null);
expect(result.map((r) => r.alert)).toStrictEqual(['C', 'A', 'B']);
});
});
describe('filterRulesByFilters', () => {
const r1 = makeRule({
id: '1',
alert: 'R1',
state: RuletypesAlertStateDTO.firing,
labels: { severity: 'critical' },
});
const r2 = makeRule({
id: '2',
alert: 'R2',
state: RuletypesAlertStateDTO.inactive,
labels: { severity: 'warning' },
});
const r3 = makeRule({
id: '3',
alert: 'R3',
state: RuletypesAlertStateDTO.firing,
labels: { severity: 'warning' },
});
const rules = [r1, r2, r3];
it('returns input when filters empty', () => {
expect(filterRulesByFilters(rules, [])).toStrictEqual(rules);
});
it('filters by state', () => {
const result = filterRulesByFilters(rules, ['state:firing']);
expect(result.map((r) => r.id)).toStrictEqual(['1', '3']);
});
it('filters by severity', () => {
const result = filterRulesByFilters(rules, ['severity:warning']);
expect(result.map((r) => r.id)).toStrictEqual(['2', '3']);
});
it('combines state AND severity', () => {
const result = filterRulesByFilters(rules, [
'state:firing',
'severity:warning',
]);
expect(result.map((r) => r.id)).toStrictEqual(['3']);
});
it('OR within same key (state)', () => {
const result = filterRulesByFilters(rules, [
'state:firing',
'state:inactive',
]);
expect(result.map((r) => r.id)).toStrictEqual(['1', '2', '3']);
});
it('matches values case-insensitively', () => {
const result = filterRulesByFilters(rules, ['state:FIRING']);
expect(result.map((r) => r.id)).toStrictEqual(['1', '3']);
});
it('ignores prefixes with wrong case (state: is required lowercase)', () => {
const result = filterRulesByFilters(rules, ['STATE:FIRING']);
expect(result).toStrictEqual(rules);
});
it('returns empty when no rule matches', () => {
expect(filterRulesByFilters(rules, ['state:nonexistent'])).toStrictEqual([]);
});
it('ignores unknown prefix', () => {
expect(filterRulesByFilters(rules, ['foo:bar'])).toStrictEqual(rules);
});
});
describe('alertActionLogEvent', () => {
it('logs with mapped action label', () => {
const rule = makeRule({
id: 'rule-1',
alert: 'My Rule',
alertType: 'METRIC_BASED_ALERT' as AlertRule['alertType'],
});
alertActionLogEvent(ALERT_ACTIONS.EDIT, rule);
expect(logEventMock).toHaveBeenCalledWith('Alert: Action', {
ruleId: 'rule-1',
dataSource: expect.any(String),
name: 'My Rule',
action: 'Edit',
});
});
it('falls back to raw action when unmapped', () => {
alertActionLogEvent('custom', baseRule);
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'custom' }),
);
});
it('maps TOGGLE action', () => {
alertActionLogEvent(ALERT_ACTIONS.TOGGLE, baseRule);
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Action',
expect.objectContaining({ action: 'Enable/Disable' }),
);
});
it('maps DELETE and CLONE', () => {
alertActionLogEvent(ALERT_ACTIONS.DELETE, baseRule);
alertActionLogEvent(ALERT_ACTIONS.CLONE, baseRule);
expect(logEventMock).toHaveBeenNthCalledWith(
1,
'Alert: Action',
expect.objectContaining({ action: 'Delete' }),
);
expect(logEventMock).toHaveBeenNthCalledWith(
2,
'Alert: Action',
expect.objectContaining({ action: 'Clone' }),
);
});
});

View File

@@ -1,65 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import { render, RenderResult, screen } from '@testing-library/react';
import ListAlertRules from 'container/ListAlertRules';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { AppContext } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import { onNuqsUrlUpdate, resetNuqsState } from 'tests/nuqs-helpers';
import { getAppContextMock } from 'tests/test-utils';
interface RenderOptions {
role?: string;
initialRoute?: string;
}
export function renderListAlertRules(
options: RenderOptions = {},
): RenderResult {
const { role = 'ADMIN', initialRoute = '/' } = options;
const initialSearch = initialRoute.includes('?')
? initialRoute.slice(initialRoute.indexOf('?'))
: '';
resetNuqsState(initialSearch);
const queryClient = new QueryClient({
defaultOptions: {
queries: { refetchOnWindowFocus: false, retry: false },
mutations: { retry: false },
},
});
return render(
<MemoryRouter initialEntries={[initialRoute]}>
<NuqsTestingAdapter
searchParams={initialSearch}
onUrlUpdate={onNuqsUrlUpdate}
rateLimitFactor={0}
hasMemory
>
<QueryClientProvider client={queryClient}>
<AppContext.Provider value={getAppContextMock(role)}>
<TimezoneProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 800, itemHeight: 46 }}
>
<ListAlertRules />
</VirtuosoMockContext.Provider>
</TimezoneProvider>
</AppContext.Provider>
</QueryClientProvider>
</NuqsTestingAdapter>
</MemoryRouter>,
);
}
export async function findAlertRow(alertName: string): Promise<HTMLElement> {
const cell = await screen.findByText(alertName, {}, { timeout: 5000 });
const row = cell.closest('tr');
if (!row) {
throw new Error(`Row not found for alert "${alertName}"`);
}
return row as HTMLElement;
}

View File

@@ -47,7 +47,6 @@ function ColumnSelector<TData>({
size="sm"
color="secondary"
prefix={<Columns3 size={14} />}
data-testid="alert-columns-button"
>
Columns
</Button>

View File

@@ -136,7 +136,6 @@ function ListAlertRules(): JSX.Element {
prefix={<Plus size={14} />}
onClick={handleNewAlert}
color="primary"
testId="list-alerts-new-alert-button"
>
New Alert
</Button>
@@ -158,7 +157,6 @@ function ListAlertRules(): JSX.Element {
value={searchText}
onChange={handleSearchChange}
suffix={<Search size={14} className={styles.searchIcon} />}
testId="list-alerts-search-input"
/>
</div>
)}

View File

@@ -26,18 +26,14 @@ export function getAlertRuleColumns(
enableSort: true,
enableRemove: false,
enableMove: false,
cell: ({ row, value }): JSX.Element => {
cell: ({ value }): JSX.Element => {
const state = String(value ?? '').toLowerCase();
const config = STATE_CONFIG[state] ?? {
color: 'secondary' as BadgeColor,
label: 'Unknown',
};
return (
<Badge
color={config.color}
variant="outline"
testId={`alert-row-${row.id ?? ''}-state`}
>
<Badge color={config.color} variant="outline">
{config.label}
</Badge>
);
@@ -51,11 +47,8 @@ export function getAlertRuleColumns(
enableSort: true,
enableRemove: false,
enableMove: false,
cell: ({ row, value }): JSX.Element => (
<TanStackTable.Text
title={value}
data-testid={`alert-row-${row.id ?? ''}-name`}
>
cell: ({ value }): JSX.Element => (
<TanStackTable.Text title={value}>
{String(value ?? '-')}
</TanStackTable.Text>
),
@@ -67,20 +60,15 @@ export function getAlertRuleColumns(
width: { fixed: '120px' },
enableSort: true,
enableMove: false,
cell: ({ row, value }): JSX.Element => {
cell: ({ value }): JSX.Element => {
const severity = String(value ?? '').toLowerCase();
if (!severity) {
return (
<TanStackTable.Text data-testid={`alert-row-${row.id ?? ''}-severity`}>
-
</TanStackTable.Text>
);
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<Badge
color={SEVERITY_BADGE_COLORS[severity] ?? 'secondary'}
variant="outline"
testId={`alert-row-${row.id ?? ''}-severity`}
>
{severity}
</Badge>

View File

@@ -1,8 +1,8 @@
import { logEventMock } from '__tests__/logEventMock';
import { render, screen, userEvent } from 'tests/test-utils';
import MCPServerSettings from './MCPServerSettings';
const mockLogEvent = jest.fn();
const mockCopyToClipboard = jest.fn();
const mockHistoryPush = jest.fn();
const mockUseGetGlobalConfig = jest.fn();
@@ -11,6 +11,11 @@ const mockUseGetTenantLicense = jest.fn();
const mockToastSuccess = jest.fn();
const mockToastWarning = jest.fn();
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: (...args: unknown[]): unknown => mockLogEvent(...args),
}));
jest.mock('api/generated/services/global', () => ({
useGetGlobalConfig: (...args: unknown[]): unknown =>
mockUseGetGlobalConfig(...args),
@@ -143,7 +148,7 @@ describe('MCPServerSettings', () => {
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
expect(logEventMock).toHaveBeenCalledWith('MCP Settings: Page viewed', {
expect(mockLogEvent).toHaveBeenCalledWith('MCP Settings: Page viewed', {
role: 'ADMIN',
});
});

View File

@@ -1,6 +1,5 @@
import userEvent from '@testing-library/user-event';
import MySettingsContainer from 'container/MySettings';
import { logEventMock } from '__tests__/logEventMock';
import {
act,
fireEvent,
@@ -13,6 +12,7 @@ import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
const updateMyPasswordFn = jest.fn();
@@ -62,6 +62,11 @@ jest.mock('hooks/useDarkMode', () => ({
})),
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn((eventName, data) => logEventFunction(eventName, data)),
}));
const errorNotification = jest.fn();
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
@@ -130,7 +135,7 @@ describe('MySettings Flows', () => {
await waitFor(() => {
expect(toggleThemeFunction).toHaveBeenCalled();
expect(logEventMock).toHaveBeenCalledWith(
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
theme: 'light',

View File

@@ -9,6 +9,11 @@ import {
import InviteTeamMembers from '../InviteTeamMembers';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
const mockNotificationSuccess = jest.fn() as jest.MockedFunction<
(args: { message: string }) => void
>;

View File

@@ -4,6 +4,11 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import OnboardingQuestionaire from '../index';
// Mock dependencies
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('lib/history', () => ({
__esModule: true,
default: {

View File

@@ -4,13 +4,15 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import logEvent from 'api/common/logEvent';
import i18n from 'ReactI18';
import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
jest.mock('api/common/logEvent');
describe('PipelinePage container test', () => {
it('should render CreatePipelineButton section', async () => {
const { asFragment } = render(
@@ -51,12 +53,9 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(logEventMock).toHaveBeenCalledWith(
'Logs: Pipelines: Entered Edit Mode',
{
source: 'signoz-ui',
},
);
expect(logEvent).toHaveBeenCalledWith('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
});
it('CreatePipelineButton - add new mode & tracking', async () => {
@@ -79,7 +78,7 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(logEventMock).toHaveBeenCalledWith(
expect(logEvent).toHaveBeenCalledWith(
'Logs: Pipelines: Clicked Add New Pipeline',
{
source: 'signoz-ui',

View File

@@ -1,118 +0,0 @@
import userEvent from '@testing-library/user-event';
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import ROUTES from 'constants/routes';
import { triggeredAlertsFixture } from 'mocks-server/__mockdata__/triggered_alerts';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen, waitFor } from 'tests/test-utils';
import { renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — empty / error states', () => {
it('shows the "No alerts firing" empty state when the API returns []', async () => {
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [], status: 'success' })),
),
);
renderTriggeredAlerts();
await screen.findByText('No alerts firing');
expect(
screen.getByTestId('triggered-alerts-empty-create-button'),
).toBeInTheDocument();
expect(
screen.getByTestId('triggered-alerts-empty-refresh-button'),
).toBeInTheDocument();
});
it('navigates to ROUTES.ALERTS_NEW when "Create Alert Rule" is clicked', async () => {
const user = userEvent.setup({ delay: null });
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [], status: 'success' })),
),
);
renderTriggeredAlerts();
await screen.findByText('No alerts firing');
await user.click(screen.getByTestId('triggered-alerts-empty-create-button'));
expect(safeNavigateMock).toHaveBeenCalledWith(
ROUTES.ALERTS_NEW,
expect.objectContaining({ newTab: false }),
);
});
it('shows ErrorEmptyState when the API returns 500', async () => {
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(ctx.status(500)),
),
);
renderTriggeredAlerts();
await screen.findByTestId('error-empty-state');
expect(screen.getByTestId('error-refresh-button')).toBeInTheDocument();
});
it('refetches on refresh button click after an initial error', async () => {
let callCount = 0;
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) => {
callCount += 1;
if (callCount === 1) {
return res(ctx.status(500));
}
return res(
ctx.status(200),
ctx.json({ data: triggeredAlertsFixture, status: 'success' }),
);
}),
);
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await screen.findByTestId('error-refresh-button');
await user.click(screen.getByTestId('error-refresh-button'));
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
});
it('shows NoResultsEmptyState when filters yield zero matches', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'this-matches-nothing-xyz');
await screen.findByTestId('no-results-empty-state');
expect(screen.getByTestId('no-results-title')).toHaveTextContent(
'No matching alerts',
);
expect(screen.getByTestId('no-results-subtitle')).toHaveTextContent(
'No alerts match your current filters. Try adjusting your search criteria.',
);
await user.click(screen.getByTestId('no-results-clear-button'));
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
expect(
(screen.getByTestId('triggered-alerts-search-input') as HTMLInputElement)
.value,
).toBe('');
});
});

View File

@@ -1,87 +0,0 @@
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — severity filter', () => {
it('filters to only critical-severity rows when "Critical" is selected', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
await user.click(screen.getByTestId('triggered-alerts-filter-combobox'));
const criticalOption = await screen.findByText(
'Critical (severity:critical)',
);
await user.click(criticalOption);
await user.keyboard('{Escape}');
await waitFor(() => {
expect(screen.getByText('High CPU Usage')).toBeInTheDocument();
expect(screen.queryByText('Memory Warning')).not.toBeInTheDocument();
expect(screen.queryByText('Disk Slow')).not.toBeInTheDocument();
expect(screen.queryByText('Network Hiccup')).not.toBeInTheDocument();
});
});
it('shows union when multiple severities are selected', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
await user.click(screen.getByTestId('triggered-alerts-filter-combobox'));
const critical = await screen.findByText('Critical (severity:critical)');
await user.click(critical);
const warning = await screen.findByText('Warning (severity:warning)');
await user.click(warning);
await user.keyboard('{Escape}');
await waitFor(() => {
expect(screen.getByText('High CPU Usage')).toBeInTheDocument();
expect(screen.getByText('Memory Warning')).toBeInTheDocument();
expect(screen.queryByText('Disk Slow')).not.toBeInTheDocument();
expect(screen.queryByText('Network Hiccup')).not.toBeInTheDocument();
});
});
it('clearing the filter shows all rows again', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
await user.click(screen.getByTestId('triggered-alerts-filter-combobox'));
const critical = await screen.findByText('Critical (severity:critical)');
await user.click(critical);
await user.keyboard('{Escape}');
await waitFor(() =>
expect(screen.queryByText('Memory Warning')).not.toBeInTheDocument(),
);
// Reopen the filter combobox and deselect Critical (clicking again toggles).
await user.click(screen.getByTestId('triggered-alerts-filter-combobox'));
const criticalAgain = await screen.findByText('Critical (severity:critical)');
await user.click(criticalAgain);
await user.keyboard('{Escape}');
await waitFor(() => {
expect(screen.getByText('High CPU Usage')).toBeInTheDocument();
expect(screen.getByText('Memory Warning')).toBeInTheDocument();
expect(screen.getByText('Disk Slow')).toBeInTheDocument();
expect(screen.getByText('Network Hiccup')).toBeInTheDocument();
});
});
});

View File

@@ -1,85 +0,0 @@
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — group by', () => {
it('renders a flat table when no group-by is selected', async () => {
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
// No "Group" column header in flat mode.
expect(screen.queryByText('Group')).not.toBeInTheDocument();
});
it('groups by service when "service" is selected', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
await user.click(screen.getByTestId('triggered-alerts-groupby-combobox'));
const serviceOption = await screen.findByText('service');
await user.click(serviceOption);
await user.keyboard('{Escape}');
await waitFor(() => expect(screen.getByText('Group')).toBeInTheDocument());
await waitFor(() => {
expect(screen.getByText('service:frontend')).toBeInTheDocument();
expect(screen.getByText('service:backend')).toBeInTheDocument();
expect(screen.getByText('service:misc')).toBeInTheDocument();
});
});
it('expands and collapses a group row to reveal nested alerts', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
await user.click(screen.getByTestId('triggered-alerts-groupby-combobox'));
const serviceOption = await screen.findByText('service');
await user.click(serviceOption);
await user.keyboard('{Escape}');
await waitFor(() =>
expect(screen.getByText('service:frontend')).toBeInTheDocument(),
);
// Nested rows aren't shown yet.
expect(screen.queryByText('High CPU Usage')).not.toBeInTheDocument();
// The "frontend" group sits first in the table; its expand toggle is the
// first `group-expand-toggle` in DOM order. Targeting by testid is safe
// against design changes that add other buttons to the row.
const expandToggles = screen.getAllByTestId('group-expand-toggle');
expect(expandToggles.length).toBeGreaterThan(0);
const frontendGroupBadge = screen.getByText('service:frontend');
const frontendRow = frontendGroupBadge.closest('tr');
expect(frontendRow).not.toBeNull();
const frontendToggle = (frontendRow as HTMLElement).querySelector(
'[data-testid="group-expand-toggle"]',
) as HTMLElement | null;
expect(frontendToggle).not.toBeNull();
await user.click(frontendToggle as HTMLElement);
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
expect(screen.getByText('Disk Slow')).toBeInTheDocument();
await user.click(frontendToggle as HTMLElement);
await waitFor(() =>
expect(screen.queryByText('High CPU Usage')).not.toBeInTheDocument(),
);
});
});

View File

@@ -1,115 +0,0 @@
import { screen, waitFor } from 'tests/test-utils';
import { renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — list rendering', () => {
beforeEach(() => {
jest.setSystemTime(new Date('2023-10-20T12:00:00Z'));
});
it('renders alerts from the API', async () => {
renderTriggeredAlerts();
await expect(
screen.findByTestId('alert-row-fp-critical-1-name'),
).resolves.toHaveTextContent('High CPU Usage');
expect(screen.getByTestId('alert-row-fp-warning-1-name')).toHaveTextContent(
'Memory Warning',
);
expect(screen.getByTestId('alert-row-fp-info-1-name')).toHaveTextContent(
'Disk Slow',
);
});
it('renders severity badges for alerts that have severity', async () => {
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId('alert-row-fp-critical-1-name'),
).toBeInTheDocument(),
);
expect(
screen.getByTestId('alert-row-fp-critical-1-severity'),
).toHaveTextContent('critical');
expect(
screen.getByTestId('alert-row-fp-warning-1-severity'),
).toHaveTextContent('warning');
expect(screen.getByTestId('alert-row-fp-info-1-severity')).toHaveTextContent(
'info',
);
expect(
screen.getByTestId('alert-row-fp-noseverity-severity'),
).toHaveTextContent('-');
});
it('renders status tags reflecting the alert state', async () => {
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId('alert-row-fp-critical-1-name'),
).toBeInTheDocument(),
);
expect(
screen.getByTestId('alert-row-fp-critical-1-status'),
).toHaveTextContent('Firing');
expect(screen.getByTestId('alert-row-fp-info-1-status')).toHaveTextContent(
'Unprocessed',
);
expect(
screen.getByTestId('alert-row-fp-suppressed-1-status'),
).toHaveTextContent('Suppressed');
});
it('renders status badges with semantic colors', async () => {
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId('alert-row-fp-critical-1-status'),
).toBeInTheDocument(),
);
expect(screen.getByTestId('alert-row-fp-critical-1-status')).toHaveAttribute(
'data-color',
'cherry',
);
expect(screen.getByTestId('alert-row-fp-info-1-status')).toHaveAttribute(
'data-color',
'forest',
);
expect(
screen.getByTestId('alert-row-fp-critical-1-severity'),
).toHaveAttribute('data-color', 'cherry');
expect(screen.getByTestId('alert-row-fp-warning-1-severity')).toHaveAttribute(
'data-color',
'amber',
);
});
it('renders the search input and filter comboboxes', async () => {
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId('alert-row-fp-critical-1-name'),
).toBeInTheDocument(),
);
expect(
screen.getByTestId('triggered-alerts-search-input'),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Search alerts by name'),
).toBeInTheDocument();
expect(
screen.getByTestId('triggered-alerts-filter-combobox'),
).toBeInTheDocument();
expect(
screen.getByTestId('triggered-alerts-groupby-combobox'),
).toBeInTheDocument();
});
});

View File

@@ -1,76 +0,0 @@
import userEvent from '@testing-library/user-event';
import { triggeredAlertsPaginationFixture } from 'mocks-server/__mockdata__/triggered_alerts';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen, waitFor } from 'tests/test-utils';
import { getCurrentNuqsQueryString } from 'tests/nuqs-helpers';
import { renderTriggeredAlerts } from './_helpers';
function usePaginationHandler(): void {
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: triggeredAlertsPaginationFixture,
status: 'success',
}),
),
),
);
}
describe('TriggeredAlerts — pagination', () => {
// Default sort is duration ascending = newest startsAt first. Fixture indices
// 0..14 use startsAt 2023-10-01..15, so index 14 (newest) appears first and
// index 0 (oldest) appears last. Page 1 (limit 10) = items 14..5. Page 2 = 4..0.
it('shows the first 10 rows on page 1 by default', async () => {
usePaginationHandler();
renderTriggeredAlerts();
await screen.findByText('Pag Alert 14');
expect(screen.getByText('Pag Alert 5')).toBeInTheDocument();
expect(screen.queryByText('Pag Alert 4')).not.toBeInTheDocument();
expect(screen.queryByText('Pag Alert 0')).not.toBeInTheDocument();
});
it('renders a "page 2" pagination button reflecting total=15 with pageSize=10', async () => {
usePaginationHandler();
renderTriggeredAlerts();
await screen.findByText('Pag Alert 14');
const nav = screen.getByRole('navigation');
const page2 = Array.from(nav.querySelectorAll('button')).find(
(b) => b.textContent?.trim() === '2',
);
expect(page2).toBeDefined();
});
it('navigates to page 2 and shows the next batch of alerts', async () => {
usePaginationHandler();
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await screen.findByText('Pag Alert 14');
const nav = screen.getByRole('navigation');
const page2Button = Array.from(nav.querySelectorAll('button')).find(
(b) => b.textContent?.trim() === '2',
);
if (!page2Button) {
throw new Error('Page 2 button not found');
}
await user.click(page2Button);
await waitFor(() =>
expect(screen.getByText('Pag Alert 0')).toBeInTheDocument(),
);
expect(screen.getByText('Pag Alert 4')).toBeInTheDocument();
expect(screen.queryByText('Pag Alert 14')).not.toBeInTheDocument();
await waitFor(() => expect(getCurrentNuqsQueryString()).toContain('page=2'));
});
});

View File

@@ -1,143 +0,0 @@
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen, waitFor } from 'tests/test-utils';
import { getTriggeredAlertRowTestId, renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — row click', () => {
it('navigates to the alert overview with the rule id from labels on row click', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId(getTriggeredAlertRowTestId('fp-critical-1', 'name')),
).toBeInTheDocument(),
);
await user.click(
screen.getByTestId(getTriggeredAlertRowTestId('fp-critical-1', 'name')),
);
expect(safeNavigateMock).toHaveBeenCalledWith(
'/alerts/overview?ruleId=rule-1',
);
expect(logEventMock).toHaveBeenCalledWith(
'Alert: Triggered alert clicked',
expect.objectContaining({
ruleId: 'rule-1',
alertName: 'High CPU Usage',
}),
);
});
it('opens in a new tab when ctrl+clicked', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId(getTriggeredAlertRowTestId('fp-warning-1', 'name')),
).toBeInTheDocument(),
);
await user.keyboard('{Control>}');
await user.click(
screen.getByTestId(getTriggeredAlertRowTestId('fp-warning-1', 'name')),
);
await user.keyboard('{/Control}');
expect(safeNavigateMock).toHaveBeenCalledWith(
'/alerts/overview?ruleId=rule-2',
{ newTab: true },
);
});
it('navigates correctly when ruleId is parsed from generatorURL', async () => {
// Override fixture so the alert has no labels.ruleId but a valid
// generatorURL → getRuleId() falls back to parsing the URL.
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: [
{
fingerprint: 'fp-no-rule-label',
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
updatedAt: '2023-10-20T00:00:00Z',
generatorURL: 'http://localhost/alerts/edit?ruleId=rule-from-url',
labels: { alertname: 'URL Rule Alert' },
annotations: {},
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
},
],
status: 'success',
}),
),
),
);
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId(getTriggeredAlertRowTestId('fp-no-rule-label', 'name')),
).toBeInTheDocument(),
);
await user.click(
screen.getByTestId(getTriggeredAlertRowTestId('fp-no-rule-label', 'name')),
);
expect(safeNavigateMock).toHaveBeenCalledWith(
'/alerts/overview?ruleId=rule-from-url',
);
});
it('does not navigate when the row has no ruleId anywhere', async () => {
server.use(
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: [
{
fingerprint: 'fp-no-rule',
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
updatedAt: '2023-10-20T00:00:00Z',
labels: { alertname: 'No Rule Alert' },
annotations: {},
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
},
],
status: 'success',
}),
),
),
);
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(
screen.getByTestId(getTriggeredAlertRowTestId('fp-no-rule', 'name')),
).toBeInTheDocument(),
);
await user.click(
screen.getByTestId(getTriggeredAlertRowTestId('fp-no-rule', 'name')),
);
expect(safeNavigateMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,118 +0,0 @@
import userEvent from '@testing-library/user-event';
import { screen, waitFor } from 'tests/test-utils';
import { renderTriggeredAlerts } from './_helpers';
describe('TriggeredAlerts — search', () => {
it('filters rows by alertname when typing in the search input', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
expect(screen.getByText('Memory Warning')).toBeInTheDocument();
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'CPU');
await waitFor(() => {
expect(screen.getByText('High CPU Usage')).toBeInTheDocument();
expect(screen.queryByText('Memory Warning')).not.toBeInTheDocument();
});
});
it('shows all rows again when search is cleared', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'CPU');
await waitFor(() =>
expect(screen.queryByText('Memory Warning')).not.toBeInTheDocument(),
);
await user.clear(input);
await waitFor(() => {
expect(screen.getByText('High CPU Usage')).toBeInTheDocument();
expect(screen.getByText('Memory Warning')).toBeInTheDocument();
expect(screen.getByText('Disk Slow')).toBeInTheDocument();
});
});
it('matches rows by label value (case-insensitive)', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
// "backend" matches `service: backend` labels on Memory Warning and Network Hiccup.
await user.type(input, 'backend');
await waitFor(() => {
expect(screen.getByText('Memory Warning')).toBeInTheDocument();
expect(screen.getByText('Network Hiccup')).toBeInTheDocument();
expect(screen.queryByText('High CPU Usage')).not.toBeInTheDocument();
expect(screen.queryByText('Disk Slow')).not.toBeInTheDocument();
});
});
it('matches rows by label key', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'staging');
await waitFor(() => {
expect(screen.getByText('Disk Slow')).toBeInTheDocument();
expect(screen.queryByText('High CPU Usage')).not.toBeInTheDocument();
expect(screen.queryByText('Memory Warning')).not.toBeInTheDocument();
});
});
it('renders the no-results empty state when the search matches nothing', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts();
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'zzzzznever-matches-anything');
await waitFor(() =>
expect(screen.getByTestId('no-results-empty-state')).toBeInTheDocument(),
);
});
it('resets pagination to page 1 when the search changes', async () => {
const user = userEvent.setup({ delay: null });
renderTriggeredAlerts({ initialRoute: '/?page=2&limit=2' });
await waitFor(() =>
expect(screen.getByText('Disk Slow')).toBeInTheDocument(),
);
const input = screen.getByTestId('triggered-alerts-search-input');
await user.type(input, 'CPU');
await waitFor(() =>
expect(screen.getByText('High CPU Usage')).toBeInTheDocument(),
);
});
});

View File

@@ -1,181 +0,0 @@
import type { SortState } from 'components/TanStackTableView/types';
import type { Alert } from '../types';
import {
getAlertSortValue,
getRuleId,
normalizeAlerts,
sortAlerts,
} from '../utils';
const alertWithFingerprint: Alert = {
fingerprint: 'fp-existing',
labels: { alertname: 'Test', severity: 'critical' },
annotations: {},
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
};
const alertWithoutFingerprint: Alert = {
...alertWithFingerprint,
fingerprint: undefined,
};
describe('normalizeAlerts', () => {
it('returns empty array when given undefined', () => {
expect(normalizeAlerts(undefined)).toStrictEqual([]);
});
it('preserves existing fingerprints', () => {
const result = normalizeAlerts([alertWithFingerprint]);
expect(result).toHaveLength(1);
expect(result[0].fingerprint).toBe('fp-existing');
});
it('mints fingerprint when missing', () => {
const result = normalizeAlerts([alertWithoutFingerprint]);
expect(result[0].fingerprint).toBeDefined();
expect(typeof result[0].fingerprint).toBe('string');
expect(result[0].fingerprint?.length).toBeGreaterThan(0);
});
it('does not mutate the input array', () => {
const input = [alertWithFingerprint];
const copy = JSON.parse(JSON.stringify(input));
normalizeAlerts(input);
expect(input).toStrictEqual(copy);
});
});
describe('getAlertSortValue', () => {
const alert: Alert = {
fingerprint: 'fp',
labels: { alertname: 'CPU', severity: 'critical' },
annotations: {},
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
};
it('returns status.state for "status"', () => {
expect(getAlertSortValue(alert, 'status')).toBe('active');
});
it('returns alertname for "alertName"', () => {
expect(getAlertSortValue(alert, 'alertName')).toBe('CPU');
});
it('returns severity for "severity"', () => {
expect(getAlertSortValue(alert, 'severity')).toBe('critical');
});
it('returns elapsed ms for "firingSince"', () => {
const result = getAlertSortValue(alert, 'firingSince');
expect(typeof result).toBe('number');
expect(result).toBeGreaterThan(0);
});
it('returns elapsed ms for "duration"', () => {
const result = getAlertSortValue(alert, 'duration');
expect(typeof result).toBe('number');
});
it('returns empty string for unknown columns', () => {
expect(getAlertSortValue(alert, 'unknown')).toBe('');
});
it('returns empty string for missing fields', () => {
const empty = { ...alert, status: undefined, labels: undefined };
expect(getAlertSortValue(empty, 'status')).toBe('');
expect(getAlertSortValue(empty, 'alertName')).toBe('');
expect(getAlertSortValue(empty, 'severity')).toBe('');
});
it('returns empty for firingSince with no startsAt', () => {
const empty = { ...alert, startsAt: undefined };
expect(getAlertSortValue(empty, 'firingSince')).toBe('');
});
});
describe('sortAlerts', () => {
const a: Alert = {
fingerprint: 'a',
labels: { alertname: 'A' },
annotations: {},
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
};
const b: Alert = { ...a, fingerprint: 'b', labels: { alertname: 'B' } };
const c: Alert = { ...a, fingerprint: 'c', labels: { alertname: 'C' } };
it('sorts ascending when given orderBy', () => {
const order: SortState = { columnName: 'alertName', order: 'asc' };
const result = sortAlerts([c, a, b], order);
expect(result.map((x) => x.labels?.alertname)).toStrictEqual(['A', 'B', 'C']);
});
it('sorts descending', () => {
const order: SortState = { columnName: 'alertName', order: 'desc' };
const result = sortAlerts([a, b, c], order);
expect(result.map((x) => x.labels?.alertname)).toStrictEqual(['C', 'B', 'A']);
});
it('falls back to default duration asc when orderBy is null', () => {
const result = sortAlerts([a, b, c], null);
expect(result).toHaveLength(3);
});
});
describe('getRuleId', () => {
const base: Alert = {
labels: {},
annotations: {},
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
fingerprint: 'fp',
};
it('returns labels.ruleId when present', () => {
expect(getRuleId({ ...base, labels: { ruleId: 'rule-1' } })).toBe('rule-1');
});
it('falls back to generatorURL when ruleId label missing', () => {
expect(
getRuleId({
...base,
generatorURL: 'http://localhost/foo?ruleId=rule-42',
}),
).toBe('rule-42');
});
it('prefers labels.ruleId over generatorURL', () => {
expect(
getRuleId({
...base,
labels: { ruleId: 'from-label' },
generatorURL: 'http://localhost/foo?ruleId=from-url',
}),
).toBe('from-label');
});
it('returns null when generatorURL has no ruleId param', () => {
expect(
getRuleId({ ...base, generatorURL: 'http://localhost/foo' }),
).toBeNull();
});
it('returns null when generatorURL is invalid', () => {
expect(getRuleId({ ...base, generatorURL: 'not-a-url' })).toBeNull();
});
it('returns null when no source available', () => {
expect(getRuleId(base)).toBeNull();
});
});

View File

@@ -1,71 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import { render, RenderResult, screen } from '@testing-library/react';
import TriggeredAlerts from 'container/TriggeredAlerts';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { AppContext } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import { onNuqsUrlUpdate, resetNuqsState } from 'tests/nuqs-helpers';
import { getAppContextMock } from 'tests/test-utils';
interface RenderOptions {
initialRoute?: string;
}
export function renderTriggeredAlerts(
options: RenderOptions = {},
): RenderResult {
const { initialRoute = '/' } = options;
const initialSearch = initialRoute.includes('?')
? initialRoute.slice(initialRoute.indexOf('?'))
: '';
resetNuqsState(initialSearch);
const queryClient = new QueryClient({
defaultOptions: {
queries: { refetchOnWindowFocus: false, retry: false },
mutations: { retry: false },
},
});
return render(
<MemoryRouter initialEntries={[initialRoute]}>
<NuqsTestingAdapter
searchParams={initialSearch}
onUrlUpdate={onNuqsUrlUpdate}
rateLimitFactor={0}
hasMemory
>
<QueryClientProvider client={queryClient}>
<AppContext.Provider value={getAppContextMock('ADMIN')}>
<TimezoneProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 800, itemHeight: 46 }}
>
<TriggeredAlerts />
</VirtuosoMockContext.Provider>
</TimezoneProvider>
</AppContext.Provider>
</QueryClientProvider>
</NuqsTestingAdapter>
</MemoryRouter>,
);
}
export async function findAlertRow(alertName: string): Promise<HTMLElement> {
const cell = await screen.findByText(alertName, {}, { timeout: 5000 });
const row = cell.closest('tr');
if (!row) {
throw new Error(`Row not found for alert "${alertName}"`);
}
return row as HTMLElement;
}
export function getTriggeredAlertRowTestId(
fingerprint: string,
column: 'name' | 'severity' | 'status',
): string {
return `alert-row-${fingerprint}-${column}`;
}

View File

@@ -2,32 +2,31 @@ import { Badge } from '@signozhq/ui/badge';
interface AlertStatusTagProps {
state: string;
testId?: string;
}
function AlertStatusTag({ state, testId }: AlertStatusTagProps): JSX.Element {
function AlertStatusTag({ state }: AlertStatusTagProps): JSX.Element {
switch (state) {
case 'unprocessed':
return (
<Badge color="success" variant="outline" testId={testId}>
<Badge color="success" variant="outline">
Unprocessed
</Badge>
);
case 'active':
return (
<Badge color="error" variant="outline" testId={testId}>
<Badge color="error" variant="outline">
Firing
</Badge>
);
case 'suppressed':
return (
<Badge color="error" variant="outline" testId={testId}>
<Badge color="error" variant="outline">
Suppressed
</Badge>
);
default:
return (
<Badge color="secondary" variant="outline" testId={testId}>
<Badge color="secondary" variant="outline">
Unknown
</Badge>
);

View File

@@ -3,7 +3,6 @@ import { CircleCheck, Plus, RefreshCw } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isModifierKeyPressed } from 'utils/app';
import styles from './EmptyStates.module.scss';
@@ -14,12 +13,9 @@ interface EmptyStateProps {
export function EmptyState({ onRefresh }: EmptyStateProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const handleCreateAlert = useCallback(
(e: React.MouseEvent): void => {
safeNavigate(ROUTES.ALERTS_NEW, { newTab: isModifierKeyPressed(e) });
},
[safeNavigate],
);
const handleCreateAlert = useCallback((): void => {
safeNavigate(ROUTES.ALERTS_NEW);
}, [safeNavigate]);
return (
<div className={styles.emptyState}>
@@ -34,7 +30,6 @@ export function EmptyState({ onRefresh }: EmptyStateProps): JSX.Element {
color="primary"
prefix={<Plus size={14} />}
onClick={handleCreateAlert}
testId="triggered-alerts-empty-create-button"
>
Create Alert Rule
</Button>
@@ -44,7 +39,6 @@ export function EmptyState({ onRefresh }: EmptyStateProps): JSX.Element {
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
testId="triggered-alerts-empty-refresh-button"
>
Refresh
</Button>

View File

@@ -44,7 +44,6 @@ export function GroupTagsCell({
prefix={
localIsExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />
}
testId="group-expand-toggle"
/>
<div className={styles.tagsContainer}>
{tags.map((tag) => (

View File

@@ -175,7 +175,6 @@ function TriggeredAlerts(): JSX.Element {
value={searchText}
onChange={handleSearchChange}
suffix={<Search size={14} className={styles.searchIcon} />}
testId="triggered-alerts-search-input"
/>
<ComboboxSimple
className={styles.filterSelect}
@@ -187,7 +186,6 @@ function TriggeredAlerts(): JSX.Element {
allowCreate
items={severyFilters}
maxDisplayedPills={2}
testId="triggered-alerts-filter-combobox"
/>
<ComboboxSimple
className={styles.filterSelect}
@@ -198,7 +196,6 @@ function TriggeredAlerts(): JSX.Element {
items={labelOptions}
multiple
maxDisplayedPills={2}
testId="triggered-alerts-groupby-combobox"
/>
</div>

View File

@@ -21,11 +21,8 @@ export function getAlertColumns(
width: { fixed: '100px' },
enableSort: false,
enableMove: false,
cell: ({ row, value }): JSX.Element => (
<AlertStatusTag
state={String(value ?? '')}
testId={`alert-row-${row.fingerprint ?? ''}-status`}
/>
cell: ({ value }): JSX.Element => (
<AlertStatusTag state={String(value ?? '')} />
),
},
{
@@ -35,11 +32,8 @@ export function getAlertColumns(
width: { default: '100%' },
enableSort: true,
enableMove: false,
cell: ({ row, value }): JSX.Element => (
<TanStackTable.Text
title={value}
data-testid={`alert-row-${row.fingerprint ?? ''}-name`}
>
cell: ({ value }): JSX.Element => (
<TanStackTable.Text title={value}>
{String(value ?? '-')}
</TanStackTable.Text>
),
@@ -51,17 +45,15 @@ export function getAlertColumns(
width: { fixed: '120px' },
enableSort: true,
enableMove: false,
cell: ({ row, value }): JSX.Element => {
cell: ({ value }): JSX.Element => {
const severity = String(value ?? '').toLowerCase();
const testId = `alert-row-${row.fingerprint ?? ''}-severity`;
if (!severity) {
return <TanStackTable.Text data-testid={testId}>-</TanStackTable.Text>;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<Badge
color={SEVERITY_BADGE_COLORS[severity] ?? 'secondary'}
variant="outline"
testId={testId}
>
{severity}
</Badge>

View File

@@ -17,6 +17,11 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('api/dashboard/substitute_vars', () => ({
getSubstituteVars: jest.fn(),
}));

View File

@@ -1,85 +0,0 @@
import {
RuletypesAlertStateDTO,
RuletypesAlertTypeDTO,
RuletypesPanelTypeDTO,
RuletypesQueryTypeDTO,
RuletypesRuleTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import type {
RuletypesRuleConditionDTO,
RuletypesRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
const baseCondition: RuletypesRuleConditionDTO = {
compositeQuery: {
queryType: RuletypesQueryTypeDTO.builder,
panelType: RuletypesPanelTypeDTO.graph,
queries: null,
},
} as unknown as RuletypesRuleConditionDTO;
const make = (
id: string,
overrides: Partial<RuletypesRuleDTO>,
): RuletypesRuleDTO => ({
id,
alert: `Alert ${id}`,
alertType: RuletypesAlertTypeDTO.METRIC_BASED_ALERT,
condition: baseCondition,
createdAt: '2023-10-15T10:00:00Z',
updatedAt: '2023-10-19T10:00:00Z',
createdBy: 'alice@signoz.io',
updatedBy: 'alice@signoz.io',
disabled: false,
state: RuletypesAlertStateDTO.inactive,
labels: { severity: 'info' },
annotations: {},
source: '',
evalWindow: '5m0s',
frequency: '1m0s',
ruleType: RuletypesRuleTypeDTO.threshold_rule,
...overrides,
});
export const alertRulesFixture: RuletypesRuleDTO[] = [
make('rule-1', {
alert: 'High CPU Alert',
state: RuletypesAlertStateDTO.firing,
labels: { severity: 'critical', team: 'infra' },
}),
make('rule-2', {
alert: 'Memory Pending Alert',
state: RuletypesAlertStateDTO.pending,
labels: { severity: 'warning', team: 'backend' },
}),
make('rule-3', {
alert: 'Healthy Alert',
state: RuletypesAlertStateDTO.inactive,
labels: { severity: 'info', team: 'infra' },
}),
make('rule-4', {
alert: 'Disabled Alert',
state: RuletypesAlertStateDTO.disabled,
disabled: true,
labels: { severity: 'critical', team: 'frontend' },
}),
make('rule-5', {
alert: 'No Labels Alert',
state: RuletypesAlertStateDTO.inactive,
labels: {},
}),
];
export const alertRulesPaginationFixture: RuletypesRuleDTO[] = Array.from(
{ length: 15 },
(_, i) =>
make(`rule-pag-${i}`, {
alert: `Pag Rule ${i}`,
state:
i % 2 === 0
? RuletypesAlertStateDTO.firing
: RuletypesAlertStateDTO.inactive,
labels: { severity: i % 2 === 0 ? 'critical' : 'warning' },
createdAt: `2023-10-${String(i + 1).padStart(2, '0')}T10:00:00Z`,
}),
);

View File

@@ -1,102 +0,0 @@
import type { AlertmanagertypesDeprecatedGettableAlertDTO } from 'api/generated/services/sigNoz.schemas';
export const triggeredAlertsFixture: AlertmanagertypesDeprecatedGettableAlertDTO[] =
[
{
fingerprint: 'fp-critical-1',
startsAt: '2023-10-19T10:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
generatorURL: 'http://localhost/alerts/edit?ruleId=rule-1&panelTypes=graph',
labels: {
alertname: 'High CPU Usage',
severity: 'critical',
ruleId: 'rule-1',
service: 'frontend',
env: 'prod',
},
annotations: {
summary: 'CPU above 90%',
description: 'Frontend CPU usage critical',
},
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: ['slack'],
},
{
fingerprint: 'fp-warning-1',
startsAt: '2023-10-19T09:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
generatorURL: 'http://localhost/alerts/edit?ruleId=rule-2',
labels: {
alertname: 'Memory Warning',
severity: 'warning',
ruleId: 'rule-2',
service: 'backend',
env: 'prod',
},
annotations: { summary: 'Memory high' },
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: ['slack'],
},
{
fingerprint: 'fp-info-1',
startsAt: '2023-10-19T08:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
generatorURL: 'http://localhost/alerts/edit?ruleId=rule-3',
labels: {
alertname: 'Disk Slow',
severity: 'info',
ruleId: 'rule-3',
service: 'frontend',
env: 'staging',
},
annotations: { summary: 'Disk slow' },
status: { state: 'unprocessed', silencedBy: [], inhibitedBy: [] },
receivers: ['email'],
},
{
fingerprint: 'fp-suppressed-1',
startsAt: '2023-10-19T07:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
generatorURL: 'http://localhost/alerts/edit?ruleId=rule-4',
labels: {
alertname: 'Network Hiccup',
severity: 'error',
ruleId: 'rule-4',
service: 'backend',
env: 'dev',
},
annotations: { summary: 'Network errors' },
status: { state: 'suppressed', silencedBy: ['s-1'], inhibitedBy: [] },
receivers: ['pagerduty'],
},
{
fingerprint: 'fp-noseverity',
startsAt: '2023-10-19T06:00:00Z',
endsAt: '0001-01-01T00:00:00Z',
labels: {
alertname: 'Unknown Alert',
service: 'misc',
},
annotations: {},
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
},
];
// Bigger fixture for pagination tests (15 entries → 2 pages at limit=10).
export const triggeredAlertsPaginationFixture: AlertmanagertypesDeprecatedGettableAlertDTO[] =
Array.from({ length: 15 }, (_, i) => ({
fingerprint: `fp-pag-${i}`,
startsAt: `2023-10-${String(i + 1).padStart(2, '0')}T10:00:00Z`,
endsAt: '0001-01-01T00:00:00Z',
generatorURL: `http://localhost/alerts/edit?ruleId=rule-pag-${i}`,
labels: {
alertname: `Pag Alert ${i}`,
severity: i % 2 === 0 ? 'critical' : 'warning',
ruleId: `rule-pag-${i}`,
service: 'frontend',
},
annotations: {},
status: { state: 'active', silencedBy: [], inhibitedBy: [] },
receivers: [],
}));

View File

@@ -3,8 +3,6 @@ import { rest } from 'msw';
import commonEnTranslation from '../../public/locales/en/common.json';
import enTranslation from '../../public/locales/en/translation.json';
import { allAlertChannels } from './__mockdata__/alerts';
import { alertRulesFixture } from './__mockdata__/alert_rules';
import { triggeredAlertsFixture } from './__mockdata__/triggered_alerts';
import { billingSuccessResponse } from './__mockdata__/billing';
import {
dashboardSuccessResponse,
@@ -238,38 +236,6 @@ export const handlers = [
rest.get('http://localhost/api/v1/channels', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: allAlertChannels, status: 'success' })),
),
rest.get('http://localhost/api/v1/alerts', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ data: triggeredAlertsFixture, status: 'success' }),
),
),
rest.get('http://localhost/api/v2/rules', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ data: alertRulesFixture, status: 'success' }),
),
),
rest.post('http://localhost/api/v2/rules', async (req, res, ctx) => {
const body = (await req.json()) as { alert?: string };
return res(
ctx.status(201),
ctx.json({
data: {
...alertRulesFixture[0],
id: 'new-rule-id',
alert: body?.alert ?? 'New Rule',
},
status: 'success',
}),
);
}),
rest.patch('http://localhost/api/v2/rules/:id', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success' })),
),
rest.delete('http://localhost/api/v2/rules/:id', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success' })),
),
rest.delete('http://localhost/api/v1/channels/:id', (_, res, ctx) =>
res(
ctx.status(200),

View File

@@ -10,6 +10,11 @@ jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
children,
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('lib/history', () => ({
push: jest.fn(),
listen: jest.fn(() => jest.fn()),

View File

@@ -14,6 +14,11 @@ jest.mock('AppRoutes/utils', () => ({
const mockAfterLogin = jest.mocked(afterLogin);
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('lib/history', () => ({
__esModule: true,
default: {

View File

@@ -1,41 +0,0 @@
// Helpers for tests that drive components built with `nuqs`.
//
// We replace `NuqsAdapter` (which throttles URL writes to `window.history`)
// with `NuqsTestingAdapter`. The testing adapter applies URL updates
// synchronously (`rateLimitFactor: 0`) and stores state in memory
// (`hasMemory: true`) so subsequent reads see the latest value without any
// `flushNuqsUrl` sleep. Each test gets a fresh queue because
// `resetUrlUpdateQueueOnMount` defaults to `true`.
//
// Reads on `window.location.search` are no longer authoritative since the
// adapter does not push to the browser history. Use
// `getCurrentNuqsQueryString()` (or assert on `lastNuqsUrlUpdate`) instead.
import type { OnUrlUpdateFunction } from 'nuqs/adapters/testing';
let lastUrlUpdate: { searchParams: URLSearchParams; queryString: string } = {
searchParams: new URLSearchParams(),
queryString: '',
};
export function resetNuqsState(initialQuery = ''): void {
lastUrlUpdate = {
searchParams: new URLSearchParams(initialQuery),
queryString: initialQuery,
};
}
export const onNuqsUrlUpdate: OnUrlUpdateFunction = (event) => {
lastUrlUpdate = {
searchParams: event.searchParams,
queryString: event.queryString,
};
};
export function getCurrentNuqsQueryString(): string {
return lastUrlUpdate.queryString;
}
export function getCurrentNuqsSearchParams(): URLSearchParams {
return lastUrlUpdate.searchParams;
}

View File

@@ -648,3 +648,176 @@ describe('getQueryContextAtCursor - trailing dot in key/value', () => {
expect(ctx.keyToken).toBe('k8s.namespace');
});
});
describe('getQueryContextAtCursor - partial operator', () => {
it('treats text after an incomplete key as an operator prefix', () => {
const q = 'service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'c',
position: expect.objectContaining({
operatorStart: 13,
operatorEnd: 13,
}),
}),
);
});
it('keeps the operator context while completing contains', () => {
const q = 'service.name cont';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('cont');
});
it('treats cursor mid-token as operator context', () => {
const q = 'service.name cont';
// cursor sits between "con" and "t" — user still typing the operator
const ctx = getQueryContextAtCursor(q, 15);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('cont');
});
it('keeps operator context when an AND conjunction precedes the pair', () => {
const q = 'a = 1 AND service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'c',
position: expect.objectContaining({
operatorStart: 23,
operatorEnd: 23,
}),
}),
);
});
it('keeps operator context when an open parenthesis precedes the pair', () => {
const q = '(service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
});
it('re-glues a partial operator that follows a NOT negation', () => {
const q = 'service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
// operatorStart points at the partial operator (post-NOT), not at the
// negation — so suggestion selection only replaces the partial, never
// the user's typed NOT.
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'NOT c',
hasNegation: true,
position: expect.objectContaining({
negationStart: 13,
negationEnd: 15,
operatorStart: 17,
operatorEnd: 17,
}),
}),
);
});
it('re-glues a multi-character partial operator after NOT', () => {
const q = 'service.name NOT lik';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT lik');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('re-glues an uppercase partial operator after NOT', () => {
const q = 'service.name NOT EXI';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT EXI');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('preserves original NOT casing in the operator text', () => {
const q = 'service.name not c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('not c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('tolerates extra whitespace between NOT and the partial operator', () => {
const q = 'service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
// Display text uses a canonical single space between NOT and the
// partial, regardless of how many spaces the user typed.
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('keeps operator context for NOT-prefixed partial inside parentheses', () => {
const q = '(service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('keeps operator context for NOT-prefixed partial after an AND conjunction', () => {
const q = 'a = 1 AND service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('re-glues the most recent incomplete pair when three partial tokens are typed', () => {
// Pins documented behavior: with two trailing partial pairs (`c` and
// `k`), the heuristic pairs the most recent two — `c` becomes the
// key, `k` becomes the partial operator. The earlier `service.name`
// is dropped from the current pair view.
const q = 'service.name c k';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('c');
expect(ctx.operatorToken).toBe('k');
});
});

View File

@@ -605,6 +605,98 @@ export function getQueryContextAtCursor(
queryPairs,
);
// Re-glue a partial operator that ANTLR has lexed as a second key.
//
// When the user types `service.name c` (or `service.name NOT c`), the
// lexer sees two KEY tokens (`service.name`, `c`) instead of a key +
// partial operator, so `extractQueryPairs` emits two consecutive
// key-only incomplete pairs. Downstream, that makes the dropdown
// suggest keys when it should be suggesting operators.
//
// Detect that pattern — a previous incomplete key-only pair followed
// by another incomplete key-only `currentPair`, separated only by
// whitespace (or by a negation token attached to the previous pair) —
// and rebuild a single synthetic pair where the previous pair's key
// is the key and the current pair's key is treated as the partial
// operator. The synthetic pair inherits the previous pair's negation
// flag and positions via the spread, so `NOT <partial>` propagates
// correctly to consumers.
const previousIncompletePair = queryPairs
.filter(
(pair) =>
!pair.isComplete &&
!!pair.key &&
!pair.operator &&
pair.position.keyEnd < (currentPair?.position.keyStart ?? cursorIndex),
)
.sort((a, b) => b.position.keyEnd - a.position.keyEnd)[0];
if (
previousIncompletePair &&
currentPair &&
currentPair !== previousIncompletePair &&
!currentPair.operator &&
currentPair.position.keyStart > previousIncompletePair.position.keyEnd
) {
const negationStart = previousIncompletePair.position.negationStart ?? 0;
const negationEnd = previousIncompletePair.position.negationEnd ?? 0;
const negationAfterKey =
previousIncompletePair.hasNegation &&
negationStart > previousIncompletePair.position.keyEnd;
const gapStart = negationAfterKey
? negationEnd + 1
: previousIncompletePair.position.keyEnd + 1;
const textBetweenPairs = query.slice(
gapStart,
currentPair.position.keyStart,
);
if (textBetweenPairs.trim() === '') {
// The replacement range (operatorStart/operatorEnd) must point
// at the partial operator only, NOT the leading negation.
// Consumers like QuerySearch use it to splice the chosen
// suggestion in-place, so including the negation would let a
// `NOT lik` -> `LIKE` selection erase the user's typed `NOT`.
// Matches the convention used for complete pairs in
// extractQueryPairs, where operatorStart starts after the
// negation token.
const operatorStart = currentPair.position.keyStart;
const operatorEnd = currentPair.position.keyEnd;
const partialOperator = query.slice(operatorStart, operatorEnd + 1);
const operatorText = negationAfterKey
? `${query.slice(negationStart, negationEnd + 1)} ${partialOperator}`
: partialOperator;
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: operatorText,
isInKey: false,
isInNegation: false,
isInOperator: true,
isInValue: false,
isInConjunction: false,
isInFunction: false,
isInParenthesis: false,
isInBracketList: false,
keyToken: previousIncompletePair.key,
operatorToken: operatorText,
queryPairs,
currentPair: {
...previousIncompletePair,
operator: operatorText,
position: {
...previousIncompletePair.position,
operatorStart,
operatorEnd,
},
},
};
}
}
// Check if cursor is within any of the specific context boundaries
// FIXED: Include the case where the cursor is exactly at the end of a boundary
const isInKeyBoundary =

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="b27f1ad0-7d11-4247-9da3-91bce6211f32" x1="8.798" y1="8.703" x2="14.683" y2="8.703" gradientUnits="userSpaceOnUse"><stop offset="0.001" stop-color="#773adc"/><stop offset="1" stop-color="#552f99"/></linearGradient><linearGradient id="b2f92112-4ca9-4b17-a019-c9f26c1a4a8f" x1="5.764" y1="3.777" x2="5.764" y2="13.78" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a67af4"/><stop offset="0.999" stop-color="#773adc"/></linearGradient></defs><g id="b8a0486a-5501-4d92-b540-a766c4b3b548"><g><g><g><path d="M16.932,11.578a8.448,8.448,0,0,1-7.95,5.59,8.15,8.15,0,0,1-2.33-.33,2.133,2.133,0,0,0,.18-.83c.01,0,.03.01.04.01a7.422,7.422,0,0,0,2.11.3,7.646,7.646,0,0,0,6.85-4.28l.01-.01Z" fill="#32bedd"/><path d="M3.582,14.068a2.025,2.025,0,0,0-.64.56,8.6,8.6,0,0,1-1.67-2.44l1.04.23v.26a.6.6,0,0,0,.47.59l.14.03a6.136,6.136,0,0,0,.62.73Z" fill="#32bedd"/><path d="M12.352.958a2.28,2.28,0,0,0-.27.81c-.02-.01-.05-.02-.07-.03a7.479,7.479,0,0,0-3.03-.63,7.643,7.643,0,0,0-5.9,2.8l-.29.06a.6.6,0,0,0-.48.58v.46l-1.02.19A8.454,8.454,0,0,1,8.982.268,8.6,8.6,0,0,1,12.352.958Z" fill="#32bedd"/><path d="M16.872,5.7l-1.09-.38a6.6,6.6,0,0,0-.72-1.16c-.02-.03-.04-.05-.05-.07a2.083,2.083,0,0,0,.72-.45A7.81,7.81,0,0,1,16.872,5.7Z" fill="#32bedd"/><path d="M10.072,11.908l2.54.56L8.672,14.1c-.02,0-.03.01-.05.01a.154.154,0,0,1-.15-.15V3.448a.154.154,0,0,1,.15-.15.09.09,0,0,1,.05.01l4.46,1.56-3.05.57a.565.565,0,0,0-.44.54v5.4A.537.537,0,0,0,10.072,11.908Z" fill="#fff"/><g><g id="e918f286-5032-4942-ad29-ea17e6f1cc90"><path d="M1.1,5.668l1.21-.23v6.55l-1.23-.27-.99-.22a.111.111,0,0,1-.09-.12v-5.4a.12.12,0,0,1,.09-.12Z" fill="#a67af4"/></g><g><g id="a47a99dd-4d47-4c70-8c42-c5ac274ce496"><g><path d="M10.072,11.908l2.54.56L8.672,14.1c-.02,0-.03.01-.05.01a.154.154,0,0,1-.15-.15V3.448a.154.154,0,0,1,.15-.15.09.09,0,0,1,.05.01l4.46,1.56-3.05.57a.565.565,0,0,0-.44.54v5.4A.537.537,0,0,0,10.072,11.908Z" fill="url(#b27f1ad0-7d11-4247-9da3-91bce6211f32)"/><path d="M8.586,3.3,2.878,4.378a.177.177,0,0,0-.14.175V12.68a.177.177,0,0,0,.137.174L8.581,14.1a.176.176,0,0,0,.21-.174V3.478A.175.175,0,0,0,8.619,3.3Z" fill="url(#b2f92112-4ca9-4b17-a019-c9f26c1a4a8f)"/></g></g><polygon points="5.948 4.921 5.948 12.483 7.934 12.814 7.934 4.564 5.948 4.921" fill="#b796f9" opacity="0.5"/><polygon points="3.509 5.329 3.509 11.954 5.238 12.317 5.238 5.031 3.509 5.329" fill="#b796f9" opacity="0.5"/></g></g></g><path d="M16,2.048a1.755,1.755,0,1,1-1.76-1.76A1.756,1.756,0,0,1,16,2.048Z" fill="#32bedd"/><circle cx="4.65" cy="15.973" r="1.759" fill="#32bedd"/></g><path d="M18,6.689v3.844a.222.222,0,0,1-.133.2l-.766.316-3.07,1.268-.011,0a.126.126,0,0,1-.038,0,.1.1,0,0,1-.1-.1V5.234a.1.1,0,0,1,.054-.088l0,0,.019,0a.031.031,0,0,1,.019,0,.055.055,0,0,1,.034.008l.011,0,.012,0L17.05,6.2l.8.282A.213.213,0,0,1,18,6.689Z" fill="#773adc"/><path d="M13.959,5.14l-3.8.715a.118.118,0,0,0-.093.117v5.409a.118.118,0,0,0,.091.116l3.8.831a.115.115,0,0,0,.137-.09.109.109,0,0,0,0-.026V5.256a.117.117,0,0,0-.115-.118A.082.082,0,0,0,13.959,5.14Z" fill="#a67af4"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,263 +0,0 @@
{
"id": "containerapp",
"title": "Container App",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "azure_rxbytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_rxbytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_rxbytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_rxbytes_total",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_txbytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_txbytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_txbytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_txbytes_total",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_restartcount_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_restartcount_maximum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_restartcount_minimum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_restartcount_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_replicas_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_replicas_maximum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_replicas_minimum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_replicas_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cpupercentage_average",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cpupercentage_maximum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cpupercentage_minimum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_cpupercentage_total",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_memorypercentage_average",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_memorypercentage_maximum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_memorypercentage_minimum",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_memorypercentage_total",
"unit": "Percent",
"type": "Gauge",
"description": ""
},
{
"name": "azure_usagenanocores_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_usagenanocores_maximum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_usagenanocores_minimum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_usagenanocores_total",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_workingsetbytes_average",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_workingsetbytes_maximum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_workingsetbytes_minimum",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_workingsetbytes_total",
"unit": "Bytes",
"type": "Gauge",
"description": ""
},
{
"name": "azure_coresquotaused_maximum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_coresquotaused_minimum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_totalcoresquotaused_average",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_totalcoresquotaused_maximum",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "azure_totalcoresquotaused_minimum",
"unit": "Count",
"type": "Gauge",
"description": ""
}
],
"logs": [
{
"name": "Resource ID",
"path": "resources.azure.resource.id",
"type": "string"
}
]
},
"telemetryCollectionStrategy": {
"azure": {
"resourceProvider": "Microsoft.App",
"resourceType": "containerApps",
"metrics": {},
"logs": {
"categoryGroups": ["ContainerAppConsoleLogs", "ContainerAppSystemLogs"]
}
}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "Container App Overview",
"description": "Overview of Container App metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -1,7 +0,0 @@
### Monitor Container Apps with SigNoz
Collect key Container App metrics and view them with an out of the box dashboard.
To collect logs, you need to make sure that you have chosen "Azure Monitor" as the logging option for Container's App Environment.
Note: This integration ingests logs for only "ContainerAppConsoleLogs" and "ContainerAppSystemLogs" diagnostic settings categories.

View File

@@ -27,7 +27,6 @@ var (
// Azure services.
AzureServiceStorageAccountsBlob = ServiceID{valuer.NewString("storageaccountsblob")}
AzureServiceCDNProfile = ServiceID{valuer.NewString("cdnprofile")}
AzureServiceContainerApp = ServiceID{valuer.NewString("containerapp")}
)
func (ServiceID) Enum() []any {
@@ -47,7 +46,6 @@ func (ServiceID) Enum() []any {
AWSServiceSQS,
AzureServiceStorageAccountsBlob,
AzureServiceCDNProfile,
AzureServiceContainerApp,
}
}
@@ -71,7 +69,6 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
CloudProviderTypeAzure: {
AzureServiceStorageAccountsBlob,
AzureServiceCDNProfile,
AzureServiceContainerApp,
},
}