mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
chore: edit alerts api integration (#9210)
This commit is contained in:
parent
9ffe0d8143
commit
cbb24d9a34
26
frontend/src/api/alerts/updateAlertRule.ts
Normal file
26
frontend/src/api/alerts/updateAlertRule.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export interface UpdateAlertRuleResponse {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const updateAlertRule = async (
|
||||
id: string,
|
||||
postableAlertRule: PostableAlertRuleV2,
|
||||
): Promise<SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse> => {
|
||||
const response = await axios.put(`/rules/${id}`, {
|
||||
...postableAlertRule,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateAlertRule;
|
||||
@ -6,9 +6,7 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
export interface CreateRoutingPolicyBody {
|
||||
name: string;
|
||||
expression: string;
|
||||
actions: {
|
||||
channels: string[];
|
||||
};
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@ -23,7 +21,7 @@ const createRoutingPolicy = async (
|
||||
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post(`/notification-policy`, props);
|
||||
const response = await axios.post(`/route_policies`, props);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
|
||||
@ -14,9 +14,7 @@ const deleteRoutingPolicy = async (
|
||||
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.delete(
|
||||
`/notification-policy/${routingPolicyId}`,
|
||||
);
|
||||
const response = await axios.delete(`/route_policies/${routingPolicyId}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
|
||||
@ -25,7 +25,7 @@ export const getRoutingPolicies = async (
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2> => {
|
||||
try {
|
||||
const response = await axios.get('/notification-policy', {
|
||||
const response = await axios.get('/route_policies', {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
@ -6,9 +6,7 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
export interface UpdateRoutingPolicyBody {
|
||||
name: string;
|
||||
expression: string;
|
||||
actions: {
|
||||
channels: string[];
|
||||
};
|
||||
description: string;
|
||||
}
|
||||
|
||||
@ -24,7 +22,7 @@ const updateRoutingPolicy = async (
|
||||
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.put(`/notification-policy/${id}`, {
|
||||
const response = await axios.put(`/route_policies/${id}`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import { showNewCreateAlertsPage } from 'container/CreateAlertV2/utils';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
@ -127,7 +126,8 @@ function CreateRules(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const showNewCreateAlertsPageFlag = showNewCreateAlertsPage();
|
||||
const showNewCreateAlertsPageFlag =
|
||||
queryParams.get('showNewCreateAlertsPage') === 'true';
|
||||
|
||||
if (
|
||||
showNewCreateAlertsPageFlag &&
|
||||
|
||||
@ -5,6 +5,7 @@ import { Button, Select, Tooltip, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
@ -46,6 +47,19 @@ function AlertThreshold({
|
||||
|
||||
const queryNames = getQueryNames(currentQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
queryNames.length > 0 &&
|
||||
!queryNames.some((query) => query.value === thresholdState.selectedQuery)
|
||||
) {
|
||||
setThresholdState({
|
||||
type: 'SET_SELECTED_QUERY',
|
||||
payload: queryNames[0].value,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryNames, thresholdState.selectedQuery]);
|
||||
|
||||
const selectedCategory = getCategoryByOptionId(alertState.yAxisUnit || '');
|
||||
const categorySelectOptions = getCategorySelectOptionByName(
|
||||
selectedCategory || '',
|
||||
|
||||
@ -108,6 +108,11 @@ jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
]),
|
||||
}));
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
showCondensedLayout: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
const TEST_STRINGS = {
|
||||
ADD_THRESHOLD: 'Add Threshold',
|
||||
AT_LEAST_ONCE: 'AT LEAST ONCE',
|
||||
@ -204,11 +209,11 @@ describe('AlertThreshold', () => {
|
||||
|
||||
// First addition should add WARNING threshold
|
||||
fireEvent.click(addButton);
|
||||
expect(screen.getByText('WARNING')).toBeInTheDocument();
|
||||
expect(screen.getByText('warning')).toBeInTheDocument();
|
||||
|
||||
// Second addition should add INFO threshold
|
||||
fireEvent.click(addButton);
|
||||
expect(screen.getByText('INFO')).toBeInTheDocument();
|
||||
expect(screen.getByText('info')).toBeInTheDocument();
|
||||
|
||||
// Third addition should add random threshold
|
||||
fireEvent.click(addButton);
|
||||
@ -280,7 +285,7 @@ describe('AlertThreshold', () => {
|
||||
renderAlertThreshold();
|
||||
|
||||
// Should have initial critical threshold
|
||||
expect(screen.getByText('CRITICAL')).toBeInTheDocument();
|
||||
expect(screen.getByText('critical')).toBeInTheDocument();
|
||||
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
|
||||
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
|
||||
});
|
||||
|
||||
@ -494,10 +494,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-threshold-btn {
|
||||
.add-threshold-btn,
|
||||
.ant-btn.add-threshold-btn {
|
||||
border: 1px dashed var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-300);
|
||||
background-color: transparent;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
@ -506,7 +513,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.condensed-evaluation-settings-container {
|
||||
.ant-btn {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import './styles.scss';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
@ -8,7 +9,7 @@ import { useCreateAlertState } from '../context';
|
||||
import LabelsInput from './LabelsInput';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState } = useCreateAlertState();
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
@ -34,10 +35,14 @@ function CreateAlertHeader(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="alert-header">
|
||||
<div
|
||||
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
|
||||
>
|
||||
{!isEditMode && (
|
||||
<div className="alert-header__tab-bar">
|
||||
<div className="alert-header__tab">New Alert Rule</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="alert-header__content">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
|
||||
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
|
||||
import * as useUpdateAlertRuleHook from '../../../../hooks/alerts/useUpdateAlertRule';
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import CreateAlertHeader from '../CreateAlertHeader';
|
||||
|
||||
@ -15,6 +18,10 @@ jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useUpdateAlertRuleHook, 'useUpdateAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
@ -37,6 +44,8 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const ENTER_ALERT_RULE_NAME_PLACEHOLDER = 'Enter alert rule name';
|
||||
|
||||
const renderCreateAlertHeader = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
@ -52,7 +61,9 @@ describe('CreateAlertHeader', () => {
|
||||
|
||||
it('renders name input with placeholder', () => {
|
||||
renderCreateAlertHeader();
|
||||
const nameInput = screen.getByPlaceholderText('Enter alert rule name');
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
ENTER_ALERT_RULE_NAME_PLACEHOLDER,
|
||||
);
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -63,10 +74,30 @@ describe('CreateAlertHeader', () => {
|
||||
|
||||
it('updates name when typing in name input', () => {
|
||||
renderCreateAlertHeader();
|
||||
const nameInput = screen.getByPlaceholderText('Enter alert rule name');
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
ENTER_ALERT_RULE_NAME_PLACEHOLDER,
|
||||
);
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'Test Alert' } });
|
||||
|
||||
expect(nameInput).toHaveValue('Test Alert');
|
||||
});
|
||||
|
||||
it('renders the header with title when isEditMode is true', () => {
|
||||
render(
|
||||
<CreateAlertProvider
|
||||
isEditMode
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
initialAlertState={getCreateAlertLocalStateFromAlertDef(
|
||||
defaultPostableAlertRuleV2,
|
||||
)}
|
||||
>
|
||||
<CreateAlertHeader />
|
||||
</CreateAlertProvider>,
|
||||
);
|
||||
expect(screen.queryByText('New Alert Rule')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText(ENTER_ALERT_RULE_NAME_PLACEHOLDER),
|
||||
).toHaveValue('TEST_ALERT');
|
||||
});
|
||||
});
|
||||
|
||||
@ -175,10 +175,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.edit-alert-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-alert-header .alert-header__content {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.labels-input {
|
||||
&__add-button {
|
||||
color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
|
||||
@ -2,7 +2,7 @@ import './styles.scss';
|
||||
|
||||
import { Switch, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { IAdvancedOptionItemProps } from '../types';
|
||||
|
||||
@ -12,9 +12,14 @@ function AdvancedOptionItem({
|
||||
input,
|
||||
tooltipText,
|
||||
onToggle,
|
||||
defaultShowInput,
|
||||
}: IAdvancedOptionItemProps): JSX.Element {
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowInput(defaultShowInput);
|
||||
}, [defaultShowInput]);
|
||||
|
||||
const handleOnToggle = (): void => {
|
||||
onToggle?.();
|
||||
setShowInput((currentShowInput) => !currentShowInput);
|
||||
@ -42,7 +47,7 @@ function AdvancedOptionItem({
|
||||
>
|
||||
{input}
|
||||
</div>
|
||||
<Switch onChange={handleOnToggle} />
|
||||
<Switch onChange={handleOnToggle} checked={showInput} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -42,6 +42,7 @@ function AdvancedOptions(): JSX.Element {
|
||||
payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled,
|
||||
})
|
||||
}
|
||||
defaultShowInput={advancedOptions.sendNotificationIfDataIsMissing.enabled}
|
||||
/>
|
||||
<AdvancedOptionItem
|
||||
title="Minimum data required"
|
||||
@ -72,6 +73,7 @@ function AdvancedOptions(): JSX.Element {
|
||||
payload: !advancedOptions.enforceMinimumDatapoints.enabled,
|
||||
})
|
||||
}
|
||||
defaultShowInput={advancedOptions.enforceMinimumDatapoints.enabled}
|
||||
/>
|
||||
{/* TODO: Add back when the functionality is implemented */}
|
||||
{/* <AdvancedOptionItem
|
||||
|
||||
@ -33,6 +33,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -50,6 +51,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -65,6 +67,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -88,6 +91,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -117,6 +121,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -146,6 +151,7 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -160,9 +166,24 @@ describe('AdvancedOptionItem', () => {
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
tooltipText="mock tooltip text"
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
const tooltipIcon = screen.getByTestId('tooltip-icon');
|
||||
expect(tooltipIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show input when defaultShowInput is true', () => {
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput
|
||||
/>,
|
||||
);
|
||||
const inputElement = screen.getByTestId(TEST_INPUT_TEST_ID);
|
||||
expect(inputElement).toBeInTheDocument();
|
||||
expect(inputElement).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,6 +6,11 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import EvaluationSettings from '../EvaluationSettings';
|
||||
import { createMockAlertContextState } from './testUtils';
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
showCondensedLayout: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
const mockSetEvaluationWindow = jest.fn();
|
||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
|
||||
@ -31,6 +31,9 @@ export const createMockAlertContextState = (
|
||||
isCreatingAlertRule: false,
|
||||
isTestingAlertRule: false,
|
||||
createAlertRule: jest.fn(),
|
||||
isUpdatingAlertRule: false,
|
||||
updateAlertRule: jest.fn(),
|
||||
isEditMode: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ export interface IAdvancedOptionItemProps {
|
||||
input: JSX.Element;
|
||||
tooltipText?: string;
|
||||
onToggle?: () => void;
|
||||
defaultShowInput: boolean;
|
||||
}
|
||||
|
||||
export enum RollingWindowTimeframes {
|
||||
|
||||
@ -26,11 +26,17 @@ function Footer(): JSX.Element {
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
} = useCreateAlertState();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (): void => discardAlertRule();
|
||||
const handleDiscard = (): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts');
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
() =>
|
||||
@ -99,6 +105,17 @@ function Footer(): JSX.Element {
|
||||
notificationSettings,
|
||||
query: currentQuery,
|
||||
});
|
||||
if (isEditMode) {
|
||||
updateAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule created successfully');
|
||||
@ -108,6 +125,7 @@ function Footer(): JSX.Element {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
alertType,
|
||||
basicAlertState,
|
||||
@ -116,16 +134,22 @@ function Footer(): JSX.Element {
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
isEditMode,
|
||||
updateAlertRule,
|
||||
createAlertRule,
|
||||
safeNavigate,
|
||||
]);
|
||||
|
||||
const disableButtons =
|
||||
isCreatingAlertRule || isTestingAlertRule || !!alertValidationMessage;
|
||||
isCreatingAlertRule || isTestingAlertRule || isUpdatingAlertRule;
|
||||
|
||||
const saveAlertButton = useMemo(() => {
|
||||
let button = (
|
||||
<Button type="primary" onClick={handleSaveAlert} disabled={disableButtons}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveAlert}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
>
|
||||
<Check size={14} />
|
||||
<Typography.Text>Save Alert Rule</Typography.Text>
|
||||
</Button>
|
||||
@ -141,7 +165,7 @@ function Footer(): JSX.Element {
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleTestNotification}
|
||||
disabled={disableButtons}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
>
|
||||
<Send size={14} />
|
||||
<Typography.Text>Test Notification</Typography.Text>
|
||||
@ -155,7 +179,7 @@ function Footer(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="create-alert-v2-footer">
|
||||
<Button type="text" onClick={handleDiscard} disabled={disableButtons}>
|
||||
<Button type="default" onClick={handleDiscard} disabled={disableButtons}>
|
||||
<X size={14} /> Discard
|
||||
</Button>
|
||||
<div className="button-group">
|
||||
|
||||
@ -0,0 +1,248 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
|
||||
import * as createAlertState from '../../context';
|
||||
import Footer from '../Footer';
|
||||
|
||||
// Mock the hooks used by Footer component
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockCreateAlertRule = jest.fn();
|
||||
const mockTestAlertRule = jest.fn();
|
||||
const mockUpdateAlertRule = jest.fn();
|
||||
const mockDiscardAlertRule = jest.fn();
|
||||
|
||||
// Import the mocked hooks
|
||||
const { useQueryBuilder } = jest.requireMock(
|
||||
'hooks/queryBuilder/useQueryBuilder',
|
||||
);
|
||||
const { useSafeNavigate } = jest.requireMock('hooks/useSafeNavigate');
|
||||
|
||||
const mockAlertContextState = createMockAlertContextState({
|
||||
createAlertRule: mockCreateAlertRule,
|
||||
testAlertRule: mockTestAlertRule,
|
||||
updateAlertRule: mockUpdateAlertRule,
|
||||
discardAlertRule: mockDiscardAlertRule,
|
||||
alertState: {
|
||||
name: 'Test Alert',
|
||||
labels: {},
|
||||
yAxisUnit: undefined,
|
||||
},
|
||||
thresholdState: {
|
||||
selectedQuery: 'A',
|
||||
operator: AlertThresholdOperator.ABOVE_BELOW,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
evaluationWindow: '5m0s',
|
||||
algorithm: 'standard',
|
||||
seasonality: 'hourly',
|
||||
thresholds: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'CRITICAL',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
unit: '',
|
||||
channels: ['test-channel'],
|
||||
color: '#ff0000',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
|
||||
const SAVE_ALERT_RULE_TEXT = 'Save Alert Rule';
|
||||
const TEST_NOTIFICATION_TEXT = 'Test Notification';
|
||||
const DISCARD_TEXT = 'Discard';
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
useQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
queryType: 'builder',
|
||||
},
|
||||
});
|
||||
|
||||
useSafeNavigate.mockReturnValue({
|
||||
safeNavigate: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discard action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update alert rule action works correctly', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when creating alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when updating alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when testing alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('create and test buttons are disabled when alert name is missing', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
alertState: {
|
||||
...mockAlertContextState.alertState,
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('create and test buttons are disabled when notifcation channels are missing and routing policies are disabled', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
notificationSettings: {
|
||||
...mockAlertContextState.notificationSettings,
|
||||
routingPolicies: false,
|
||||
},
|
||||
thresholdState: {
|
||||
...mockAlertContextState.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...mockAlertContextState.thresholdState.thresholds[0],
|
||||
channels: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('buttons are enabled even with no notification channels when routing policies are enabled', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
notificationSettings: {
|
||||
...mockAlertContextState.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
thresholdState: {
|
||||
...mockAlertContextState.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...mockAlertContextState.thresholdState.thresholds[0],
|
||||
channels: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,524 @@
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from 'container/CreateAlertV2/context/constants';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { BuildCreateAlertRulePayloadArgs } from '../types';
|
||||
import {
|
||||
buildCreateThresholdAlertRulePayload,
|
||||
getAlertOnAbsentProps,
|
||||
getEnforceMinimumDatapointsProps,
|
||||
getEvaluationProps,
|
||||
getFormattedTimeValue,
|
||||
getNotificationSettingsProps,
|
||||
validateCreateAlertState,
|
||||
} from '../utils';
|
||||
|
||||
describe('Footer utils', () => {
|
||||
describe('getFormattedTimeValue', () => {
|
||||
it('for 60 seconds', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.SECONDS)).toBe('60s');
|
||||
});
|
||||
it('for 60 minutes', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.MINUTES)).toBe('60m');
|
||||
});
|
||||
it('for 60 hours', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.HOURS)).toBe('60h');
|
||||
});
|
||||
it('for 60 days', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.DAYS)).toBe('60d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCreateAlertState', () => {
|
||||
const args: BuildCreateAlertRulePayloadArgs = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
basicAlertState: INITIAL_ALERT_STATE,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
query: initialQueriesMap.metrics,
|
||||
};
|
||||
|
||||
it('when alert name is not provided', () => {
|
||||
expect(validateCreateAlertState(args)).toBeDefined();
|
||||
expect(validateCreateAlertState(args)).toBe('Please enter an alert name');
|
||||
});
|
||||
|
||||
it('when threshold label is not provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
label: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeDefined();
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please enter a label for each threshold',
|
||||
);
|
||||
});
|
||||
|
||||
it('when threshold channels are not provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeDefined();
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please select at least one channel for each threshold or enable routing policies',
|
||||
);
|
||||
});
|
||||
|
||||
it('when threshold channels are not provided but routing policies are enabled', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
notificationSettings: {
|
||||
...args.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
|
||||
it('when threshold channels are provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
channels: ['test channel'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsProps', () => {
|
||||
it('when initial notification settings are provided', () => {
|
||||
const notificationSettings = INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renotification is enabled', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
reNotification: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: ['firing'],
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: true,
|
||||
interval: '1m',
|
||||
alertStates: ['firing'],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('routing policies are enabled', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
routingPolicies: true,
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('group by notifications are provided', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
multipleNotifications: ['test group'],
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: ['test group'],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertOnAbsentProps', () => {
|
||||
it('when alert on absent is disabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: false,
|
||||
toleranceLimit: 0,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getAlertOnAbsentProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alertOnAbsent: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('when alert on absent is enabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: true,
|
||||
toleranceLimit: 13,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getAlertOnAbsentProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alertOnAbsent: true,
|
||||
absentFor: 13,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnforceMinimumDatapointsProps', () => {
|
||||
it('when enforce minimum datapoints is disabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: false,
|
||||
minimumDatapoints: 0,
|
||||
},
|
||||
};
|
||||
const props = getEnforceMinimumDatapointsProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
requireMinPoints: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('when enforce minimum datapoints is enabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: true,
|
||||
minimumDatapoints: 12,
|
||||
},
|
||||
};
|
||||
const props = getEnforceMinimumDatapointsProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
requireMinPoints: true,
|
||||
requiredNumPoints: 12,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEvaluationProps', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'default',
|
||||
default: {
|
||||
value: 12,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('for rolling window with non-custom timeframe', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '12m',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for rolling window with custom timeframe', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'rolling',
|
||||
timeframe: 'custom',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '13',
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '13m',
|
||||
frequency: '12m',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current hour', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '14',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'hourly', minute: 14 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current day', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
time: '15:43:00',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'daily', hour: 15, minute: 43 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current month', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentMonth',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '17',
|
||||
timezone: 'UTC',
|
||||
time: '16:34:00',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'monthly', day: 17, hour: 16, minute: 34 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCreateThresholdAlertRulePayload', () => {
|
||||
const mockCreateAlertContextState = createMockAlertContextState();
|
||||
const INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS: BuildCreateAlertRulePayloadArgs = {
|
||||
basicAlertState: mockCreateAlertContextState.alertState,
|
||||
thresholdState: mockCreateAlertContextState.thresholdState,
|
||||
advancedOptions: mockCreateAlertContextState.advancedOptions,
|
||||
evaluationWindow: mockCreateAlertContextState.evaluationWindow,
|
||||
notificationSettings: mockCreateAlertContextState.notificationSettings,
|
||||
query: initialQueriesMap.metrics,
|
||||
alertType: mockCreateAlertContextState.alertType,
|
||||
};
|
||||
|
||||
it('verify buildCreateThresholdAlertRulePayload', () => {
|
||||
const props = buildCreateThresholdAlertRulePayload(
|
||||
INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
|
||||
);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alert: '',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
annotations: {
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
summary:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
},
|
||||
condition: {
|
||||
alertOnAbsent: false,
|
||||
compositeQuery: {
|
||||
builderQueries: undefined,
|
||||
chQueries: undefined,
|
||||
panelType: 'graph',
|
||||
promQueries: undefined,
|
||||
queries: [
|
||||
{
|
||||
spec: {
|
||||
aggregations: [
|
||||
{
|
||||
metricName: '',
|
||||
reduceTo: undefined,
|
||||
spaceAggregation: 'sum',
|
||||
temporality: undefined,
|
||||
timeAggregation: 'count',
|
||||
},
|
||||
],
|
||||
disabled: false,
|
||||
filter: {
|
||||
expression: '',
|
||||
},
|
||||
functions: undefined,
|
||||
groupBy: undefined,
|
||||
having: undefined,
|
||||
legend: undefined,
|
||||
limit: undefined,
|
||||
name: 'A',
|
||||
offset: undefined,
|
||||
order: undefined,
|
||||
selectFields: undefined,
|
||||
signal: 'metrics',
|
||||
source: '',
|
||||
stepInterval: null,
|
||||
},
|
||||
type: 'builder_query',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
},
|
||||
requireMinPoints: false,
|
||||
selectedQueryName: 'A',
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
channels: [],
|
||||
matchType: '1',
|
||||
name: 'critical',
|
||||
op: '1',
|
||||
target: 0,
|
||||
targetUnit: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
evaluation: {
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m',
|
||||
},
|
||||
},
|
||||
labels: {},
|
||||
notificationSettings: {
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
},
|
||||
ruleType: 'threshold_rule',
|
||||
schemaVersion: 'v2alpha1',
|
||||
source: 'http://localhost/',
|
||||
version: 'v5',
|
||||
});
|
||||
});
|
||||
|
||||
it('verify for promql query type', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
|
||||
query: {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS.query,
|
||||
queryType: EQueryType.PROM,
|
||||
},
|
||||
};
|
||||
const props = buildCreateThresholdAlertRulePayload(currentArgs);
|
||||
expect(props).toBeDefined();
|
||||
expect(props.condition.compositeQuery.queryType).toBe('promql');
|
||||
expect(props.ruleType).toBe('promql_rule');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -17,7 +17,7 @@ import {
|
||||
import { BuildCreateAlertRulePayloadArgs } from './types';
|
||||
|
||||
// Get formatted time/unit pairs for create alert api payload
|
||||
function getFormattedTimeValue(timeValue: number, unit: string): string {
|
||||
export function getFormattedTimeValue(timeValue: number, unit: string): string {
|
||||
const unitMap: Record<string, string> = {
|
||||
[UniversalYAxisUnit.SECONDS]: 's',
|
||||
[UniversalYAxisUnit.MINUTES]: 'm',
|
||||
@ -57,19 +57,17 @@ export function getNotificationSettingsProps(
|
||||
notificationSettings: NotificationSettingsState,
|
||||
): PostableAlertRuleV2['notificationSettings'] {
|
||||
const notificationSettingsProps: PostableAlertRuleV2['notificationSettings'] = {
|
||||
notificationGroupBy: notificationSettings.multipleNotifications || [],
|
||||
alertStates: notificationSettings.reNotification.enabled
|
||||
? notificationSettings.reNotification.conditions
|
||||
: [],
|
||||
notificationPolicy: notificationSettings.routingPolicies,
|
||||
};
|
||||
|
||||
if (notificationSettings.reNotification.enabled) {
|
||||
notificationSettingsProps.renotify = getFormattedTimeValue(
|
||||
groupBy: notificationSettings.multipleNotifications || [],
|
||||
usePolicy: notificationSettings.routingPolicies,
|
||||
renotify: {
|
||||
enabled: notificationSettings.reNotification.enabled,
|
||||
interval: getFormattedTimeValue(
|
||||
notificationSettings.reNotification.value,
|
||||
notificationSettings.reNotification.unit,
|
||||
);
|
||||
}
|
||||
),
|
||||
alertStates: notificationSettings.reNotification.conditions,
|
||||
},
|
||||
};
|
||||
|
||||
return notificationSettingsProps;
|
||||
}
|
||||
|
||||
@ -103,6 +103,7 @@ function NotificationSettings(): JSX.Element {
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultShowInput={notificationSettings.reNotification.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -24,6 +24,11 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
showCondensedLayout: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
const initialNotificationSettings = createMockAlertContextState()
|
||||
.notificationSettings;
|
||||
const mockSetNotificationSettings = jest.fn();
|
||||
|
||||
@ -132,7 +132,7 @@
|
||||
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
background-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
358
frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx
Normal file
358
frontend/src/container/CreateAlertV2/__tests__/utils.test.tsx
Normal file
@ -0,0 +1,358 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import { defaultPostableAlertRuleV2 } from '../constants';
|
||||
import { INITIAL_ALERT_STATE } from '../context/constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from '../context/types';
|
||||
import {
|
||||
getAdvancedOptionsStateFromAlertDef,
|
||||
getColorForThreshold,
|
||||
getCreateAlertLocalStateFromAlertDef,
|
||||
getEvaluationWindowStateFromAlertDef,
|
||||
getNotificationSettingsStateFromAlertDef,
|
||||
getThresholdStateFromAlertDef,
|
||||
parseGoTime,
|
||||
} from '../utils';
|
||||
|
||||
describe('CreateAlertV2 utils', () => {
|
||||
describe('getColorForThreshold', () => {
|
||||
it('should return the correct color for the pre-defined threshold', () => {
|
||||
expect(getColorForThreshold('critical')).toBe(Color.BG_SAKURA_500);
|
||||
expect(getColorForThreshold('warning')).toBe(Color.BG_AMBER_500);
|
||||
expect(getColorForThreshold('info')).toBe(Color.BG_ROBIN_500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGoTime', () => {
|
||||
it('should return the correct time and unit for the given input', () => {
|
||||
expect(parseGoTime('1h')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.HOURS,
|
||||
});
|
||||
expect(parseGoTime('1m')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
});
|
||||
expect(parseGoTime('1s')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.SECONDS,
|
||||
});
|
||||
expect(parseGoTime('1h0m')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.HOURS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEvaluationWindowStateFromAlertDef', () => {
|
||||
it('for rolling window with non-custom timeframe', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
});
|
||||
});
|
||||
|
||||
it('for rolling window with custom timeframe', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '13m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'rolling',
|
||||
timeframe: 'custom',
|
||||
startingAt: {
|
||||
number: '13',
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current hour', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'hourly',
|
||||
minute: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
startingAt: {
|
||||
number: '14',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current day', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'daily',
|
||||
hour: 14,
|
||||
minute: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
startingAt: {
|
||||
time: '14:15:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current month', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'monthly',
|
||||
day: 12,
|
||||
hour: 16,
|
||||
minute: 34,
|
||||
},
|
||||
timezone: 'UTC',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentMonth',
|
||||
startingAt: {
|
||||
number: '12',
|
||||
timezone: 'UTC',
|
||||
time: '16:34:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsStateFromAlertDef', () => {
|
||||
it('should return the correct notification settings state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
notificationSettings: {
|
||||
groupBy: ['email'],
|
||||
renotify: {
|
||||
enabled: true,
|
||||
interval: '1m0s',
|
||||
alertStates: ['firing'],
|
||||
},
|
||||
usePolicy: true,
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
multipleNotifications: ['email'],
|
||||
reNotification: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: ['firing'],
|
||||
},
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
routingPolicies: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('when renotification is not provided', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
notificationSettings: {
|
||||
groupBy: ['email'],
|
||||
usePolicy: false,
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
multipleNotifications: ['email'],
|
||||
reNotification: {
|
||||
enabled: false,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdvancedOptionsStateFromAlertDef', () => {
|
||||
it('should return the correct advanced options state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
compositeQuery: {
|
||||
...defaultPostableAlertRuleV2.condition.compositeQuery,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
requiredNumPoints: 13,
|
||||
requireMinPoints: true,
|
||||
alertOnAbsent: true,
|
||||
absentFor: 12,
|
||||
},
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
spec: {
|
||||
frequency: '1m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getAdvancedOptionsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: true,
|
||||
toleranceLimit: 12,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: true,
|
||||
minimumDatapoints: 13,
|
||||
},
|
||||
evaluationCadence: {
|
||||
mode: 'default',
|
||||
default: {
|
||||
value: 1,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
annotations: {
|
||||
summary: 'test summary',
|
||||
description: 'test description',
|
||||
},
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: ['email'],
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
op: AlertThresholdOperator.IS_ABOVE,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectedQueryName: 'test',
|
||||
},
|
||||
};
|
||||
const props = getThresholdStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
selectedQuery: 'test',
|
||||
operator: AlertThresholdOperator.IS_ABOVE,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
thresholds: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
label: 'critical',
|
||||
thresholdValue: 1,
|
||||
recoveryThresholdValue: null,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
color: Color.BG_SAKURA_500,
|
||||
channels: ['email'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCreateAlertLocalStateFromAlertDef', () => {
|
||||
it('should return the correct create alert local state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
annotations: {
|
||||
summary: 'test summary',
|
||||
description: 'test description',
|
||||
},
|
||||
alert: 'test-alert',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
team: 'test-team',
|
||||
},
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
compositeQuery: {
|
||||
...defaultPostableAlertRuleV2.condition.compositeQuery,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getCreateAlertLocalStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
basicAlertState: {
|
||||
...INITIAL_ALERT_STATE,
|
||||
name: 'test-alert',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
team: 'test-team',
|
||||
},
|
||||
yAxisUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
// as we have already verified these utils in their respective tests
|
||||
thresholdState: expect.any(Object),
|
||||
advancedOptionsState: expect.any(Object),
|
||||
evaluationWindowState: expect.any(Object),
|
||||
notificationSettingsState: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
74
frontend/src/container/CreateAlertV2/constants.ts
Normal file
74
frontend/src/container/CreateAlertV2/constants.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
const defaultAnnotations = {
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
summary:
|
||||
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}',
|
||||
};
|
||||
|
||||
const defaultNotificationSettings: PostableAlertRuleV2['notificationSettings'] = {
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '1m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
};
|
||||
|
||||
const defaultEvaluation: PostableAlertRuleV2['evaluation'] = {
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m',
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultPostableAlertRuleV2: PostableAlertRuleV2 = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V5,
|
||||
schemaVersion: NEW_ALERT_SCHEMA_VERSION,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: initialQueryBuilderFormValuesMap.metrics,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
A: {
|
||||
name: 'A',
|
||||
query: ``,
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: undefined,
|
||||
},
|
||||
selectedQueryName: 'A',
|
||||
alertOnAbsent: true,
|
||||
absentFor: 10,
|
||||
requireMinPoints: false,
|
||||
requiredNumPoints: 0,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
notificationSettings: defaultNotificationSettings,
|
||||
alert: 'TEST_ALERT',
|
||||
evaluation: defaultEvaluation,
|
||||
};
|
||||
@ -27,7 +27,7 @@ export const INITIAL_ALERT_STATE: AlertState = {
|
||||
|
||||
export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'CRITICAL',
|
||||
label: 'critical',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
unit: '',
|
||||
@ -37,7 +37,7 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||
|
||||
export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'WARNING',
|
||||
label: 'warning',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
unit: '',
|
||||
@ -47,7 +47,7 @@ export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||
|
||||
export const INITIAL_INFO_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'INFO',
|
||||
label: 'info',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
unit: '',
|
||||
@ -177,7 +177,7 @@ export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
||||
|
||||
export const RE_NOTIFICATION_CONDITION_OPTIONS = [
|
||||
{ value: 'firing', label: 'Firing' },
|
||||
{ value: 'no-data', label: 'No Data' },
|
||||
{ value: 'nodata', label: 'No Data' },
|
||||
];
|
||||
|
||||
export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
||||
|
||||
@ -2,6 +2,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
|
||||
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
|
||||
import { useUpdateAlertRule } from 'hooks/alerts/useUpdateAlertRule';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import {
|
||||
@ -50,7 +51,7 @@ export const useCreateAlertState = (): ICreateAlertContextProps => {
|
||||
export function CreateAlertProvider(
|
||||
props: ICreateAlertProviderProps,
|
||||
): JSX.Element {
|
||||
const { children } = props;
|
||||
const { children, initialAlertState, isEditMode, ruleId } = props;
|
||||
|
||||
const [alertState, setAlertState] = useReducer(
|
||||
alertCreationReducer,
|
||||
@ -114,6 +115,31 @@ export function CreateAlertProvider(
|
||||
});
|
||||
}, [alertType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
setAlertState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.basicAlertState,
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.thresholdState,
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.evaluationWindowState,
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.advancedOptionsState,
|
||||
});
|
||||
setNotificationSettings({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.notificationSettingsState,
|
||||
});
|
||||
}
|
||||
}, [initialAlertState, isEditMode]);
|
||||
|
||||
const discardAlertRule = useCallback(() => {
|
||||
setAlertState({
|
||||
type: 'RESET',
|
||||
@ -143,6 +169,11 @@ export function CreateAlertProvider(
|
||||
isLoading: isTestingAlertRule,
|
||||
} = useTestAlertRule();
|
||||
|
||||
const {
|
||||
mutate: updateAlertRule,
|
||||
isLoading: isUpdatingAlertRule,
|
||||
} = useUpdateAlertRule(ruleId || '');
|
||||
|
||||
const contextValue: ICreateAlertContextProps = useMemo(
|
||||
() => ({
|
||||
alertState,
|
||||
@ -162,6 +193,9 @@ export function CreateAlertProvider(
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode: isEditMode || false,
|
||||
}),
|
||||
[
|
||||
alertState,
|
||||
@ -176,6 +210,9 @@ export function CreateAlertProvider(
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
||||
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Dispatch } from 'react';
|
||||
import { UseMutateFunction } from 'react-query';
|
||||
@ -8,6 +9,8 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
|
||||
|
||||
export interface ICreateAlertContextProps {
|
||||
alertState: AlertState;
|
||||
setAlertState: Dispatch<CreateAlertAction>;
|
||||
@ -36,11 +39,22 @@ export interface ICreateAlertContextProps {
|
||||
unknown
|
||||
>;
|
||||
discardAlertRule: () => void;
|
||||
isUpdatingAlertRule: boolean;
|
||||
updateAlertRule: UseMutateFunction<
|
||||
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateAlertProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialAlertType: AlertTypes;
|
||||
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
|
||||
isEditMode?: boolean;
|
||||
ruleId?: string;
|
||||
}
|
||||
|
||||
export enum AlertCreationStep {
|
||||
@ -60,6 +74,7 @@ export type CreateAlertAction =
|
||||
| { type: 'SET_ALERT_NAME'; payload: string }
|
||||
| { type: 'SET_ALERT_LABELS'; payload: Labels }
|
||||
| { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AlertState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface Threshold {
|
||||
@ -127,6 +142,7 @@ export type AlertThresholdAction =
|
||||
| { type: 'SET_ALGORITHM'; payload: string }
|
||||
| { type: 'SET_SEASONALITY'; payload: string }
|
||||
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AlertThresholdState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface AdvancedOptionsState {
|
||||
@ -198,6 +214,7 @@ export type AdvancedOptionsAction =
|
||||
};
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AdvancedOptionsState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface EvaluationWindowState {
|
||||
@ -219,6 +236,7 @@ export type EvaluationWindowAction =
|
||||
payload: { time: string; number: string; timezone: string; unit: string };
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: EvaluationWindowState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
||||
@ -229,7 +247,7 @@ export interface NotificationSettingsState {
|
||||
enabled: boolean;
|
||||
value: number;
|
||||
unit: string;
|
||||
conditions: ('firing' | 'no-data')[];
|
||||
conditions: ('firing' | 'nodata')[];
|
||||
};
|
||||
description: string;
|
||||
routingPolicies: boolean;
|
||||
@ -246,9 +264,10 @@ export type NotificationSettingsAction =
|
||||
enabled: boolean;
|
||||
value: number;
|
||||
unit: string;
|
||||
conditions: ('firing' | 'no-data')[];
|
||||
conditions: ('firing' | 'nodata')[];
|
||||
};
|
||||
}
|
||||
| { type: 'SET_DESCRIPTION'; payload: string }
|
||||
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
@ -53,6 +53,8 @@ export const alertCreationReducer = (
|
||||
};
|
||||
case 'RESET':
|
||||
return INITIAL_ALERT_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -119,6 +121,8 @@ export const alertThresholdReducer = (
|
||||
return { ...state, thresholds: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_ALERT_THRESHOLD_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -174,6 +178,8 @@ export const advancedOptionsReducer = (
|
||||
...state,
|
||||
evaluationCadence: { ...state.evaluationCadence, mode: action.payload },
|
||||
};
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
case 'RESET':
|
||||
return INITIAL_ADVANCED_OPTIONS_STATE;
|
||||
default:
|
||||
@ -202,6 +208,8 @@ export const evaluationWindowReducer = (
|
||||
return { ...state, startingAt: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_EVALUATION_WINDOW_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -222,6 +230,8 @@ export const notificationSettingsReducer = (
|
||||
return { ...state, routingPolicies: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -1,5 +1,21 @@
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from './context/types';
|
||||
|
||||
export interface CreateAlertV2Props {
|
||||
alertType: AlertTypes;
|
||||
}
|
||||
|
||||
export interface GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
basicAlertState: AlertState;
|
||||
thresholdState: AlertThresholdState;
|
||||
advancedOptionsState: AdvancedOptionsState;
|
||||
evaluationWindowState: EvaluationWindowState;
|
||||
notificationSettingsState: NotificationSettingsState;
|
||||
}
|
||||
|
||||
@ -1,7 +1,31 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Spin } from 'antd';
|
||||
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { getRandomColor } from 'container/ExplorerOptions/utils';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useCreateAlertState } from './context';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from './context/constants';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
AlertThresholdState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from './context/types';
|
||||
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
|
||||
|
||||
// UI side feature flag
|
||||
export const showNewCreateAlertsPage = (): boolean =>
|
||||
@ -11,12 +35,12 @@ export const showNewCreateAlertsPage = (): boolean =>
|
||||
// Layout 1 - Default layout
|
||||
// Layout 2 - Condensed layout
|
||||
export const showCondensedLayout = (): boolean =>
|
||||
localStorage.getItem('showCondensedLayout') === 'true';
|
||||
localStorage.getItem('hideCondensedLayout') !== 'true';
|
||||
|
||||
export function Spinner(): JSX.Element | null {
|
||||
const { isCreatingAlertRule } = useCreateAlertState();
|
||||
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
|
||||
|
||||
if (!isCreatingAlertRule) return null;
|
||||
if (!isCreatingAlertRule && !isUpdatingAlertRule) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="sticky-page-spinner">
|
||||
@ -25,3 +49,263 @@ export function Spinner(): JSX.Element | null {
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function getColorForThreshold(thresholdLabel: string): string {
|
||||
if (thresholdLabel === 'critical') {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (thresholdLabel === 'warning') {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
if (thresholdLabel === 'info') {
|
||||
return Color.BG_ROBIN_500;
|
||||
}
|
||||
return getRandomColor();
|
||||
}
|
||||
|
||||
export function parseGoTime(
|
||||
input: string,
|
||||
): { time: number; unit: UniversalYAxisUnit } {
|
||||
const regex = /(\d+)([hms])/g;
|
||||
const matches = [...input.matchAll(regex)];
|
||||
|
||||
const nonZero = matches.find(([, value]) => parseInt(value, 10) > 0);
|
||||
if (!nonZero) {
|
||||
return { time: 1, unit: UniversalYAxisUnit.MINUTES };
|
||||
}
|
||||
|
||||
const time = parseInt(nonZero[1], 10);
|
||||
const unitMap: Record<string, UniversalYAxisUnit> = {
|
||||
h: UniversalYAxisUnit.HOURS,
|
||||
m: UniversalYAxisUnit.MINUTES,
|
||||
s: UniversalYAxisUnit.SECONDS,
|
||||
};
|
||||
|
||||
return { time, unit: unitMap[nonZero[2]] };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function getEvaluationWindowStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): EvaluationWindowState {
|
||||
const windowType = alertDef.evaluation?.kind as 'rolling' | 'cumulative';
|
||||
|
||||
function getRollingWindowTimeframe(): string {
|
||||
if (
|
||||
// Default values for rolling window
|
||||
EVALUATION_WINDOW_TIMEFRAME.rolling
|
||||
.map((option) => option.value)
|
||||
.includes(alertDef.evaluation?.spec?.evalWindow || '')
|
||||
) {
|
||||
return alertDef.evaluation?.spec?.evalWindow || '';
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
function getCumulativeWindowTimeframe(): string {
|
||||
switch (alertDef.evaluation?.spec?.schedule?.type) {
|
||||
case 'hourly':
|
||||
return 'currentHour';
|
||||
case 'daily':
|
||||
return 'currentDay';
|
||||
case 'monthly':
|
||||
return 'currentMonth';
|
||||
default:
|
||||
return 'currentHour';
|
||||
}
|
||||
}
|
||||
|
||||
function convertApiFieldToTime(hour: number, minute: number): string {
|
||||
return `${hour.toString().padStart(2, '0')}:${minute
|
||||
.toString()
|
||||
.padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
function getCumulativeWindowStartingAt(): EvaluationWindowState['startingAt'] {
|
||||
const timeframe = getCumulativeWindowTimeframe();
|
||||
if (timeframe === 'currentHour') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: alertDef.evaluation?.spec?.schedule?.minute?.toString() || '0',
|
||||
};
|
||||
}
|
||||
if (timeframe === 'currentDay') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
time: convertApiFieldToTime(
|
||||
alertDef.evaluation?.spec?.schedule?.hour || 0,
|
||||
alertDef.evaluation?.spec?.schedule?.minute || 0,
|
||||
),
|
||||
timezone: alertDef.evaluation?.spec?.timezone || TIMEZONE_DATA[0].value,
|
||||
};
|
||||
}
|
||||
if (timeframe === 'currentMonth') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: alertDef.evaluation?.spec?.schedule?.day?.toString() || '0',
|
||||
timezone: alertDef.evaluation?.spec?.timezone || TIMEZONE_DATA[0].value,
|
||||
time: convertApiFieldToTime(
|
||||
alertDef.evaluation?.spec?.schedule?.hour || 0,
|
||||
alertDef.evaluation?.spec?.schedule?.minute || 0,
|
||||
),
|
||||
};
|
||||
}
|
||||
return INITIAL_EVALUATION_WINDOW_STATE.startingAt;
|
||||
}
|
||||
|
||||
if (windowType === 'rolling') {
|
||||
const timeframe = getRollingWindowTimeframe();
|
||||
if (timeframe === 'custom') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe,
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: parseGoTime(
|
||||
alertDef.evaluation?.spec?.evalWindow || '1m',
|
||||
).time.toString(),
|
||||
unit: parseGoTime(alertDef.evaluation?.spec?.evalWindow || '1m').unit,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe: getCumulativeWindowTimeframe(),
|
||||
startingAt: getCumulativeWindowStartingAt(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotificationSettingsStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): NotificationSettingsState {
|
||||
const description = alertDef.annotations?.description || '';
|
||||
const multipleNotifications = alertDef.notificationSettings?.groupBy || [];
|
||||
const routingPolicies = alertDef.notificationSettings?.usePolicy || false;
|
||||
|
||||
const reNotificationEnabled =
|
||||
alertDef.notificationSettings?.renotify?.enabled || false;
|
||||
const reNotificationConditions =
|
||||
alertDef.notificationSettings?.renotify?.alertStates?.map(
|
||||
(state) => state as 'firing' | 'nodata',
|
||||
) || [];
|
||||
const reNotificationValue = alertDef.notificationSettings?.renotify
|
||||
? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').time
|
||||
: 1;
|
||||
const reNotificationUnit = alertDef.notificationSettings?.renotify
|
||||
? parseGoTime(alertDef.notificationSettings.renotify.interval || '1m').unit
|
||||
: UniversalYAxisUnit.MINUTES;
|
||||
|
||||
return {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
description,
|
||||
multipleNotifications,
|
||||
routingPolicies,
|
||||
reNotification: {
|
||||
enabled: reNotificationEnabled,
|
||||
conditions: reNotificationConditions,
|
||||
value: reNotificationValue,
|
||||
unit: reNotificationUnit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getAdvancedOptionsStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AdvancedOptionsState {
|
||||
return {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.sendNotificationIfDataIsMissing,
|
||||
toleranceLimit: alertDef.condition.absentFor || 0,
|
||||
enabled: alertDef.condition.alertOnAbsent || false,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.enforceMinimumDatapoints,
|
||||
minimumDatapoints: alertDef.condition.requiredNumPoints || 0,
|
||||
enabled: alertDef.condition.requireMinPoints || false,
|
||||
},
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'default',
|
||||
default: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence.default,
|
||||
value: parseGoTime(alertDef.evaluation?.spec?.frequency || '1m').time,
|
||||
timeUnit: parseGoTime(alertDef.evaluation?.spec?.frequency || '1m').unit,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getThresholdStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AlertThresholdState {
|
||||
return {
|
||||
...INITIAL_ALERT_THRESHOLD_STATE,
|
||||
thresholds:
|
||||
alertDef.condition.thresholds?.spec.map((threshold) => ({
|
||||
id: v4(),
|
||||
label: threshold.name,
|
||||
thresholdValue: threshold.target,
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
channels: threshold.channels,
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
(alertDef.condition.thresholds?.spec[0].op as AlertThresholdOperator) ||
|
||||
AlertThresholdOperator.IS_ABOVE,
|
||||
matchType:
|
||||
(alertDef.condition.thresholds?.spec[0]
|
||||
.matchType as AlertThresholdMatchType) ||
|
||||
AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCreateAlertLocalStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2 | undefined,
|
||||
): GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
if (!alertDef) {
|
||||
return {
|
||||
basicAlertState: INITIAL_ALERT_STATE,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptionsState: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindowState: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettingsState: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
};
|
||||
}
|
||||
// Basic alert state
|
||||
const basicAlertState: AlertState = {
|
||||
...INITIAL_ALERT_STATE,
|
||||
name: alertDef.alert,
|
||||
labels: alertDef.labels || {},
|
||||
yAxisUnit: alertDef.condition.compositeQuery.unit,
|
||||
};
|
||||
|
||||
const thresholdState = getThresholdStateFromAlertDef(alertDef);
|
||||
|
||||
const advancedOptionsState = getAdvancedOptionsStateFromAlertDef(alertDef);
|
||||
|
||||
const evaluationWindowState = getEvaluationWindowStateFromAlertDef(alertDef);
|
||||
|
||||
const notificationSettingsState = getNotificationSettingsStateFromAlertDef(
|
||||
alertDef,
|
||||
);
|
||||
|
||||
return {
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptionsState,
|
||||
evaluationWindowState,
|
||||
notificationSettingsState,
|
||||
};
|
||||
}
|
||||
|
||||
56
frontend/src/container/EditAlertV2/EditAlertV2.tsx
Normal file
56
frontend/src/container/EditAlertV2/EditAlertV2.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import '../CreateAlertV2/CreateAlertV2.styles.scss';
|
||||
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { useMemo } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import AlertCondition from '../CreateAlertV2/AlertCondition';
|
||||
import { buildInitialAlertDef } from '../CreateAlertV2/context/utils';
|
||||
import EvaluationSettings from '../CreateAlertV2/EvaluationSettings';
|
||||
import Footer from '../CreateAlertV2/Footer';
|
||||
import NotificationSettings from '../CreateAlertV2/NotificationSettings';
|
||||
import QuerySection from '../CreateAlertV2/QuerySection';
|
||||
import { showCondensedLayout, Spinner } from '../CreateAlertV2/utils';
|
||||
|
||||
interface EditAlertV2Props {
|
||||
alertType?: AlertTypes;
|
||||
initialAlert: PostableAlertRuleV2;
|
||||
}
|
||||
|
||||
function EditAlertV2({
|
||||
alertType = AlertTypes.METRICS_BASED_ALERT,
|
||||
initialAlert,
|
||||
}: EditAlertV2Props): JSX.Element {
|
||||
const currentQueryToRedirect = useMemo(() => {
|
||||
const basicAlertDef = buildInitialAlertDef(alertType);
|
||||
return mapQueryDataFromApi(
|
||||
initialAlert?.condition.compositeQuery ||
|
||||
basicAlertDef.condition.compositeQuery,
|
||||
);
|
||||
}, [initialAlert, alertType]);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
|
||||
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spinner />
|
||||
<div className="create-alert-v2-container">
|
||||
<QuerySection />
|
||||
<AlertCondition />
|
||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EditAlertV2.defaultProps = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
};
|
||||
|
||||
export default EditAlertV2;
|
||||
3
frontend/src/container/EditAlertV2/index.ts
Normal file
3
frontend/src/container/EditAlertV2/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import EditAlertV2 from './EditAlertV2';
|
||||
|
||||
export default EditAlertV2;
|
||||
@ -1,11 +1,32 @@
|
||||
import { Form } from 'antd';
|
||||
import EditAlertV2 from 'container/EditAlertV2';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
function EditRules({
|
||||
initialValue,
|
||||
ruleId,
|
||||
initialV2AlertValue,
|
||||
}: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
if (
|
||||
initialV2AlertValue !== null &&
|
||||
initialV2AlertValue.schemaVersion === NEW_ALERT_SCHEMA_VERSION
|
||||
) {
|
||||
return (
|
||||
<EditAlertV2
|
||||
initialAlert={initialV2AlertValue}
|
||||
alertType={initialValue.alertType as AlertTypes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={
|
||||
@ -23,6 +44,7 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
interface EditRulesProps {
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
initialV2AlertValue: PostableAlertRuleV2 | null;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Flex, Input, Typography } from 'antd';
|
||||
import { Button, Dropdown, Flex, Input, MenuProps, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@ -31,7 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import DeleteAlert from './DeleteAlert';
|
||||
import { Button, ColumnButton, SearchContainer } from './styles';
|
||||
import { ColumnButton, SearchContainer } from './styles';
|
||||
import Status from './TableComponents/Status';
|
||||
import ToggleAlertState from './ToggleAlertState';
|
||||
import { alertActionLogEvent, filterAlerts } from './utils';
|
||||
@ -97,14 +97,37 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
const onClickNewAlertV2Handler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
history.push(`${ROUTES.ALERTS_NEW}?showNewCreateAlertsPage=true`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onClickNewClassicAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'classic',
|
||||
});
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const newAlertMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'new',
|
||||
label: 'Try the new experience',
|
||||
onClick: onClickNewAlertV2Handler,
|
||||
},
|
||||
{
|
||||
key: 'classic',
|
||||
label: 'Continue with the current experience',
|
||||
onClick: onClickNewClassicAlertHandler,
|
||||
},
|
||||
];
|
||||
|
||||
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||
params.set(
|
||||
@ -368,13 +391,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
/>
|
||||
<Flex gap={12}>
|
||||
{addNewAlert && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickNewAlertHandler}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
<Dropdown menu={{ items: newAlertMenuItems }} trigger={['click']}>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
New Alert
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
<TextToolTip
|
||||
{...{
|
||||
|
||||
@ -36,9 +36,7 @@ export function mapRoutingPolicyToCreateApiPayload(
|
||||
return {
|
||||
name,
|
||||
expression,
|
||||
actions: {
|
||||
channels,
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
@ -53,9 +51,7 @@ export function mapRoutingPolicyToUpdateApiPayload(
|
||||
return {
|
||||
name,
|
||||
expression,
|
||||
actions: {
|
||||
channels,
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
22
frontend/src/hooks/alerts/useUpdateAlertRule.ts
Normal file
22
frontend/src/hooks/alerts/useUpdateAlertRule.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import updateAlertRule, {
|
||||
UpdateAlertRuleResponse,
|
||||
} from 'api/alerts/updateAlertRule';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export function useUpdateAlertRule(
|
||||
id: string,
|
||||
): UseMutationResult<
|
||||
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
> {
|
||||
return useMutation<
|
||||
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
>({
|
||||
mutationFn: (alertData) => updateAlertRule(id, alertData),
|
||||
});
|
||||
}
|
||||
@ -6,10 +6,14 @@ import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
@ -85,6 +89,16 @@ function AlertDetails(): JSX.Element {
|
||||
document.title = alertTitle || document.title;
|
||||
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
|
||||
|
||||
const alertRuleDetails = useMemo(
|
||||
() => alertDetailsResponse?.payload?.data as PostableAlertRuleV2 | undefined,
|
||||
[alertDetailsResponse],
|
||||
);
|
||||
|
||||
const initialAlertState = useMemo(
|
||||
() => getCreateAlertLocalStateFromAlertDef(alertRuleDetails),
|
||||
[alertRuleDetails],
|
||||
);
|
||||
|
||||
if (
|
||||
isError ||
|
||||
!isValidRuleId ||
|
||||
@ -104,6 +118,12 @@ function AlertDetails(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<CreateAlertProvider
|
||||
ruleId={ruleId || ''}
|
||||
isEditMode
|
||||
initialAlertType={alertRuleDetails?.alertType as AlertTypes}
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<div className="alert-details">
|
||||
<Breadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
@ -134,6 +154,7 @@ function AlertDetails(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CreateAlertProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { AlertHeaderProps } from '../AlertHeader';
|
||||
@ -60,7 +61,11 @@ function AlertActionButtons({
|
||||
setIsRenameAlertOpen(false);
|
||||
}, [handleAlertUpdate]);
|
||||
|
||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
...(!isV2Alert
|
||||
? [
|
||||
{
|
||||
key: 'rename-rule',
|
||||
label: 'Rename',
|
||||
@ -68,6 +73,8 @@ function AlertActionButtons({
|
||||
onClick: handleRename,
|
||||
style: menuItemStyle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'duplicate-rule',
|
||||
label: 'Duplicate',
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import './AlertHeader.styles.scss';
|
||||
|
||||
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import AlertActionButtons from './ActionButtons/ActionButtons';
|
||||
import AlertLabels from './AlertLabels/AlertLabels';
|
||||
@ -10,13 +16,7 @@ import AlertSeverity from './AlertSeverity/AlertSeverity';
|
||||
import AlertState from './AlertState/AlertState';
|
||||
|
||||
export type AlertHeaderProps = {
|
||||
alertDetails: {
|
||||
state: string;
|
||||
alert: string;
|
||||
id: string;
|
||||
labels: Record<string, string | undefined> | undefined;
|
||||
disabled: boolean;
|
||||
};
|
||||
alertDetails: GettableAlert | PostableAlertRuleV2;
|
||||
};
|
||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { state, alert: alertName, labels } = alertDetails;
|
||||
@ -32,12 +32,13 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
return {};
|
||||
}, [labels]);
|
||||
|
||||
return (
|
||||
<div className="alert-info">
|
||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||
|
||||
const CreateAlertV1Header = (
|
||||
<div className="alert-info__info-wrapper">
|
||||
<div className="top-section">
|
||||
<div className="alert-title-wrapper">
|
||||
<AlertState state={alertRuleState ?? state} />
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={updatedName || alertName} />
|
||||
</div>
|
||||
@ -54,10 +55,15 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
<AlertLabels labels={labelsWithoutSeverity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="alert-info">
|
||||
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
|
||||
<div className="alert-info__action-buttons">
|
||||
<AlertActionButtons
|
||||
alertDetails={alertDetails}
|
||||
ruleId={alertDetails.id}
|
||||
ruleId={alertDetails?.id || ''}
|
||||
setUpdatedName={setUpdatedName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,10 @@ import history from 'lib/history';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import {
|
||||
errorMessageReceivedFromBackend,
|
||||
@ -88,9 +92,18 @@ function EditRules(): JSX.Element {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
let initialV2AlertValue: PostableAlertRuleV2 | null = null;
|
||||
if (data.payload.data.schemaVersion === NEW_ALERT_SCHEMA_VERSION) {
|
||||
initialV2AlertValue = data.payload.data as PostableAlertRuleV2;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="edit-rules-container">
|
||||
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
|
||||
<EditRulesContainer
|
||||
ruleId={ruleId || ''}
|
||||
initialValue={data.payload.data}
|
||||
initialV2AlertValue={initialV2AlertValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,9 +13,10 @@ export interface BasicThreshold {
|
||||
|
||||
export interface PostableAlertRuleV2 {
|
||||
schemaVersion: string;
|
||||
id?: string;
|
||||
alert: string;
|
||||
alertType: AlertTypes;
|
||||
ruleType: string;
|
||||
alertType?: AlertTypes;
|
||||
ruleType?: string;
|
||||
condition: {
|
||||
thresholds?: {
|
||||
kind: string;
|
||||
@ -28,13 +29,13 @@ export interface PostableAlertRuleV2 {
|
||||
requireMinPoints?: boolean;
|
||||
requiredNumPoints?: number;
|
||||
};
|
||||
evaluation: {
|
||||
kind: 'rolling' | 'cumulative';
|
||||
spec: {
|
||||
evaluation?: {
|
||||
kind?: 'rolling' | 'cumulative';
|
||||
spec?: {
|
||||
evalWindow?: string;
|
||||
frequency: string;
|
||||
frequency?: string;
|
||||
schedule?: {
|
||||
type: 'hourly' | 'daily' | 'monthly';
|
||||
type?: 'hourly' | 'daily' | 'monthly';
|
||||
minute?: number;
|
||||
hour?: number;
|
||||
day?: number;
|
||||
@ -42,19 +43,24 @@ export interface PostableAlertRuleV2 {
|
||||
timezone?: string;
|
||||
};
|
||||
};
|
||||
labels: Labels;
|
||||
annotations: {
|
||||
labels?: Labels;
|
||||
annotations?: {
|
||||
description: string;
|
||||
summary: string;
|
||||
};
|
||||
notificationSettings: {
|
||||
notificationGroupBy: string[];
|
||||
renotify?: string;
|
||||
alertStates: string[];
|
||||
notificationPolicy: boolean;
|
||||
notificationSettings?: {
|
||||
groupBy?: string[];
|
||||
renotify?: {
|
||||
enabled: boolean;
|
||||
interval?: string;
|
||||
alertStates?: string[];
|
||||
};
|
||||
version: string;
|
||||
source: string;
|
||||
usePolicy?: boolean;
|
||||
};
|
||||
version?: string;
|
||||
source?: string;
|
||||
state?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
||||
@ -66,3 +72,5 @@ export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
||||
updateAt: string;
|
||||
updateBy: string;
|
||||
}
|
||||
|
||||
export const NEW_ALERT_SCHEMA_VERSION = 'v2alpha1';
|
||||
|
||||
@ -13,6 +13,7 @@ export interface GettableAlert extends AlertDef {
|
||||
createBy: string;
|
||||
updateAt: string;
|
||||
updateBy: string;
|
||||
schemaVersion: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user