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 {
|
export interface CreateRoutingPolicyBody {
|
||||||
name: string;
|
name: string;
|
||||||
expression: string;
|
expression: string;
|
||||||
actions: {
|
|
||||||
channels: string[];
|
channels: string[];
|
||||||
};
|
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +21,7 @@ const createRoutingPolicy = async (
|
|||||||
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2
|
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`/notification-policy`, props);
|
const response = await axios.post(`/route_policies`, props);
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
|||||||
@ -14,9 +14,7 @@ const deleteRoutingPolicy = async (
|
|||||||
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2
|
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete(
|
const response = await axios.delete(`/route_policies/${routingPolicyId}`);
|
||||||
`/notification-policy/${routingPolicyId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const getRoutingPolicies = async (
|
|||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
): Promise<SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2> => {
|
): Promise<SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/notification-policy', {
|
const response = await axios.get('/route_policies', {
|
||||||
signal,
|
signal,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,9 +6,7 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|||||||
export interface UpdateRoutingPolicyBody {
|
export interface UpdateRoutingPolicyBody {
|
||||||
name: string;
|
name: string;
|
||||||
expression: string;
|
expression: string;
|
||||||
actions: {
|
|
||||||
channels: string[];
|
channels: string[];
|
||||||
};
|
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +22,7 @@ const updateRoutingPolicy = async (
|
|||||||
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2
|
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`/notification-policy/${id}`, {
|
const response = await axios.put(`/route_policies/${id}`, {
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||||
import { showNewCreateAlertsPage } from 'container/CreateAlertV2/utils';
|
|
||||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
@ -127,7 +126,8 @@ function CreateRules(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showNewCreateAlertsPageFlag = showNewCreateAlertsPage();
|
const showNewCreateAlertsPageFlag =
|
||||||
|
queryParams.get('showNewCreateAlertsPage') === 'true';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
showNewCreateAlertsPageFlag &&
|
showNewCreateAlertsPageFlag &&
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Button, Select, Tooltip, Typography } from 'antd';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
import {
|
import {
|
||||||
@ -46,6 +47,19 @@ function AlertThreshold({
|
|||||||
|
|
||||||
const queryNames = getQueryNames(currentQuery);
|
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 selectedCategory = getCategoryByOptionId(alertState.yAxisUnit || '');
|
||||||
const categorySelectOptions = getCategorySelectOptionByName(
|
const categorySelectOptions = getCategorySelectOptionByName(
|
||||||
selectedCategory || '',
|
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 = {
|
const TEST_STRINGS = {
|
||||||
ADD_THRESHOLD: 'Add Threshold',
|
ADD_THRESHOLD: 'Add Threshold',
|
||||||
AT_LEAST_ONCE: 'AT LEAST ONCE',
|
AT_LEAST_ONCE: 'AT LEAST ONCE',
|
||||||
@ -204,11 +209,11 @@ describe('AlertThreshold', () => {
|
|||||||
|
|
||||||
// First addition should add WARNING threshold
|
// First addition should add WARNING threshold
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
expect(screen.getByText('WARNING')).toBeInTheDocument();
|
expect(screen.getByText('warning')).toBeInTheDocument();
|
||||||
|
|
||||||
// Second addition should add INFO threshold
|
// Second addition should add INFO threshold
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
expect(screen.getByText('INFO')).toBeInTheDocument();
|
expect(screen.getByText('info')).toBeInTheDocument();
|
||||||
|
|
||||||
// Third addition should add random threshold
|
// Third addition should add random threshold
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
@ -280,7 +285,7 @@ describe('AlertThreshold', () => {
|
|||||||
renderAlertThreshold();
|
renderAlertThreshold();
|
||||||
|
|
||||||
// Should have initial critical threshold
|
// Should have initial critical threshold
|
||||||
expect(screen.getByText('CRITICAL')).toBeInTheDocument();
|
expect(screen.getByText('critical')).toBeInTheDocument();
|
||||||
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
|
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
|
||||||
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
|
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);
|
border: 1px dashed var(--bg-vanilla-300);
|
||||||
color: var(--bg-ink-300);
|
color: var(--bg-ink-300);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--bg-ink-300);
|
border-color: var(--bg-ink-300);
|
||||||
@ -506,7 +513,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.condensed-evaluation-settings-container {
|
.condensed-evaluation-settings-container {
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Labels } from 'types/api/alerts/def';
|
import { Labels } from 'types/api/alerts/def';
|
||||||
@ -8,7 +9,7 @@ import { useCreateAlertState } from '../context';
|
|||||||
import LabelsInput from './LabelsInput';
|
import LabelsInput from './LabelsInput';
|
||||||
|
|
||||||
function CreateAlertHeader(): JSX.Element {
|
function CreateAlertHeader(): JSX.Element {
|
||||||
const { alertState, setAlertState } = useCreateAlertState();
|
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@ -34,10 +35,14 @@ function CreateAlertHeader(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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-bar">
|
||||||
<div className="alert-header__tab">New Alert Rule</div>
|
<div className="alert-header__tab">New Alert Rule</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="alert-header__content">
|
<div className="alert-header__content">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
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 { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
|
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
|
||||||
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
|
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
|
||||||
|
import * as useUpdateAlertRuleHook from '../../../../hooks/alerts/useUpdateAlertRule';
|
||||||
import { CreateAlertProvider } from '../../context';
|
import { CreateAlertProvider } from '../../context';
|
||||||
import CreateAlertHeader from '../CreateAlertHeader';
|
import CreateAlertHeader from '../CreateAlertHeader';
|
||||||
|
|
||||||
@ -15,6 +18,10 @@ jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
|
|||||||
mutate: jest.fn(),
|
mutate: jest.fn(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
} as any);
|
} as any);
|
||||||
|
jest.spyOn(useUpdateAlertRuleHook, 'useUpdateAlertRule').mockReturnValue({
|
||||||
|
mutate: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
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> =>
|
const renderCreateAlertHeader = (): ReturnType<typeof render> =>
|
||||||
render(
|
render(
|
||||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
@ -52,7 +61,9 @@ describe('CreateAlertHeader', () => {
|
|||||||
|
|
||||||
it('renders name input with placeholder', () => {
|
it('renders name input with placeholder', () => {
|
||||||
renderCreateAlertHeader();
|
renderCreateAlertHeader();
|
||||||
const nameInput = screen.getByPlaceholderText('Enter alert rule name');
|
const nameInput = screen.getByPlaceholderText(
|
||||||
|
ENTER_ALERT_RULE_NAME_PLACEHOLDER,
|
||||||
|
);
|
||||||
expect(nameInput).toBeInTheDocument();
|
expect(nameInput).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,10 +74,30 @@ describe('CreateAlertHeader', () => {
|
|||||||
|
|
||||||
it('updates name when typing in name input', () => {
|
it('updates name when typing in name input', () => {
|
||||||
renderCreateAlertHeader();
|
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' } });
|
fireEvent.change(nameInput, { target: { value: 'Test Alert' } });
|
||||||
|
|
||||||
expect(nameInput).toHaveValue('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 {
|
.labels-input {
|
||||||
&__add-button {
|
&__add-button {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--bg-ink-300);
|
border-color: var(--bg-ink-300);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import './styles.scss';
|
|||||||
|
|
||||||
import { Switch, Tooltip, Typography } from 'antd';
|
import { Switch, Tooltip, Typography } from 'antd';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IAdvancedOptionItemProps } from '../types';
|
import { IAdvancedOptionItemProps } from '../types';
|
||||||
|
|
||||||
@ -12,9 +12,14 @@ function AdvancedOptionItem({
|
|||||||
input,
|
input,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
defaultShowInput,
|
||||||
}: IAdvancedOptionItemProps): JSX.Element {
|
}: IAdvancedOptionItemProps): JSX.Element {
|
||||||
const [showInput, setShowInput] = useState<boolean>(false);
|
const [showInput, setShowInput] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowInput(defaultShowInput);
|
||||||
|
}, [defaultShowInput]);
|
||||||
|
|
||||||
const handleOnToggle = (): void => {
|
const handleOnToggle = (): void => {
|
||||||
onToggle?.();
|
onToggle?.();
|
||||||
setShowInput((currentShowInput) => !currentShowInput);
|
setShowInput((currentShowInput) => !currentShowInput);
|
||||||
@ -42,7 +47,7 @@ function AdvancedOptionItem({
|
|||||||
>
|
>
|
||||||
{input}
|
{input}
|
||||||
</div>
|
</div>
|
||||||
<Switch onChange={handleOnToggle} />
|
<Switch onChange={handleOnToggle} checked={showInput} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -42,6 +42,7 @@ function AdvancedOptions(): JSX.Element {
|
|||||||
payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled,
|
payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
defaultShowInput={advancedOptions.sendNotificationIfDataIsMissing.enabled}
|
||||||
/>
|
/>
|
||||||
<AdvancedOptionItem
|
<AdvancedOptionItem
|
||||||
title="Minimum data required"
|
title="Minimum data required"
|
||||||
@ -72,6 +73,7 @@ function AdvancedOptions(): JSX.Element {
|
|||||||
payload: !advancedOptions.enforceMinimumDatapoints.enabled,
|
payload: !advancedOptions.enforceMinimumDatapoints.enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
defaultShowInput={advancedOptions.enforceMinimumDatapoints.enabled}
|
||||||
/>
|
/>
|
||||||
{/* TODO: Add back when the functionality is implemented */}
|
{/* TODO: Add back when the functionality is implemented */}
|
||||||
{/* <AdvancedOptionItem
|
{/* <AdvancedOptionItem
|
||||||
|
|||||||
@ -33,6 +33,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,6 +67,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -88,6 +91,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -117,6 +121,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -146,6 +151,7 @@ describe('AdvancedOptionItem', () => {
|
|||||||
title={defaultProps.title}
|
title={defaultProps.title}
|
||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -160,9 +166,24 @@ describe('AdvancedOptionItem', () => {
|
|||||||
description={defaultProps.description}
|
description={defaultProps.description}
|
||||||
input={defaultProps.input}
|
input={defaultProps.input}
|
||||||
tooltipText="mock tooltip text"
|
tooltipText="mock tooltip text"
|
||||||
|
defaultShowInput={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const tooltipIcon = screen.getByTestId('tooltip-icon');
|
const tooltipIcon = screen.getByTestId('tooltip-icon');
|
||||||
expect(tooltipIcon).toBeInTheDocument();
|
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 EvaluationSettings from '../EvaluationSettings';
|
||||||
import { createMockAlertContextState } from './testUtils';
|
import { createMockAlertContextState } from './testUtils';
|
||||||
|
|
||||||
|
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||||
|
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||||
|
showCondensedLayout: jest.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockSetEvaluationWindow = jest.fn();
|
const mockSetEvaluationWindow = jest.fn();
|
||||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||||
createMockAlertContextState({
|
createMockAlertContextState({
|
||||||
|
|||||||
@ -31,6 +31,9 @@ export const createMockAlertContextState = (
|
|||||||
isCreatingAlertRule: false,
|
isCreatingAlertRule: false,
|
||||||
isTestingAlertRule: false,
|
isTestingAlertRule: false,
|
||||||
createAlertRule: jest.fn(),
|
createAlertRule: jest.fn(),
|
||||||
|
isUpdatingAlertRule: false,
|
||||||
|
updateAlertRule: jest.fn(),
|
||||||
|
isEditMode: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface IAdvancedOptionItemProps {
|
|||||||
input: JSX.Element;
|
input: JSX.Element;
|
||||||
tooltipText?: string;
|
tooltipText?: string;
|
||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
|
defaultShowInput: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RollingWindowTimeframes {
|
export enum RollingWindowTimeframes {
|
||||||
|
|||||||
@ -26,11 +26,17 @@ function Footer(): JSX.Element {
|
|||||||
isCreatingAlertRule,
|
isCreatingAlertRule,
|
||||||
testAlertRule,
|
testAlertRule,
|
||||||
isTestingAlertRule,
|
isTestingAlertRule,
|
||||||
|
updateAlertRule,
|
||||||
|
isUpdatingAlertRule,
|
||||||
|
isEditMode,
|
||||||
} = useCreateAlertState();
|
} = useCreateAlertState();
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
const handleDiscard = (): void => discardAlertRule();
|
const handleDiscard = (): void => {
|
||||||
|
discardAlertRule();
|
||||||
|
safeNavigate('/alerts');
|
||||||
|
};
|
||||||
|
|
||||||
const alertValidationMessage = useMemo(
|
const alertValidationMessage = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -99,6 +105,17 @@ function Footer(): JSX.Element {
|
|||||||
notificationSettings,
|
notificationSettings,
|
||||||
query: currentQuery,
|
query: currentQuery,
|
||||||
});
|
});
|
||||||
|
if (isEditMode) {
|
||||||
|
updateAlertRule(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Alert rule updated successfully');
|
||||||
|
safeNavigate('/alerts');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
createAlertRule(payload, {
|
createAlertRule(payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Alert rule created successfully');
|
toast.success('Alert rule created successfully');
|
||||||
@ -108,6 +125,7 @@ function Footer(): JSX.Element {
|
|||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
alertType,
|
alertType,
|
||||||
basicAlertState,
|
basicAlertState,
|
||||||
@ -116,16 +134,22 @@ function Footer(): JSX.Element {
|
|||||||
evaluationWindow,
|
evaluationWindow,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
|
isEditMode,
|
||||||
|
updateAlertRule,
|
||||||
createAlertRule,
|
createAlertRule,
|
||||||
safeNavigate,
|
safeNavigate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const disableButtons =
|
const disableButtons =
|
||||||
isCreatingAlertRule || isTestingAlertRule || !!alertValidationMessage;
|
isCreatingAlertRule || isTestingAlertRule || isUpdatingAlertRule;
|
||||||
|
|
||||||
const saveAlertButton = useMemo(() => {
|
const saveAlertButton = useMemo(() => {
|
||||||
let button = (
|
let button = (
|
||||||
<Button type="primary" onClick={handleSaveAlert} disabled={disableButtons}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSaveAlert}
|
||||||
|
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||||
|
>
|
||||||
<Check size={14} />
|
<Check size={14} />
|
||||||
<Typography.Text>Save Alert Rule</Typography.Text>
|
<Typography.Text>Save Alert Rule</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
@ -141,7 +165,7 @@ function Footer(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
onClick={handleTestNotification}
|
onClick={handleTestNotification}
|
||||||
disabled={disableButtons}
|
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||||
>
|
>
|
||||||
<Send size={14} />
|
<Send size={14} />
|
||||||
<Typography.Text>Test Notification</Typography.Text>
|
<Typography.Text>Test Notification</Typography.Text>
|
||||||
@ -155,7 +179,7 @@ function Footer(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-alert-v2-footer">
|
<div className="create-alert-v2-footer">
|
||||||
<Button type="text" onClick={handleDiscard} disabled={disableButtons}>
|
<Button type="default" onClick={handleDiscard} disabled={disableButtons}>
|
||||||
<X size={14} /> Discard
|
<X size={14} /> Discard
|
||||||
</Button>
|
</Button>
|
||||||
<div className="button-group">
|
<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';
|
import { BuildCreateAlertRulePayloadArgs } from './types';
|
||||||
|
|
||||||
// Get formatted time/unit pairs for create alert api payload
|
// 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> = {
|
const unitMap: Record<string, string> = {
|
||||||
[UniversalYAxisUnit.SECONDS]: 's',
|
[UniversalYAxisUnit.SECONDS]: 's',
|
||||||
[UniversalYAxisUnit.MINUTES]: 'm',
|
[UniversalYAxisUnit.MINUTES]: 'm',
|
||||||
@ -57,19 +57,17 @@ export function getNotificationSettingsProps(
|
|||||||
notificationSettings: NotificationSettingsState,
|
notificationSettings: NotificationSettingsState,
|
||||||
): PostableAlertRuleV2['notificationSettings'] {
|
): PostableAlertRuleV2['notificationSettings'] {
|
||||||
const notificationSettingsProps: PostableAlertRuleV2['notificationSettings'] = {
|
const notificationSettingsProps: PostableAlertRuleV2['notificationSettings'] = {
|
||||||
notificationGroupBy: notificationSettings.multipleNotifications || [],
|
groupBy: notificationSettings.multipleNotifications || [],
|
||||||
alertStates: notificationSettings.reNotification.enabled
|
usePolicy: notificationSettings.routingPolicies,
|
||||||
? notificationSettings.reNotification.conditions
|
renotify: {
|
||||||
: [],
|
enabled: notificationSettings.reNotification.enabled,
|
||||||
notificationPolicy: notificationSettings.routingPolicies,
|
interval: getFormattedTimeValue(
|
||||||
};
|
|
||||||
|
|
||||||
if (notificationSettings.reNotification.enabled) {
|
|
||||||
notificationSettingsProps.renotify = getFormattedTimeValue(
|
|
||||||
notificationSettings.reNotification.value,
|
notificationSettings.reNotification.value,
|
||||||
notificationSettings.reNotification.unit,
|
notificationSettings.reNotification.unit,
|
||||||
);
|
),
|
||||||
}
|
alertStates: notificationSettings.reNotification.conditions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return notificationSettingsProps;
|
return notificationSettingsProps;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,6 +103,7 @@ function NotificationSettings(): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
defaultShowInput={notificationSettings.reNotification.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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()
|
const initialNotificationSettings = createMockAlertContextState()
|
||||||
.notificationSettings;
|
.notificationSettings;
|
||||||
const mockSetNotificationSettings = jest.fn();
|
const mockSetNotificationSettings = jest.fn();
|
||||||
|
|||||||
@ -132,7 +132,7 @@
|
|||||||
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
&.active-tab {
|
&.active-tab {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-vanilla-100) !important;
|
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 = {
|
export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'CRITICAL',
|
label: 'critical',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: null,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
@ -37,7 +37,7 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
|||||||
|
|
||||||
export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'WARNING',
|
label: 'warning',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: null,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
@ -47,7 +47,7 @@ export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
|||||||
|
|
||||||
export const INITIAL_INFO_THRESHOLD: Threshold = {
|
export const INITIAL_INFO_THRESHOLD: Threshold = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'INFO',
|
label: 'info',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: null,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
@ -177,7 +177,7 @@ export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
|||||||
|
|
||||||
export const RE_NOTIFICATION_CONDITION_OPTIONS = [
|
export const RE_NOTIFICATION_CONDITION_OPTIONS = [
|
||||||
{ value: 'firing', label: 'Firing' },
|
{ value: 'firing', label: 'Firing' },
|
||||||
{ value: 'no-data', label: 'No Data' },
|
{ value: 'nodata', label: 'No Data' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { QueryParams } from 'constants/query';
|
|||||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
|
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
|
||||||
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
|
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
|
||||||
|
import { useUpdateAlertRule } from 'hooks/alerts/useUpdateAlertRule';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import {
|
import {
|
||||||
@ -50,7 +51,7 @@ export const useCreateAlertState = (): ICreateAlertContextProps => {
|
|||||||
export function CreateAlertProvider(
|
export function CreateAlertProvider(
|
||||||
props: ICreateAlertProviderProps,
|
props: ICreateAlertProviderProps,
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const { children } = props;
|
const { children, initialAlertState, isEditMode, ruleId } = props;
|
||||||
|
|
||||||
const [alertState, setAlertState] = useReducer(
|
const [alertState, setAlertState] = useReducer(
|
||||||
alertCreationReducer,
|
alertCreationReducer,
|
||||||
@ -114,6 +115,31 @@ export function CreateAlertProvider(
|
|||||||
});
|
});
|
||||||
}, [alertType]);
|
}, [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(() => {
|
const discardAlertRule = useCallback(() => {
|
||||||
setAlertState({
|
setAlertState({
|
||||||
type: 'RESET',
|
type: 'RESET',
|
||||||
@ -143,6 +169,11 @@ export function CreateAlertProvider(
|
|||||||
isLoading: isTestingAlertRule,
|
isLoading: isTestingAlertRule,
|
||||||
} = useTestAlertRule();
|
} = useTestAlertRule();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: updateAlertRule,
|
||||||
|
isLoading: isUpdatingAlertRule,
|
||||||
|
} = useUpdateAlertRule(ruleId || '');
|
||||||
|
|
||||||
const contextValue: ICreateAlertContextProps = useMemo(
|
const contextValue: ICreateAlertContextProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
alertState,
|
alertState,
|
||||||
@ -162,6 +193,9 @@ export function CreateAlertProvider(
|
|||||||
isCreatingAlertRule,
|
isCreatingAlertRule,
|
||||||
testAlertRule,
|
testAlertRule,
|
||||||
isTestingAlertRule,
|
isTestingAlertRule,
|
||||||
|
updateAlertRule,
|
||||||
|
isUpdatingAlertRule,
|
||||||
|
isEditMode: isEditMode || false,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
alertState,
|
alertState,
|
||||||
@ -176,6 +210,9 @@ export function CreateAlertProvider(
|
|||||||
isCreatingAlertRule,
|
isCreatingAlertRule,
|
||||||
testAlertRule,
|
testAlertRule,
|
||||||
isTestingAlertRule,
|
isTestingAlertRule,
|
||||||
|
updateAlertRule,
|
||||||
|
isUpdatingAlertRule,
|
||||||
|
isEditMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
||||||
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||||
|
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
|
||||||
import { Dayjs } from 'dayjs';
|
import { Dayjs } from 'dayjs';
|
||||||
import { Dispatch } from 'react';
|
import { Dispatch } from 'react';
|
||||||
import { UseMutateFunction } from 'react-query';
|
import { UseMutateFunction } from 'react-query';
|
||||||
@ -8,6 +9,8 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
|||||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
import { Labels } from 'types/api/alerts/def';
|
import { Labels } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
|
||||||
|
|
||||||
export interface ICreateAlertContextProps {
|
export interface ICreateAlertContextProps {
|
||||||
alertState: AlertState;
|
alertState: AlertState;
|
||||||
setAlertState: Dispatch<CreateAlertAction>;
|
setAlertState: Dispatch<CreateAlertAction>;
|
||||||
@ -36,11 +39,22 @@ export interface ICreateAlertContextProps {
|
|||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
discardAlertRule: () => void;
|
discardAlertRule: () => void;
|
||||||
|
isUpdatingAlertRule: boolean;
|
||||||
|
updateAlertRule: UseMutateFunction<
|
||||||
|
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
isEditMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateAlertProviderProps {
|
export interface ICreateAlertProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
initialAlertType: AlertTypes;
|
initialAlertType: AlertTypes;
|
||||||
|
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
|
||||||
|
isEditMode?: boolean;
|
||||||
|
ruleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AlertCreationStep {
|
export enum AlertCreationStep {
|
||||||
@ -60,6 +74,7 @@ export type CreateAlertAction =
|
|||||||
| { type: 'SET_ALERT_NAME'; payload: string }
|
| { type: 'SET_ALERT_NAME'; payload: string }
|
||||||
| { type: 'SET_ALERT_LABELS'; payload: Labels }
|
| { type: 'SET_ALERT_LABELS'; payload: Labels }
|
||||||
| { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined }
|
| { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined }
|
||||||
|
| { type: 'SET_INITIAL_STATE'; payload: AlertState }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|
||||||
export interface Threshold {
|
export interface Threshold {
|
||||||
@ -127,6 +142,7 @@ export type AlertThresholdAction =
|
|||||||
| { type: 'SET_ALGORITHM'; payload: string }
|
| { type: 'SET_ALGORITHM'; payload: string }
|
||||||
| { type: 'SET_SEASONALITY'; payload: string }
|
| { type: 'SET_SEASONALITY'; payload: string }
|
||||||
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
||||||
|
| { type: 'SET_INITIAL_STATE'; payload: AlertThresholdState }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|
||||||
export interface AdvancedOptionsState {
|
export interface AdvancedOptionsState {
|
||||||
@ -198,6 +214,7 @@ export type AdvancedOptionsAction =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||||
|
| { type: 'SET_INITIAL_STATE'; payload: AdvancedOptionsState }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|
||||||
export interface EvaluationWindowState {
|
export interface EvaluationWindowState {
|
||||||
@ -219,6 +236,7 @@ export type EvaluationWindowAction =
|
|||||||
payload: { time: string; number: string; timezone: string; unit: string };
|
payload: { time: string; number: string; timezone: string; unit: string };
|
||||||
}
|
}
|
||||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||||
|
| { type: 'SET_INITIAL_STATE'; payload: EvaluationWindowState }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|
||||||
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
||||||
@ -229,7 +247,7 @@ export interface NotificationSettingsState {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
conditions: ('firing' | 'no-data')[];
|
conditions: ('firing' | 'nodata')[];
|
||||||
};
|
};
|
||||||
description: string;
|
description: string;
|
||||||
routingPolicies: boolean;
|
routingPolicies: boolean;
|
||||||
@ -246,9 +264,10 @@ export type NotificationSettingsAction =
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
conditions: ('firing' | 'no-data')[];
|
conditions: ('firing' | 'nodata')[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'SET_DESCRIPTION'; payload: string }
|
| { type: 'SET_DESCRIPTION'; payload: string }
|
||||||
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
||||||
|
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|||||||
@ -53,6 +53,8 @@ export const alertCreationReducer = (
|
|||||||
};
|
};
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_ALERT_STATE;
|
return INITIAL_ALERT_STATE;
|
||||||
|
case 'SET_INITIAL_STATE':
|
||||||
|
return action.payload;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -119,6 +121,8 @@ export const alertThresholdReducer = (
|
|||||||
return { ...state, thresholds: action.payload };
|
return { ...state, thresholds: action.payload };
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_ALERT_THRESHOLD_STATE;
|
return INITIAL_ALERT_THRESHOLD_STATE;
|
||||||
|
case 'SET_INITIAL_STATE':
|
||||||
|
return action.payload;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -174,6 +178,8 @@ export const advancedOptionsReducer = (
|
|||||||
...state,
|
...state,
|
||||||
evaluationCadence: { ...state.evaluationCadence, mode: action.payload },
|
evaluationCadence: { ...state.evaluationCadence, mode: action.payload },
|
||||||
};
|
};
|
||||||
|
case 'SET_INITIAL_STATE':
|
||||||
|
return action.payload;
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_ADVANCED_OPTIONS_STATE;
|
return INITIAL_ADVANCED_OPTIONS_STATE;
|
||||||
default:
|
default:
|
||||||
@ -202,6 +208,8 @@ export const evaluationWindowReducer = (
|
|||||||
return { ...state, startingAt: action.payload };
|
return { ...state, startingAt: action.payload };
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_EVALUATION_WINDOW_STATE;
|
return INITIAL_EVALUATION_WINDOW_STATE;
|
||||||
|
case 'SET_INITIAL_STATE':
|
||||||
|
return action.payload;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -222,6 +230,8 @@ export const notificationSettingsReducer = (
|
|||||||
return { ...state, routingPolicies: action.payload };
|
return { ...state, routingPolicies: action.payload };
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||||
|
case 'SET_INITIAL_STATE':
|
||||||
|
return action.payload;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,21 @@
|
|||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdvancedOptionsState,
|
||||||
|
AlertState,
|
||||||
|
AlertThresholdState,
|
||||||
|
EvaluationWindowState,
|
||||||
|
NotificationSettingsState,
|
||||||
|
} from './context/types';
|
||||||
|
|
||||||
export interface CreateAlertV2Props {
|
export interface CreateAlertV2Props {
|
||||||
alertType: AlertTypes;
|
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 { 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 { createPortal } from 'react-dom';
|
||||||
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useCreateAlertState } from './context';
|
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
|
// UI side feature flag
|
||||||
export const showNewCreateAlertsPage = (): boolean =>
|
export const showNewCreateAlertsPage = (): boolean =>
|
||||||
@ -11,12 +35,12 @@ export const showNewCreateAlertsPage = (): boolean =>
|
|||||||
// Layout 1 - Default layout
|
// Layout 1 - Default layout
|
||||||
// Layout 2 - Condensed layout
|
// Layout 2 - Condensed layout
|
||||||
export const showCondensedLayout = (): boolean =>
|
export const showCondensedLayout = (): boolean =>
|
||||||
localStorage.getItem('showCondensedLayout') === 'true';
|
localStorage.getItem('hideCondensedLayout') !== 'true';
|
||||||
|
|
||||||
export function Spinner(): JSX.Element | null {
|
export function Spinner(): JSX.Element | null {
|
||||||
const { isCreatingAlertRule } = useCreateAlertState();
|
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
|
||||||
|
|
||||||
if (!isCreatingAlertRule) return null;
|
if (!isCreatingAlertRule && !isUpdatingAlertRule) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="sticky-page-spinner">
|
<div className="sticky-page-spinner">
|
||||||
@ -25,3 +49,263 @@ export function Spinner(): JSX.Element | null {
|
|||||||
document.body,
|
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 { Form } from 'antd';
|
||||||
|
import EditAlertV2 from 'container/EditAlertV2';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
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';
|
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();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
|
if (
|
||||||
|
initialV2AlertValue !== null &&
|
||||||
|
initialV2AlertValue.schemaVersion === NEW_ALERT_SCHEMA_VERSION
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<EditAlertV2
|
||||||
|
initialAlert={initialV2AlertValue}
|
||||||
|
alertType={initialValue.alertType as AlertTypes}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAlertRules
|
<FormAlertRules
|
||||||
alertType={
|
alertType={
|
||||||
@ -23,6 +44,7 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
|||||||
interface EditRulesProps {
|
interface EditRulesProps {
|
||||||
initialValue: AlertDef;
|
initialValue: AlertDef;
|
||||||
ruleId: string;
|
ruleId: string;
|
||||||
|
initialV2AlertValue: PostableAlertRuleV2 | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditRules;
|
export default EditRules;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
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 type { ColumnsType } from 'antd/es/table/interface';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
@ -31,7 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
|||||||
import { GettableAlert } from 'types/api/alerts/get';
|
import { GettableAlert } from 'types/api/alerts/get';
|
||||||
|
|
||||||
import DeleteAlert from './DeleteAlert';
|
import DeleteAlert from './DeleteAlert';
|
||||||
import { Button, ColumnButton, SearchContainer } from './styles';
|
import { ColumnButton, SearchContainer } from './styles';
|
||||||
import Status from './TableComponents/Status';
|
import Status from './TableComponents/Status';
|
||||||
import ToggleAlertState from './ToggleAlertState';
|
import ToggleAlertState from './ToggleAlertState';
|
||||||
import { alertActionLogEvent, filterAlerts } from './utils';
|
import { alertActionLogEvent, filterAlerts } from './utils';
|
||||||
@ -97,14 +97,37 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, [notificationsApi, t]);
|
}, [notificationsApi, t]);
|
||||||
|
|
||||||
const onClickNewAlertHandler = useCallback(() => {
|
const onClickNewAlertV2Handler = useCallback(() => {
|
||||||
logEvent('Alert: New alert button clicked', {
|
logEvent('Alert: New alert button clicked', {
|
||||||
number: allAlertRules?.length,
|
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);
|
history.push(ROUTES.ALERTS_NEW);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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 onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||||
params.set(
|
params.set(
|
||||||
@ -368,13 +391,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
<Flex gap={12}>
|
<Flex gap={12}>
|
||||||
{addNewAlert && (
|
{addNewAlert && (
|
||||||
<Button
|
<Dropdown menu={{ items: newAlertMenuItems }} trigger={['click']}>
|
||||||
type="primary"
|
<Button type="primary" icon={<PlusOutlined />}>
|
||||||
onClick={onClickNewAlertHandler}
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
>
|
|
||||||
New Alert
|
New Alert
|
||||||
</Button>
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
<TextToolTip
|
<TextToolTip
|
||||||
{...{
|
{...{
|
||||||
|
|||||||
@ -36,9 +36,7 @@ export function mapRoutingPolicyToCreateApiPayload(
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
expression,
|
expression,
|
||||||
actions: {
|
|
||||||
channels,
|
channels,
|
||||||
},
|
|
||||||
description,
|
description,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -53,9 +51,7 @@ export function mapRoutingPolicyToUpdateApiPayload(
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
expression,
|
expression,
|
||||||
actions: {
|
|
||||||
channels,
|
channels,
|
||||||
},
|
|
||||||
description,
|
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 RouteTab from 'components/RouteTab';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||||
|
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
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 AlertHeader from './AlertHeader/AlertHeader';
|
||||||
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||||
@ -85,6 +89,16 @@ function AlertDetails(): JSX.Element {
|
|||||||
document.title = alertTitle || document.title;
|
document.title = alertTitle || document.title;
|
||||||
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
|
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
|
||||||
|
|
||||||
|
const alertRuleDetails = useMemo(
|
||||||
|
() => alertDetailsResponse?.payload?.data as PostableAlertRuleV2 | undefined,
|
||||||
|
[alertDetailsResponse],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialAlertState = useMemo(
|
||||||
|
() => getCreateAlertLocalStateFromAlertDef(alertRuleDetails),
|
||||||
|
[alertRuleDetails],
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isError ||
|
isError ||
|
||||||
!isValidRuleId ||
|
!isValidRuleId ||
|
||||||
@ -104,6 +118,12 @@ function AlertDetails(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<CreateAlertProvider
|
||||||
|
ruleId={ruleId || ''}
|
||||||
|
isEditMode
|
||||||
|
initialAlertType={alertRuleDetails?.alertType as AlertTypes}
|
||||||
|
initialAlertState={initialAlertState}
|
||||||
|
>
|
||||||
<div className="alert-details">
|
<div className="alert-details">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
className="alert-details__breadcrumb"
|
className="alert-details__breadcrumb"
|
||||||
@ -134,6 +154,7 @@ function AlertDetails(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</CreateAlertProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
|||||||
import { useAlertRule } from 'providers/Alert';
|
import { useAlertRule } from 'providers/Alert';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { CSSProperties } from 'styled-components';
|
import { CSSProperties } from 'styled-components';
|
||||||
|
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||||
import { AlertDef } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
|
||||||
import { AlertHeaderProps } from '../AlertHeader';
|
import { AlertHeaderProps } from '../AlertHeader';
|
||||||
@ -60,7 +61,11 @@ function AlertActionButtons({
|
|||||||
setIsRenameAlertOpen(false);
|
setIsRenameAlertOpen(false);
|
||||||
}, [handleAlertUpdate]);
|
}, [handleAlertUpdate]);
|
||||||
|
|
||||||
|
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = [
|
const menuItems: MenuProps['items'] = [
|
||||||
|
...(!isV2Alert
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
key: 'rename-rule',
|
key: 'rename-rule',
|
||||||
label: 'Rename',
|
label: 'Rename',
|
||||||
@ -68,6 +73,8 @@ function AlertActionButtons({
|
|||||||
onClick: handleRename,
|
onClick: handleRename,
|
||||||
style: menuItemStyle,
|
style: menuItemStyle,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: 'duplicate-rule',
|
key: 'duplicate-rule',
|
||||||
label: 'Duplicate',
|
label: 'Duplicate',
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import './AlertHeader.styles.scss';
|
import './AlertHeader.styles.scss';
|
||||||
|
|
||||||
|
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
|
||||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||||
import { useAlertRule } from 'providers/Alert';
|
import { useAlertRule } from 'providers/Alert';
|
||||||
import { useMemo, useState } from 'react';
|
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 AlertActionButtons from './ActionButtons/ActionButtons';
|
||||||
import AlertLabels from './AlertLabels/AlertLabels';
|
import AlertLabels from './AlertLabels/AlertLabels';
|
||||||
@ -10,13 +16,7 @@ import AlertSeverity from './AlertSeverity/AlertSeverity';
|
|||||||
import AlertState from './AlertState/AlertState';
|
import AlertState from './AlertState/AlertState';
|
||||||
|
|
||||||
export type AlertHeaderProps = {
|
export type AlertHeaderProps = {
|
||||||
alertDetails: {
|
alertDetails: GettableAlert | PostableAlertRuleV2;
|
||||||
state: string;
|
|
||||||
alert: string;
|
|
||||||
id: string;
|
|
||||||
labels: Record<string, string | undefined> | undefined;
|
|
||||||
disabled: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||||
const { state, alert: alertName, labels } = alertDetails;
|
const { state, alert: alertName, labels } = alertDetails;
|
||||||
@ -32,12 +32,13 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
|||||||
return {};
|
return {};
|
||||||
}, [labels]);
|
}, [labels]);
|
||||||
|
|
||||||
return (
|
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||||
<div className="alert-info">
|
|
||||||
|
const CreateAlertV1Header = (
|
||||||
<div className="alert-info__info-wrapper">
|
<div className="alert-info__info-wrapper">
|
||||||
<div className="top-section">
|
<div className="top-section">
|
||||||
<div className="alert-title-wrapper">
|
<div className="alert-title-wrapper">
|
||||||
<AlertState state={alertRuleState ?? state} />
|
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||||
<div className="alert-title">
|
<div className="alert-title">
|
||||||
<LineClampedText text={updatedName || alertName} />
|
<LineClampedText text={updatedName || alertName} />
|
||||||
</div>
|
</div>
|
||||||
@ -54,10 +55,15 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
|||||||
<AlertLabels labels={labelsWithoutSeverity} />
|
<AlertLabels labels={labelsWithoutSeverity} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert-info">
|
||||||
|
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
|
||||||
<div className="alert-info__action-buttons">
|
<div className="alert-info__action-buttons">
|
||||||
<AlertActionButtons
|
<AlertActionButtons
|
||||||
alertDetails={alertDetails}
|
alertDetails={alertDetails}
|
||||||
ruleId={alertDetails.id}
|
ruleId={alertDetails?.id || ''}
|
||||||
setUpdatedName={setUpdatedName}
|
setUpdatedName={setUpdatedName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,10 @@ import history from 'lib/history';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import {
|
||||||
|
NEW_ALERT_SCHEMA_VERSION,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
} from 'types/api/alerts/alertTypesV2';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
errorMessageReceivedFromBackend,
|
errorMessageReceivedFromBackend,
|
||||||
@ -88,9 +92,18 @@ function EditRules(): JSX.Element {
|
|||||||
return <Spinner tip="Loading Rules..." />;
|
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 (
|
return (
|
||||||
<div className="edit-rules-container">
|
<div className="edit-rules-container">
|
||||||
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
|
<EditRulesContainer
|
||||||
|
ruleId={ruleId || ''}
|
||||||
|
initialValue={data.payload.data}
|
||||||
|
initialV2AlertValue={initialV2AlertValue}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,10 @@ export interface BasicThreshold {
|
|||||||
|
|
||||||
export interface PostableAlertRuleV2 {
|
export interface PostableAlertRuleV2 {
|
||||||
schemaVersion: string;
|
schemaVersion: string;
|
||||||
|
id?: string;
|
||||||
alert: string;
|
alert: string;
|
||||||
alertType: AlertTypes;
|
alertType?: AlertTypes;
|
||||||
ruleType: string;
|
ruleType?: string;
|
||||||
condition: {
|
condition: {
|
||||||
thresholds?: {
|
thresholds?: {
|
||||||
kind: string;
|
kind: string;
|
||||||
@ -28,13 +29,13 @@ export interface PostableAlertRuleV2 {
|
|||||||
requireMinPoints?: boolean;
|
requireMinPoints?: boolean;
|
||||||
requiredNumPoints?: number;
|
requiredNumPoints?: number;
|
||||||
};
|
};
|
||||||
evaluation: {
|
evaluation?: {
|
||||||
kind: 'rolling' | 'cumulative';
|
kind?: 'rolling' | 'cumulative';
|
||||||
spec: {
|
spec?: {
|
||||||
evalWindow?: string;
|
evalWindow?: string;
|
||||||
frequency: string;
|
frequency?: string;
|
||||||
schedule?: {
|
schedule?: {
|
||||||
type: 'hourly' | 'daily' | 'monthly';
|
type?: 'hourly' | 'daily' | 'monthly';
|
||||||
minute?: number;
|
minute?: number;
|
||||||
hour?: number;
|
hour?: number;
|
||||||
day?: number;
|
day?: number;
|
||||||
@ -42,19 +43,24 @@ export interface PostableAlertRuleV2 {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
labels: Labels;
|
labels?: Labels;
|
||||||
annotations: {
|
annotations?: {
|
||||||
description: string;
|
description: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
notificationSettings: {
|
notificationSettings?: {
|
||||||
notificationGroupBy: string[];
|
groupBy?: string[];
|
||||||
renotify?: string;
|
renotify?: {
|
||||||
alertStates: string[];
|
enabled: boolean;
|
||||||
notificationPolicy: boolean;
|
interval?: string;
|
||||||
|
alertStates?: string[];
|
||||||
};
|
};
|
||||||
version: string;
|
usePolicy?: boolean;
|
||||||
source: string;
|
};
|
||||||
|
version?: string;
|
||||||
|
source?: string;
|
||||||
|
state?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
||||||
@ -66,3 +72,5 @@ export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
|||||||
updateAt: string;
|
updateAt: string;
|
||||||
updateBy: string;
|
updateBy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NEW_ALERT_SCHEMA_VERSION = 'v2alpha1';
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export interface GettableAlert extends AlertDef {
|
|||||||
createBy: string;
|
createBy: string;
|
||||||
updateAt: string;
|
updateAt: string;
|
||||||
updateBy: string;
|
updateBy: string;
|
||||||
|
schemaVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayloadProps = {
|
export type PayloadProps = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user