Compare commits

...

3 Commits

Author SHA1 Message Date
amlannandy
3b2f57266a chore: fix bug in dashboards 2025-12-02 16:59:04 +07:00
amlannandy
0cea4cf5bc chore: fix failing test 2025-11-29 16:21:21 +07:00
amlannandy
526501dc68 chore: alert type selection page improvements 2025-11-29 16:21:21 +07:00
14 changed files with 729 additions and 145 deletions

View File

@@ -45,6 +45,7 @@
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"ALERT_TYPE_SELECTION": "SigNoz | Alert Type Selection",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",

View File

@@ -1,4 +1,5 @@
import ROUTES from 'constants/routes';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
@@ -190,6 +191,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'LIST_ALL_ALERT',
},
{
path: ROUTES.ALERT_TYPE_SELECTION,
exact: true,
component: AlertTypeSelectionPage,
isPrivate: true,
key: 'ALERT_TYPE_SELECTION',
},
{
path: ROUTES.ALERTS_NEW,
exact: true,

View File

@@ -51,4 +51,6 @@ export enum QueryParams {
thresholds = 'thresholds',
selectedExplorerView = 'selectedExplorerView',
variables = 'variables',
version = 'version',
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
}

View File

@@ -27,6 +27,7 @@ const ROUTES = {
ALERTS_NEW: '/alerts/new',
ALERT_HISTORY: '/alerts/history',
ALERT_OVERVIEW: '/alerts/overview',
ALERT_TYPE_SELECTION: '/alerts/type-selection',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/edit/:channelId',

View File

@@ -1,24 +1,12 @@
import ROUTES from 'constants/routes';
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import CreateAlertPage from 'pages/CreateAlert';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
.mockReturnValue({
@@ -66,13 +54,20 @@ describe('Alert rule documentation redirection', () => {
window.open = mockWindowOpen;
});
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
}),
}));
beforeEach(() => {
act(() => {
renderResult = render(
<CreateAlertPage />,
<AlertTypeSelectionPage />,
{},
{
initialRoute: ROUTES.ALERTS_NEW,
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
},
);
});
@@ -117,18 +112,20 @@ describe('Alert rule documentation redirection', () => {
expect(mockWindowOpen).toHaveBeenCalledTimes(alertTypeCount);
});
});
describe('Create alert page redirection', () => {
Object.values(AlertTypes)
.filter((type) => type !== AlertTypes.ANOMALY_BASED_ALERT)
.forEach((alertType) => {
it(`should redirect to create alert page for ${alertType} and "Check an example alert" should redirect to the correct documentation`, () => {
const { getByTestId, getByRole } = renderResult;
const alertTypeLink = getByTestId(`alert-type-card-${alertType}`);
act(() => {
fireEvent.click(alertTypeLink);
});
const { getByRole } = render(
<CreateAlertPage />,
{},
{
initialRoute: `${ROUTES.ALERTS_NEW}?alertType=${alertType}`,
},
);
act(() => {
fireEvent.click(

View File

@@ -0,0 +1,131 @@
import { render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
import CreateAlertRule from '../index';
jest.mock('container/FormAlertRules', () => ({
__esModule: true,
default: function MockFormAlertRules({
alertType,
}: {
alertType: AlertTypes;
}): JSX.Element {
return (
<div>
<h1>Form Alert Rules</h1>
<p>{alertType}</p>
</div>
);
},
AlertDetectionTypes: {
THRESHOLD_ALERT: 'threshold_rule',
ANOMALY_DETECTION_ALERT: 'anomaly_rule',
},
}));
jest.mock('container/CreateAlertV2', () => ({
__esModule: true,
default: function MockCreateAlertV2(): JSX.Element {
return <div>Create Alert V2</div>;
},
}));
const useCompositeQueryParamSpy = jest.spyOn(
useCompositeQueryParamHooks,
'useGetCompositeQueryParam',
);
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
const mockSetUrlQuery = jest.fn();
const mockToString = jest.fn();
const mockGetUrlQuery = jest.fn();
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
describe('CreateAlertRule', () => {
beforeEach(() => {
jest.clearAllMocks();
useUrlQuerySpy.mockReturnValue(({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery,
} as Partial<URLSearchParams>) as URLSearchParams);
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
});
it('should render v1 flow when showNewCreateAlertsPage is false', () => {
mockGetUrlQuery.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render v2 flow when showNewCreateAlertsPage is true', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showNewCreateAlertsPage) {
return 'true';
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
});
it('should render v1 flow when ruleType is anomaly_rule even if showNewCreateAlertsPage is true', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showNewCreateAlertsPage) {
return 'true';
}
if (key === QueryParams.ruleType) {
return 'anomaly_rule';
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
});
it('should use alertType from URL when provided', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.alertType) {
return AlertTypes.LOGS_BASED_ALERT;
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
});
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue({
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [
{
...initialQueriesMap.metrics.builder.queryData[0],
dataSource: DataSource.TRACES,
},
],
},
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
});
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
});
});

View File

@@ -1,133 +1,49 @@
import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { Form } from 'antd';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import CreateAlertV2 from 'container/CreateAlertV2';
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import history from 'lib/history';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import {
alertDefaults,
anamolyAlertDefaults,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
} from './defaults';
import SelectAlertType from './SelectAlertType';
import { ALERTS_VALUES_MAP } from './defaults';
function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState<AlertDef | null>(null);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
const version = queryParams.get('version');
const alertTypeFromParams =
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertTypes.ANOMALY_BASED_ALERT
: queryParams.get(QueryParams.alertType);
const { thresholds } = (location.state as {
thresholds: ThresholdProps[];
}) || {
thresholds: null,
};
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
if (!compositeQuery) {
return null;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
}
const [alertType, setAlertType] = useState<AlertTypes>(
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
);
const [formInstance] = Form.useForm();
const compositeQuery = useGetCompositeQueryParam();
const queryParams = useUrlQuery();
const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ);
switch (typ) {
case AlertTypes.LOGS_BASED_ALERT:
setInitValues(logAlertDefaults);
break;
case AlertTypes.TRACES_BASED_ALERT:
setInitValues(traceAlertDefaults);
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
setInitValues(exceptionAlertDefaults);
break;
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...anamolyAlertDefaults,
version: version || ENTITY_VERSION_V5,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V5,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}
queryParams.set(
QueryParams.alertType,
typ === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: typ,
);
if (
typ === AlertTypes.ANOMALY_BASED_ALERT ||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
}
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl, {
thresholds,
});
};
useEffect(() => {
if (alertType) {
onSelectType(alertType);
} else {
logEvent('Alert: New alert data source selection page visited', {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [alertType]);
if (!initValues) {
return (
<Row wrap={false}>
<SelectAlertType onSelect={onSelectType} />
</Row>
);
}
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
const version = queryParams.get(QueryParams.version);
const showNewCreateAlertsPageFlag =
queryParams.get('showNewCreateAlertsPage') === 'true';
queryParams.get(QueryParams.showNewCreateAlertsPage) === 'true';
const alertType = useMemo(() => {
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
return AlertTypes.ANOMALY_BASED_ALERT;
}
if (!alertTypeFromURL) {
const dataSource = compositeQuery?.builder.queryData?.[0]?.dataSource;
if (dataSource) {
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
}
return AlertTypes.METRICS_BASED_ALERT;
}
return alertTypeFromURL as AlertTypes;
}, [alertTypeFromURL, ruleTypeFromURL, compositeQuery?.builder.queryData]);
const initialAlertValue: AlertDef = useMemo(
() => ({
...ALERTS_VALUES_MAP[alertType],
version: version || ENTITY_VERSION_V5,
}),
[alertType, version],
);
if (
showNewCreateAlertsPageFlag &&
@@ -140,7 +56,7 @@ function CreateRules(): JSX.Element {
<FormAlertRules
alertType={alertType}
formInstance={formInstance}
initialValue={initValues}
initialValue={initialAlertValue}
ruleId=""
/>
);

View File

@@ -29,8 +29,8 @@ import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import useInterval from 'hooks/useInterval';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
@@ -50,6 +50,7 @@ const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { t } = useTranslation('common');
const { safeNavigate } = useSafeNavigate();
const { user } = useAppContext();
const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'],
@@ -112,7 +113,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
number: allAlertRules?.length,
layout: 'new',
});
history.push(`${ROUTES.ALERTS_NEW}?showNewCreateAlertsPage=true`);
params.set(QueryParams.showNewCreateAlertsPage, 'true');
safeNavigate(`${ROUTES.ALERT_TYPE_SELECTION}?${params.toString()}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -121,7 +123,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
number: allAlertRules?.length,
layout: 'classic',
});
history.push(ROUTES.ALERTS_NEW);
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -161,7 +163,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
if (openInNewTab) {
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
} else {
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
};
@@ -190,7 +192,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setTimeout(() => {
const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
params.set(QueryParams.ruleId, String(clonedAlert.id));
history.push(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
safeNavigate(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
}, 2000);
}
if (status === 'error') {

View File

@@ -0,0 +1,266 @@
import { act, renderHook } from '@testing-library/react';
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { createMemoryHistory, MemoryHistory } from 'history';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { getGraphType } from 'utils/getGraphType';
import useCreateAlerts from '../useCreateAlerts';
jest.mock('api/dashboard/substitute_vars');
jest.mock('api/v5/v5');
jest.mock('lib/history');
jest.mock('lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi');
jest.mock('utils/getGraphType');
jest.mock('lib/dashbaordVariables/getDashboardVariables');
const mockPrepareQueryRangePayloadV5 = prepareQueryRangePayloadV5 as jest.MockedFunction<
typeof prepareQueryRangePayloadV5
>;
const mockHistoryPush = history.push as jest.MockedFunction<
typeof history.push
>;
const mockMapQueryDataFromApi = mapQueryDataFromApi as jest.MockedFunction<
typeof mapQueryDataFromApi
>;
const mockGetGraphType = getGraphType as jest.MockedFunction<
typeof getGraphType
>;
const mockGetDashboardVariables = getDashboardVariables as jest.MockedFunction<
typeof getDashboardVariables
>;
const mockNotifications = {
error: jest.fn(),
};
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): { notifications: typeof mockNotifications } => ({
notifications: mockNotifications,
}),
}));
const mockSelectedDashboard: Partial<Dashboard> = {
id: 'dashboard-123',
data: {
title: 'Test Dashboard',
variables: {},
version: 'v5',
},
};
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { selectedDashboard: Partial<Dashboard> } => ({
selectedDashboard: mockSelectedDashboard,
}),
}));
const mockGlobalTime = {
selectedTime: {
startTime: 1713734400000,
endTime: 1713738000000,
},
};
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: (): typeof mockGlobalTime => mockGlobalTime,
}));
const mockMutate = jest.fn();
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useMutation: (): { mutate: VoidFunction } => ({ mutate: mockMutate }),
}));
const mockWidget: Partial<Widgets> = {
id: 'widget-123',
panelTypes: PANEL_TYPES.TIME_SERIES,
query: {
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
id: 'query-123',
},
timePreferance: 'GLOBAL_TIME',
};
const mockStore = configureStore([]);
describe('useCreateAlerts', () => {
let queryClient: QueryClient;
let historyInstance: MemoryHistory;
let store: MockStoreEnhanced;
beforeEach(() => {
jest.clearAllMocks();
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
historyInstance = createMemoryHistory();
store = mockStore({
globalTime: mockGlobalTime,
}) as MockStoreEnhanced;
mockGetGraphType.mockReturnValue(PANEL_TYPES.TIME_SERIES);
mockGetDashboardVariables.mockReturnValue({});
mockPrepareQueryRangePayloadV5.mockReturnValue(({
queryPayload: { test: 'payload' },
legendMap: {},
} as unknown) as ReturnType<typeof prepareQueryRangePayloadV5>);
mockMapQueryDataFromApi.mockReturnValue({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
id: 'query-123',
} as Query);
});
const renderUseCreateAlerts = (
widget?: Widgets,
caller?: string,
thresholds?: ThresholdProps[],
): ReturnType<typeof renderHook> =>
renderHook(() => useCreateAlerts(widget, caller, thresholds), {
wrapper: ({ children }) => (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<Router history={historyInstance}>{children}</Router>
</QueryClientProvider>
</Provider>
),
});
it('should return with no action when widget is not provided', () => {
const { result } = renderUseCreateAlerts();
act(() => {
(result.current as () => void)();
});
expect(mockPrepareQueryRangePayloadV5).not.toHaveBeenCalled();
expect(mockMutate).not.toHaveBeenCalled();
});
it('should prepare query payload and call mutation', () => {
const { result } = renderUseCreateAlerts(mockWidget as Widgets);
act(() => {
(result.current as () => void)();
});
expect(mockPrepareQueryRangePayloadV5).toHaveBeenCalledWith({
query: mockWidget.query,
globalSelectedInterval: mockGlobalTime.selectedTime,
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: mockWidget.timePreferance,
variables: {},
originalGraphType: mockWidget.panelTypes,
dynamicVariables: [],
});
expect(mockMutate).toHaveBeenCalledWith(
{ test: 'payload' },
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
});
const simulateMutationSuccess = (): void => {
const mutateCall = mockMutate.mock.calls[0];
if (mutateCall?.[1]?.onSuccess) {
act(() => {
mutateCall[1].onSuccess({
data: { compositeQuery: { test: 'query' } },
});
});
}
};
it('should navigate to alerts page with correct URL on success', () => {
const thresholds: Partial<ThresholdProps>[] = [
{
thresholdOperator: '>',
thresholdValue: 100,
thresholdUnit: 'ms',
thresholdLabel: 'High',
thresholdColor: '#ff0000',
setThresholds: jest.fn(),
},
];
const { result } = renderUseCreateAlerts(
mockWidget as Widgets,
undefined,
thresholds as ThresholdProps[],
);
act(() => {
(result.current as () => void)();
});
simulateMutationSuccess();
const [url, state] = mockHistoryPush.mock.calls[0];
expect(url).toContain(ROUTES.ALERTS_NEW);
expect(url).toContain(QueryParams.compositeQuery);
expect(url).toContain(QueryParams.panelTypes);
expect(url).toContain(ENTITY_VERSION_V5);
expect(state).toEqual({
thresholds: [
{
thresholdOperator: '>',
thresholdValue: 100,
thresholdUnit: 'ms',
thresholdLabel: 'High',
thresholdColor: '#ff0000',
},
],
});
});
it('should show error notification on mutation failure', () => {
const { result } = renderUseCreateAlerts(mockWidget as Widgets);
act(() => {
(result.current as () => void)();
});
const mutateCall = mockMutate.mock.calls[0];
if (mutateCall?.[1]?.onError) {
act(() => {
mutateCall[1].onError(new Error('Test error'));
});
}
expect(mockNotifications.error).toHaveBeenCalledWith({
message: SOMETHING_WENT_WRONG,
});
expect(mockHistoryPush).not.toHaveBeenCalled();
});
});

View File

@@ -11,6 +11,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import pick from 'lodash-es/pick';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useMemo } from 'react';
import { useMutation } from 'react-query';
@@ -20,6 +21,21 @@ import { IDashboardVariable, Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
function serializeThresholds(
thresholds?: ThresholdProps[],
): Partial<ThresholdProps>[] {
if (!thresholds) return [];
return thresholds?.map((threshold) =>
pick(threshold, [
'thresholdOperator',
'thresholdValue',
'thresholdUnit',
'thresholdLabel',
'thresholdColor',
]),
);
}
const useCreateAlerts = (
widget?: Widgets,
caller?: string,
@@ -84,7 +100,7 @@ const useCreateAlerts = (
}=${widget.panelTypes}&version=${ENTITY_VERSION_V5}`;
history.push(url, {
thresholds,
thresholds: serializeThresholds(thresholds),
});
},
onError: () => {

View File

@@ -0,0 +1,56 @@
import { Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import SelectAlertType from 'container/CreateAlertRule/SelectAlertType';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
function AlertTypeSelectionPage(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const queryParams = useUrlQuery();
useEffect(() => {
logEvent('Alert: New alert data source selection page visited', {});
}, []);
const handleSelectType = useCallback(
(type: AlertTypes): void => {
// For anamoly based alert, we need to set the ruleType to anomaly_rule
// and alertType to metrics_based_alert
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
queryParams.set(QueryParams.alertType, AlertTypes.METRICS_BASED_ALERT);
// For other alerts, we need to set the ruleType to threshold_rule
// and alertType to the selected type
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
queryParams.set(QueryParams.alertType, type);
}
const showNewCreateAlertsPageFlag = queryParams.get(
QueryParams.showNewCreateAlertsPage,
);
if (showNewCreateAlertsPageFlag === 'true') {
queryParams.set(QueryParams.showNewCreateAlertsPage, 'true');
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
},
[queryParams, safeNavigate],
);
return (
<Row wrap={false}>
<SelectAlertType onSelect={handleSelectType} />
</Row>
);
}
export default AlertTypeSelectionPage;

View File

@@ -0,0 +1,184 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as navigateHooks from 'hooks/useSafeNavigate';
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
import * as appHooks from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import AlertTypeSelection from '../AlertTypeSelection';
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
const mockSetUrlQuery = jest.fn();
const mockSafeNavigate = jest.fn();
const mockToString = jest.fn();
const mockGetUrlQuery = jest.fn();
describe('AlertTypeSelection', () => {
beforeEach(() => {
jest.clearAllMocks();
useAppContextSpy.mockReturnValue(getAppContextMockState());
useUrlQuerySpy.mockReturnValue(({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery,
} as Partial<URLSearchParams>) as URLSearchParams);
useSafeNavigateSpy.mockReturnValue({
safeNavigate: mockSafeNavigate,
});
});
it('should render all alert type options when anomaly detection is enabled', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<AlertTypeSelection />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
});
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
render(<AlertTypeSelection />);
expect(screen.getByText('metric_based_alert')).toBeInTheDocument();
expect(screen.getByText('log_based_alert')).toBeInTheDocument();
expect(screen.getByText('traces_based_alert')).toBeInTheDocument();
expect(screen.getByText('exceptions_based_alert')).toBeInTheDocument();
expect(screen.queryByText('anomaly_based_alert')).not.toBeInTheDocument();
});
it('should navigate to metrics based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to anomaly based alert with correct params', () => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState({}),
featureFlags: [
{
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
});
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('anomaly_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to log based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('log_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.LOGS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to traces based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('traces_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.TRACES_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to exceptions based alert with correct params', () => {
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('exceptions_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(2);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.EXCEPTIONS_BASED_ALERT,
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to new create alerts page with correct params if showNewCreateAlertsPage is true', () => {
useUrlQuerySpy.mockReturnValue(({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery.mockReturnValue('true'),
} as Partial<URLSearchParams>) as URLSearchParams);
render(<AlertTypeSelection />);
fireEvent.click(screen.getByText('metric_based_alert'));
expect(mockSetUrlQuery).toHaveBeenCalledTimes(3);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.ruleType,
AlertDetectionTypes.THRESHOLD_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.alertType,
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.showNewCreateAlertsPage,
'true',
);
expect(mockSafeNavigate).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,3 @@
import AlertTypeSelectionPage from './AlertTypeSelection';
export default AlertTypeSelectionPage;

View File

@@ -126,4 +126,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
METER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
ALERT_TYPE_SELECTION: ['ADMIN', 'EDITOR'],
};